├── .changeset ├── README.md └── config.json ├── .gitattributes ├── .github ├── changeset-publish.ts ├── changeset-version.ts ├── resolve-workspace-versions.ts ├── tsconfig.json ├── version-script.ts └── workflows │ ├── prerelease.yml │ ├── pullrequest.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── biome.json ├── docs └── guides │ ├── _index.md │ ├── ai-agents.md │ ├── alarms.md │ ├── auth.md │ ├── chat.md │ ├── composition.md │ ├── configuration.md │ ├── debugging.md │ ├── durable-objects.md │ ├── full-stack.md │ ├── games.md │ ├── how it works.md │ ├── migrating-from-partykit.md │ ├── multi-parties.md │ ├── multiplayer.md │ ├── observability.md │ ├── presence.md │ ├── pubsub.md │ ├── quickstart.md │ ├── rate-limiting.md │ ├── routing.md │ ├── rpc.md │ ├── scaling.md │ ├── storage.md │ ├── testing.md │ └── text-editors.md ├── fixtures ├── chat │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── tsconfig.json │ │ ├── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ └── types.ts │ ├── vite.config.ts │ └── wrangler.json ├── globe │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── normalize.css │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── tsconfig.json │ │ ├── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ └── types.ts │ ├── vite.config.ts │ └── wrangler.toml ├── hono │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── client.ts │ │ └── server.ts │ ├── vite.config.ts │ └── wrangler.toml ├── lexical-yjs │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── lexical.css │ │ └── reset.css │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ └── tsconfig.json │ │ └── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml ├── monaco-yjs │ ├── package.json │ ├── public │ │ └── index.html │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ └── tsconfig.json │ │ └── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ └── wrangler.toml ├── node │ ├── README.md │ ├── package.json │ ├── run-with-node.mjs │ ├── src │ │ └── server.ts │ ├── tsconfig.json │ └── wrangler.toml ├── partytracks │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── normalize.css │ ├── source-flow.md │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── tsconfig.json │ │ └── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml ├── pubsub │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ └── tsconfig.json │ │ └── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml ├── rpc-sanity │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── tsconfig.json │ └── wrangler.toml ├── tiptap-yjs │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── tsconfig.json │ │ └── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml ├── tldraw │ ├── CHANGELOG.md │ ├── README.md │ ├── client │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── tsconfig.json │ │ └── useSyncStore.ts │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── index.ts │ │ └── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml ├── todo-sync │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── normalize.css │ ├── src │ │ ├── client │ │ │ ├── index.tsx │ │ │ ├── styles.css │ │ │ └── tsconfig.json │ │ ├── server │ │ │ ├── index.ts │ │ │ └── tsconfig.json │ │ └── shared.ts │ ├── vite.config.ts │ └── wrangler.toml └── video-echo │ ├── README.md │ ├── app │ ├── components │ │ └── Demo.client.tsx │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── hooks │ │ └── useIsServer.ts │ ├── root.tsx │ ├── routes │ │ ├── _index.tsx │ │ └── partytracks.$.tsx │ └── server.ts │ ├── package.json │ ├── public │ └── favicon.ico │ ├── remix.config.js │ ├── tsconfig.json │ └── wrangler.toml ├── package-lock.json ├── package.json ├── packages ├── hono-party │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── partyagent │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partybase │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partyflow │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partyfn │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ └── src │ │ ├── index.ts │ │ └── tsconfig.json ├── partyhard │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partyserver │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ ├── src │ │ ├── connection.ts │ │ ├── index.ts │ │ ├── tests │ │ │ ├── index.test.ts │ │ │ ├── tsconfig.json │ │ │ ├── vitest.config.ts │ │ │ ├── worker.ts │ │ │ └── wrangler.toml │ │ └── types.ts │ └── tsconfig.json ├── partysession │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partysmart │ ├── CHANGELOG.md │ ├── README.md │ └── package.json ├── partysocket │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── TODO.md │ ├── package.json │ ├── scripts │ │ └── build.mts │ └── src │ │ ├── event-target-polyfill.ts │ │ ├── index.ts │ │ ├── react.ts │ │ ├── tests │ │ ├── fixture.js │ │ ├── partysocket.test.ts │ │ ├── reconnecting-node.test.ts │ │ └── reconnecting.test.ts │ │ ├── tsconfig.json │ │ ├── type-helper.ts │ │ ├── use-handlers.ts │ │ ├── use-socket.ts │ │ ├── use-ws.ts │ │ └── ws.ts ├── partysub │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ └── src │ │ ├── client │ │ ├── index.ts │ │ ├── react.tsx │ │ └── tsconfig.json │ │ └── server │ │ ├── gen-ids.ts │ │ ├── index.ts │ │ ├── tests │ │ ├── gen-ids.test.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ ├── worker.ts │ │ └── wrangler.toml │ │ └── tsconfig.json ├── partysync │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ └── src │ │ ├── agent │ │ ├── index.ts │ │ └── tsconfig.json │ │ ├── client │ │ ├── index.ts │ │ ├── persist.ts │ │ └── tsconfig.json │ │ ├── react │ │ ├── index.tsx │ │ └── tsconfig.json │ │ ├── server │ │ ├── index.ts │ │ └── tsconfig.json │ │ └── types.ts ├── partytracks │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ └── build.ts │ ├── src │ │ ├── client │ │ │ ├── History.ts │ │ │ ├── PartyTracks.ts │ │ │ ├── Peer.utils.ts │ │ │ ├── audioSink.ts │ │ │ ├── blackCanvasTrack$.ts │ │ │ ├── callsTypes.ts │ │ │ ├── debug.ts │ │ │ ├── deviceManager.ts │ │ │ ├── fromFetch.ts │ │ │ ├── getDevices.ts │ │ │ ├── getScreenshare.ts │ │ │ ├── inaudibleTrack$.ts │ │ │ ├── index.ts │ │ │ ├── logging.ts │ │ │ ├── makeBroadcastTrack.ts │ │ │ ├── permission$.ts │ │ │ ├── resilientTrack$.ts │ │ │ ├── rxjs-helpers.ts │ │ │ ├── screenshare$.ts │ │ │ └── trackIsHealthy.ts │ │ ├── react │ │ │ ├── index.ts │ │ │ └── rxjsHooks.ts │ │ ├── server │ │ │ ├── index.ts │ │ │ └── routePartyTracksRequest.ts │ │ └── ts-utils.ts │ ├── tests │ │ └── Peer.utils.test.ts │ └── tsconfig.json ├── partywhen │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── build.ts │ │ └── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── tsconfig.json │ └── tests │ │ ├── env.d.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── tsconfig.json │ │ ├── vitest.config.ts │ │ └── wrangler.toml └── y-partyserver │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ ├── build.ts │ └── tsconfig.json │ └── src │ ├── provider │ ├── index.ts │ ├── react.tsx │ └── tsconfig.json │ ├── server │ ├── index.ts │ └── tsconfig.json │ └── shared │ ├── chunking.ts │ └── tsconfig.json ├── random ├── check-export-types-with-imports.ts ├── check-export-types.ts └── tsconfig.json ├── scripts ├── tsconfig.json └── typecheck.ts └── tsconfig.base.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "cloudflare/partykit" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/changeset-publish.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | 3 | execSync("npx tsx ./.github/resolve-workspace-versions.ts"); 4 | execSync("npx changeset publish"); 5 | -------------------------------------------------------------------------------- /.github/changeset-version.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | 3 | // This script is used by the `release.yml` workflow to update the version of the packages being released. 4 | // The standard step is only to run `changeset version` but this does not update the package-lock.json file. 5 | // So we also run `npm install`, which does this update. 6 | // This is a workaround until this is handled automatically by `changeset version`. 7 | // See https://github.com/changesets/changesets/issues/421. 8 | execSync("npx changeset version"); 9 | execSync("npm install"); 10 | -------------------------------------------------------------------------------- /.github/resolve-workspace-versions.ts: -------------------------------------------------------------------------------- 1 | // this looks for all package.jsons in /packages/**/package.json 2 | // and replaces it with the actual version ids 3 | 4 | import * as fs from "node:fs"; 5 | import fg from "fast-glob"; 6 | 7 | // we do this in 2 passes 8 | // first let's cycle through all packages and get thier version numbers 9 | 10 | // biome-ignore lint/suspicious/noExplicitAny: 11 | const packageJsons: Record = {}; 12 | 13 | for await (const file of await fg.glob( 14 | "./(packages|fixtures)/*/package.json" 15 | )) { 16 | const packageJson = JSON.parse(fs.readFileSync(file, "utf8")); 17 | packageJsons[packageJson.name] = { 18 | file, 19 | packageJson 20 | }; 21 | } 22 | 23 | // then we'll revisit them, and replace any "workspace:*" references 24 | // with "^(actual version)" 25 | 26 | for (const [packageName, { file, packageJson }] of Object.entries( 27 | packageJsons 28 | )) { 29 | let changed = false; 30 | for (const field of [ 31 | "dependencies", 32 | "devDependencies", 33 | "peerDependencies", 34 | "optionalDependencies" 35 | ]) { 36 | for (const [dependencyName, dependencyVersion] of Object.entries( 37 | packageJson[field] || {} 38 | )) { 39 | if (dependencyName in packageJsons) { 40 | let actualVersion = packageJsons[dependencyName].packageJson.version; 41 | if (!actualVersion.startsWith("0.0.0-")) { 42 | actualVersion = `^${actualVersion}`; 43 | } 44 | 45 | console.log( 46 | `${packageName}: setting ${field}.${dependencyName} to ${actualVersion}` 47 | ); 48 | packageJson[field][dependencyName] = actualVersion; 49 | changed = true; 50 | } 51 | } 52 | } 53 | if (changed) { 54 | fs.writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/version-script.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import { execSync } from "node:child_process"; 3 | try { 4 | console.log("Getting current git hash..."); 5 | const stdout = execSync("git rev-parse --short HEAD").toString(); 6 | 7 | for (const path of [ 8 | "./packages/partyserver/package.json", 9 | "./packages/partysocket/package.json", 10 | "./packages/y-partyserver/package.json", 11 | "./packages/partysub/package.json", 12 | "./packages/partyfn/package.json", 13 | "./packages/partysync/package.json", 14 | "./packages/partywhen/package.json", 15 | "./packages/partytracks/package.json", 16 | "./packages/hono-party/package.json" 17 | ]) { 18 | const packageJson = JSON.parse(fs.readFileSync(path, "utf-8")); 19 | packageJson.version = `0.0.0-${stdout.trim()}`; 20 | fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`); 21 | } 22 | } catch (error) { 23 | console.error(error); 24 | process.exit(1); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Prerelease 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | prerelease: 10 | if: ${{ github.repository_owner == 'cloudflare' }} 11 | timeout-minutes: 5 12 | runs-on: ubuntu-24.04 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 1 18 | 19 | - uses: actions/setup-node@v4 20 | 21 | - run: npm install 22 | 23 | - name: Modify package.json version 24 | run: npx tsx .github/version-script.ts 25 | 26 | - name: Resolve workspace dependencies 27 | run: npx tsx .github/resolve-workspace-versions.ts 28 | 29 | - run: npm run build 30 | - run: npm run check 31 | 32 | - run: npm publish --tag beta 33 | env: 34 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 35 | working-directory: packages/partyserver 36 | 37 | - run: npm publish --tag beta 38 | env: 39 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 40 | working-directory: packages/partysocket 41 | 42 | - run: npm publish --tag beta 43 | env: 44 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 45 | working-directory: packages/partyfn 46 | 47 | - run: npm publish --tag beta 48 | env: 49 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 50 | working-directory: packages/y-partyserver 51 | 52 | - run: npm publish --tag beta 53 | env: 54 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 55 | working-directory: packages/partysub 56 | 57 | - run: npm publish --tag beta 58 | env: 59 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 60 | working-directory: packages/partysync 61 | 62 | - run: npm publish --tag beta 63 | env: 64 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 65 | working-directory: packages/partywhen 66 | 67 | - run: npm publish --tag beta 68 | env: 69 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 70 | working-directory: packages/hono-party 71 | -------------------------------------------------------------------------------- /.github/workflows/pullrequest.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | check: 7 | timeout-minutes: 5 8 | strategy: 9 | matrix: 10 | os: [ 11 | ubuntu-24.04 12 | # windows-latest, 13 | # macos-latest, 14 | ] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 1 20 | 21 | - uses: actions/setup-node@v4 22 | 23 | - run: npm install 24 | - run: npm run build 25 | - run: npm run check 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | 13 | if: ${{ github.repository_owner == 'cloudflare' }} 14 | timeout-minutes: 5 15 | runs-on: ubuntu-24.04 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 1 21 | 22 | - uses: actions/setup-node@v4 23 | 24 | - run: npm install 25 | - run: npm run build 26 | 27 | - id: changesets 28 | uses: changesets/action@v1 29 | with: 30 | version: npx tsx .github/changeset-version.ts 31 | publish: npx tsx .github/changeset-publish.ts 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.PARTYSERVER_GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 35 | NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | .wrangler 134 | .DS_Store 135 | .env 136 | .dev.vars 137 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_PUBLISH_TOKEN} 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "singleQuote": false, "trailingComma": "none" } 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Sunil Pai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🎈 PartyServer 2 | 3 | _Much like life, this is a Work in Progress._ 4 | 5 | Libraries / Examples / Documentation for building real-time apps (and more!) with [Cloudflare Workers](https://workers.cloudflare.com/). Powered by Durable Objects, Inspired by [PartyKit](https://www.partykit.io/). 6 | 7 | This is the main repository for PartyServer. It contains the following packages: 8 | 9 | [PartyServer](/packages/partyserver/README.md) - The core library for building real-time applications with Durable Objects. This library enhances a standard Durable Object to make it easier to work with WebSockets, as well as adding some additional features like lifecycle hooks and broadcasting. 10 | 11 | [PartySocket](/packages/partysocket/README.md) - A library for working with WebSockets. It features reconnections, buffering, resilience, and more. 12 | 13 | [Y-PartyServer](/packages/y-partyserver/README.md) - A library that adds Yjs support to PartyServer. Yjs is a CRDT library that allows for real-time collaborative editing. This library exposes a base class extending PartyServer that adds Yjs support, with utility hooks for loading/saving Yjs documents from any external storage. 14 | 15 | [🥖 partysub](/packages/partysub/README.md) - A library for doing pubsub at scale with PartyServer. It lets you configure a "room" to be backed by N separate Durable Object instances, and a strategy for spreading the load across the world concentrated in configurable locations. 16 | 17 | [partysync](/packages/partysync/README.md) - An experimental library to synchronise state from a Durable Object to a client. 18 | 19 | [⏱️ partywhen](/packages/partywhen/README.md) - A library for scheduling tasks at scale. 20 | 21 | [hono-party](/packages/hono-party/README.md) - A middleware for Hono to handle PartyServer requests. 22 | 23 | ### Fixtures 24 | 25 | We have a set of small examples for PartyServer in the [`fixtures`](/fixtures/) directory. These are small, self-contained examples that demonstrate how to use PartyServer in different ways. We will expand this repository with broader (and deeper!) examples in the future. 26 | 27 | Reach out to [@threepointone on twitter](https://twitter.com/threepointone) or the [Cloudflare Workers Discord](https://discord.com/invite/cloudflaredev) to follow progress, and if you have any questions or feedback. 28 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [ 11 | "**/node_modules/**", 12 | "**/dist/**", 13 | "**/coverage/**", 14 | "**/*.d.ts", 15 | "**/*.d.mts", 16 | "./packages/partysocket/event-target-polyfill.*", 17 | "tsconfig.base.json", 18 | "**/.wrangler/**", 19 | "lexical.css" 20 | ] 21 | }, 22 | "formatter": { 23 | "enabled": false 24 | }, 25 | "organizeImports": { 26 | "enabled": false 27 | }, 28 | "linter": { 29 | "enabled": true, 30 | "rules": { 31 | "recommended": true, 32 | "style": { 33 | "noNonNullAssertion": "off", 34 | "noParameterAssign": "off", 35 | "noUselessElse": "off" 36 | }, 37 | "suspicious": { 38 | "noRedeclare": "off", 39 | "noConfusingVoidType": "off" 40 | }, 41 | "complexity": { 42 | "noForEach": "off" 43 | } 44 | } 45 | }, 46 | "javascript": { 47 | "formatter": { 48 | "quoteStyle": "double" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/guides/_index.md: -------------------------------------------------------------------------------- 1 | - [quickstart](quickstart.md) 2 | - [how it works](how-it-works.md) 3 | - [routing](routing.md) 4 | - [scaling](scaling.md) 5 | - [composition](composition.md) 6 | - [configuration](configuration.md) 7 | - [multiplayer](multiplayer.md) 8 | - [full stack](full-stack.md) 9 | - [more on durable objects](durable-objects.md) 10 | - [alarms](alarms.md) 11 | - [auth](auth.md) 12 | - [debugging](debugging.md) 13 | - [observability](observability.md) 14 | - [testing](testing.md) 15 | - [presence](presence.md) 16 | - [storage](storage.md) 17 | - [rate limiting](rate-limiting.md) 18 | - [rpc](rpc.md) 19 | - [multi party](multi-party.md) 20 | - [games](games.md) 21 | - [text editors](text-editors.md) 22 | - [chat](chat.md) 23 | - [ai agents](ai-agents.md) 24 | - [pubsub](pubsub.md) 25 | - [migrating from partykit](migrating-from-partykit.md) 26 | -------------------------------------------------------------------------------- /docs/guides/ai-agents.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/ai-agents.md -------------------------------------------------------------------------------- /docs/guides/alarms.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/alarms.md -------------------------------------------------------------------------------- /docs/guides/auth.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/auth.md -------------------------------------------------------------------------------- /docs/guides/chat.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/chat.md -------------------------------------------------------------------------------- /docs/guides/composition.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/composition.md -------------------------------------------------------------------------------- /docs/guides/configuration.md: -------------------------------------------------------------------------------- 1 | about wrangler.toml, migrations, etc 2 | -------------------------------------------------------------------------------- /docs/guides/debugging.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/debugging.md -------------------------------------------------------------------------------- /docs/guides/durable-objects.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/durable-objects.md -------------------------------------------------------------------------------- /docs/guides/full-stack.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/full-stack.md -------------------------------------------------------------------------------- /docs/guides/games.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/games.md -------------------------------------------------------------------------------- /docs/guides/how it works.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/how it works.md -------------------------------------------------------------------------------- /docs/guides/migrating-from-partykit.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/migrating-from-partykit.md -------------------------------------------------------------------------------- /docs/guides/multi-parties.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/multi-parties.md -------------------------------------------------------------------------------- /docs/guides/multiplayer.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/multiplayer.md -------------------------------------------------------------------------------- /docs/guides/observability.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/observability.md -------------------------------------------------------------------------------- /docs/guides/presence.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/presence.md -------------------------------------------------------------------------------- /docs/guides/pubsub.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/pubsub.md -------------------------------------------------------------------------------- /docs/guides/quickstart.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/quickstart.md -------------------------------------------------------------------------------- /docs/guides/rate-limiting.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/rate-limiting.md -------------------------------------------------------------------------------- /docs/guides/routing.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/routing.md -------------------------------------------------------------------------------- /docs/guides/rpc.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/rpc.md -------------------------------------------------------------------------------- /docs/guides/scaling.md: -------------------------------------------------------------------------------- 1 | - sharding / replicas 2 | -------------------------------------------------------------------------------- /docs/guides/storage.md: -------------------------------------------------------------------------------- 1 | using inbuilt storage, external solutions, etc 2 | -------------------------------------------------------------------------------- /docs/guides/testing.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/testing.md -------------------------------------------------------------------------------- /docs/guides/text-editors.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/docs/guides/text-editors.md -------------------------------------------------------------------------------- /fixtures/chat/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-chat 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 8 | - partyserver@0.0.71 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 15 | - partyserver@0.0.70 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 22 | - partyserver@0.0.69 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 29 | - partyserver@0.0.68 30 | 31 | ## 0.0.3 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 36 | - partyserver@0.0.67 37 | 38 | ## 0.0.2 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 43 | - partyserver@0.0.66 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 50 | - partyserver@0.0.65 51 | -------------------------------------------------------------------------------- /fixtures/chat/README.md: -------------------------------------------------------------------------------- 1 | ## chat 2 | 3 | A fixture showing the very basics of building a chat server: a server for every room, the server broadcasts every message to all clients in the room, and a client that connects to the room 4 | -------------------------------------------------------------------------------- /fixtures/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PartyServer 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-chat", 3 | "version": "0.0.7", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "@tailwindcss/vite": "^4.0.17", 11 | "nanoid": "^5.1.5", 12 | "partyserver": "^0.0.71", 13 | "partysocket": "^1.1.2", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0", 16 | "tailwindcss": "^4.0.17", 17 | "valibot": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^19.0.12", 21 | "@types/react-dom": "^19.0.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/chat/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/chat/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { nanoid } from "nanoid"; 4 | import { usePartySocket } from "partysocket/react"; 5 | 6 | import type { ChatMessage } from "../types"; 7 | 8 | import "./styles.css"; 9 | 10 | const randomNames = [ 11 | "Alice", 12 | "Bob", 13 | "Charlie", 14 | "David", 15 | "Eve", 16 | "Frank", 17 | "Grace", 18 | "Heidi", 19 | "Ivan", 20 | "Judy", 21 | "Mallory", 22 | "Oscar", 23 | "Peggy", 24 | "Trent", 25 | "Wendy" 26 | ]; 27 | 28 | const me = 29 | sessionStorage.getItem("me") ?? 30 | randomNames[Math.floor(Math.random() * randomNames.length)]; 31 | 32 | function App() { 33 | const [messages, setMessages] = useState>([]); 34 | const inputRef = useRef(null); 35 | const socket = usePartySocket({ 36 | room: "abc", 37 | party: "chat", 38 | onOpen() { 39 | console.log("Connected to the party!"); 40 | }, 41 | onMessage(evt) { 42 | console.log("Received a message:", evt.data); 43 | const message = JSON.parse(evt.data as string) as ChatMessage; 44 | setMessages((prevMessages) => [...prevMessages, message]); 45 | } 46 | }); 47 | return ( 48 | <> 49 |

50 | Hi {me}! 51 |

52 |
53 | {messages.map((message) => ( 54 |
55 | {message.sender}:   56 | {message.content} 57 |
58 | ))} 59 |
60 |
{ 62 | e.preventDefault(); 63 | if (inputRef.current) { 64 | socket.send( 65 | JSON.stringify({ 66 | id: nanoid(), 67 | type: "chat-message", 68 | content: inputRef.current.value, 69 | sender: me 70 | } satisfies ChatMessage) 71 | ); 72 | inputRef.current.value = ""; 73 | } 74 | }} 75 | > 76 | 77 |
78 | 79 | ); 80 | } 81 | 82 | const root = createRoot(document.getElementById("root")!); 83 | 84 | root.render(); 85 | -------------------------------------------------------------------------------- /fixtures/chat/src/client/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | /* @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; */ 6 | -------------------------------------------------------------------------------- /fixtures/chat/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/chat/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest, Server } from "partyserver"; 2 | 3 | import type { Connection, WSMessage } from "partyserver"; 4 | 5 | type Env = { Chat: DurableObjectNamespace }; 6 | 7 | export class Chat extends Server { 8 | static options = { hibernate: true }; 9 | 10 | onMessage(connection: Connection, message: WSMessage) { 11 | console.log("Received a message:", message); 12 | this.broadcast(message); 13 | } 14 | } 15 | 16 | export default { 17 | async fetch(request: Request, env: Env): Promise { 18 | return ( 19 | (await routePartykitRequest(request, env)) || 20 | new Response("Not Found", { status: 404 }) 21 | ); 22 | } 23 | } satisfies ExportedHandler; 24 | -------------------------------------------------------------------------------- /fixtures/chat/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/chat/src/types.ts: -------------------------------------------------------------------------------- 1 | import { literal, object, string } from "valibot"; 2 | 3 | import type { InferInput } from "valibot"; 4 | 5 | export const chatMessage = object({ 6 | type: literal("chat-message"), 7 | id: string(), 8 | content: string(), 9 | sender: string() 10 | }); 11 | 12 | export type ChatMessage = InferInput; 13 | -------------------------------------------------------------------------------- /fixtures/chat/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { cloudflare } from "@cloudflare/vite-plugin"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import react from "@vitejs/plugin-react"; 4 | import { defineConfig } from "vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [cloudflare(), react(), tailwindcss()] 8 | }); 9 | -------------------------------------------------------------------------------- /fixtures/chat/wrangler.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyserver-fixture-chat", 3 | "main": "src/server/index.ts", 4 | "compatibility_date": "2024-04-19", 5 | "assets": { "directory": "./public" }, 6 | "durable_objects": { "bindings": [{ "name": "Chat", "class_name": "Chat" }] }, 7 | "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Chat"] }] 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/globe/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-globe 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 8 | - partyserver@0.0.71 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 15 | - partyserver@0.0.70 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 22 | - partyserver@0.0.69 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 29 | - partyserver@0.0.68 30 | 31 | ## 0.0.3 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 36 | - partyserver@0.0.67 37 | 38 | ## 0.0.2 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 43 | - partyserver@0.0.66 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 50 | - partyserver@0.0.65 51 | -------------------------------------------------------------------------------- /fixtures/globe/README.md: -------------------------------------------------------------------------------- 1 | ## globe 2 | 3 | A fixture showing how to implement a spinning globe with live vistor indicators. 4 | -------------------------------------------------------------------------------- /fixtures/globe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Globe 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Loading...
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /fixtures/globe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-globe", 3 | "private": true, 4 | "version": "0.0.7", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "@types/react": "^19.0.12", 11 | "@types/react-dom": "^19.0.4", 12 | "cobe": "^0.6.3", 13 | "partyserver": "^0.0.71", 14 | "partysocket": "^1.1.2", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0" 17 | }, 18 | "devDependencies": { 19 | "@cloudflare/workers-types": "^4.20250327.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/globe/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/globe/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/globe/src/client/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | We've already included normalize.css. 3 | 4 | But we'd like a modern looking boilerplate. 5 | Clean type, sans-serif, and a nice color palette. 6 | 7 | */ 8 | 9 | body { 10 | font-family: sans-serif; 11 | font-size: 16px; 12 | line-height: 1.5; 13 | color: #999; 14 | background-color: black; 15 | text-align: center; 16 | padding-top: 40px; 17 | } 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | font-family: sans-serif; 26 | font-weight: 600; 27 | line-height: 1.25; 28 | margin-top: 0; 29 | margin-bottom: 0.5rem; 30 | } 31 | 32 | #app { 33 | padding: 1rem; 34 | } 35 | 36 | h1 { 37 | color: white; 38 | } 39 | 40 | b { 41 | color: white; 42 | font-weight: bold; 43 | } 44 | 45 | a { 46 | color: #ccc; 47 | text-decoration-line: underline; 48 | text-underline-offset: 3px; 49 | text-decoration-color: #555; 50 | } 51 | -------------------------------------------------------------------------------- /fixtures/globe/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/globe/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest, Server } from "partyserver"; 2 | 3 | import type { OutgoingMessage, Position } from "../types"; 4 | import type { Connection, ConnectionContext } from "partyserver"; 5 | 6 | type Env = { Globe: DurableObjectNamespace }; 7 | 8 | // This is the state that we'll store on each connection 9 | type ConnectionState = { position: Position }; 10 | 11 | export class Globe extends Server { 12 | // Let's use hibernation mode so we can scale to thousands of connections 13 | static options = { hibernate: true }; 14 | 15 | onConnect(conn: Connection, ctx: ConnectionContext) { 16 | // Whenever a fresh connection is made, we'll 17 | // send the entire state to the new connection 18 | 19 | // First, let's extract the position from the Cloudflare headers 20 | const { request } = ctx; 21 | const lat = Number.parseFloat(request.cf!.latitude as string); 22 | const lng = Number.parseFloat(request.cf!.longitude as string); 23 | const id = conn.id; 24 | // And save this on the connection's state 25 | conn.setState({ position: { lat, lng, id } }); 26 | 27 | // Now, let's send the entire state to the new connection 28 | for (const connection of this.getConnections()) { 29 | try { 30 | conn.send( 31 | JSON.stringify({ 32 | type: "add-marker", 33 | position: connection.state!.position 34 | } satisfies OutgoingMessage) 35 | ); 36 | 37 | // And let's send the new connection's position to all other connections 38 | if (connection.id !== conn.id) { 39 | connection.send( 40 | JSON.stringify({ 41 | type: "add-marker", 42 | position: conn.state!.position 43 | } satisfies OutgoingMessage) 44 | ); 45 | } 46 | } catch (err) { 47 | this.onCloseOrError(conn); 48 | } 49 | } 50 | } 51 | 52 | // Whenever a connection closes (or errors), 53 | // we'll broadcast a message to all other connections 54 | // to remove the marker 55 | onCloseOrError(connection: Connection) { 56 | this.broadcast( 57 | JSON.stringify({ 58 | type: "remove-marker", 59 | id: connection.id 60 | } satisfies OutgoingMessage), 61 | [connection.id] 62 | ); 63 | } 64 | 65 | onClose(connection: Connection): void | Promise { 66 | this.onCloseOrError(connection); 67 | } 68 | onError( 69 | connection: Connection, 70 | _error: Error 71 | ): void | Promise { 72 | this.onCloseOrError(connection); 73 | } 74 | } 75 | 76 | export default { 77 | async fetch(request: Request, env: Env): Promise { 78 | return ( 79 | (await routePartykitRequest(request, env)) || 80 | new Response("Not Found", { status: 404 }) 81 | ); 82 | } 83 | } satisfies ExportedHandler; 84 | -------------------------------------------------------------------------------- /fixtures/globe/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/globe/src/types.ts: -------------------------------------------------------------------------------- 1 | // Messages that we'll send to the client 2 | 3 | // Representing a person's position 4 | export type Position = { 5 | lat: number; 6 | lng: number; 7 | id: string; 8 | }; 9 | 10 | export type OutgoingMessage = 11 | | { 12 | type: "add-marker"; 13 | position: Position; 14 | } 15 | | { 16 | type: "remove-marker"; 17 | id: string; 18 | }; 19 | -------------------------------------------------------------------------------- /fixtures/globe/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | 5 | export default defineConfig({ plugins: [cloudflare(), react()] }); 6 | -------------------------------------------------------------------------------- /fixtures/globe/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-globe" 2 | main = "src/server/index.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = {directory = "./public"} 6 | 7 | 8 | [[durable_objects.bindings]] 9 | name = "Globe" 10 | class_name = "Globe" 11 | 12 | [[migrations]] 13 | tag = "v1" # Should be unique for each entry 14 | new_classes = ["Globe"] 15 | -------------------------------------------------------------------------------- /fixtures/hono/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-hono 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 8 | - partyserver@0.0.71 9 | - hono-party@0.0.12 10 | 11 | ## 0.0.6 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 16 | - partyserver@0.0.70 17 | - hono-party@0.0.11 18 | 19 | ## 0.0.5 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 24 | - partyserver@0.0.69 25 | - hono-party@0.0.10 26 | 27 | ## 0.0.4 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 32 | - partyserver@0.0.68 33 | - hono-party@0.0.9 34 | 35 | ## 0.0.3 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 40 | - partyserver@0.0.67 41 | - hono-party@0.0.8 42 | 43 | ## 0.0.2 44 | 45 | ### Patch Changes 46 | 47 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 48 | - partyserver@0.0.66 49 | - hono-party@0.0.7 50 | 51 | ## 0.0.1 52 | 53 | ### Patch Changes 54 | 55 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 56 | - partyserver@0.0.65 57 | - hono-party@0.0.6 58 | -------------------------------------------------------------------------------- /fixtures/hono/README.md: -------------------------------------------------------------------------------- 1 | ## Hono 2 | 3 | A fixture showing how to integrate PartyServer with Hono. 4 | -------------------------------------------------------------------------------- /fixtures/hono/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | PartyServer 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /fixtures/hono/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-hono", 3 | "version": "0.0.7", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "hono": "^4.7.5", 11 | "hono-party": "^0.0.12", 12 | "partyserver": "^0.0.71", 13 | "partysocket": "^1.1.2", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.0.12", 19 | "@types/react-dom": "^19.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /fixtures/hono/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/hono/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/hono/src/client.ts: -------------------------------------------------------------------------------- 1 | import { PartySocket } from "partysocket"; 2 | 3 | const id = crypto.randomUUID(); 4 | 5 | const socket = new PartySocket({ 6 | host: window.location.host, 7 | party: "chat", 8 | room: "test" 9 | }); 10 | 11 | socket.addEventListener("message", (event) => { 12 | console.log("message", event.data); 13 | }); 14 | 15 | setInterval(() => { 16 | socket.send(`hello from ${id}`); 17 | }, 1000); 18 | -------------------------------------------------------------------------------- /fixtures/hono/src/server.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { partyserverMiddleware } from "hono-party"; 3 | import { Server } from "partyserver"; 4 | 5 | import type { Connection, WSMessage } from "partyserver"; 6 | 7 | // Multiple party servers 8 | export class Chat extends Server { 9 | onMessage(connection: Connection, message: WSMessage): void | Promise { 10 | console.log("onMessage", message); 11 | this.broadcast(message, [connection.id]); 12 | } 13 | } 14 | 15 | const app = new Hono(); 16 | app.use("*", partyserverMiddleware()); 17 | 18 | app.get("/", (c) => c.text("Hello from Hono!")); 19 | 20 | export default app; 21 | -------------------------------------------------------------------------------- /fixtures/hono/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | 5 | export default defineConfig({ plugins: [cloudflare(), react()] }); 6 | -------------------------------------------------------------------------------- /fixtures/hono/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-hono" 2 | main = "src/server.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = {directory = "./public"} 6 | 7 | [[durable_objects.bindings]] 8 | name = "Chat" 9 | class_name = "Chat" 10 | 11 | [[migrations]] 12 | tag = "v1" # Should be unique for each entry 13 | new_classes = ["Chat"] 14 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/README.md: -------------------------------------------------------------------------------- 1 | ## lexical-yjs 2 | 3 | A fixture showing [https://lexical.dev/](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/) integration with [partyserver](https://www.npmjs.com/package/partyserver)/[y-partyserver](https://www.npmjs.com/package/y-partyserver). 4 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-lexical-yjs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev", 8 | "deploy": "wrangler deploy" 9 | }, 10 | "dependencies": { 11 | "@lexical/react": "^0.29.0", 12 | "@types/react": "^19.0.12", 13 | "@types/react-dom": "^19.0.4", 14 | "lexical": "^0.29.0", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/lexical-yjs/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/lexical-yjs/public/reset.css: -------------------------------------------------------------------------------- 1 | /* Box sizing rules */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | /* Remove default margin */ 9 | body, 10 | h1, 11 | h2, 12 | h3, 13 | h4, 14 | p, 15 | figure, 16 | blockquote, 17 | dl, 18 | dd { 19 | margin: 0; 20 | } 21 | 22 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 23 | ul[role="list"], 24 | ol[role="list"] { 25 | list-style: none; 26 | } 27 | 28 | /* Set core root defaults */ 29 | html:focus-within { 30 | scroll-behavior: smooth; 31 | } 32 | 33 | /* Set core body defaults */ 34 | body { 35 | min-height: 100vh; 36 | text-rendering: optimizeSpeed; 37 | line-height: 1.5; 38 | } 39 | 40 | /* A elements that don't have a class get default styles */ 41 | a:not([class]) { 42 | text-decoration-skip-ink: auto; 43 | } 44 | 45 | /* Make images easier to work with */ 46 | img, 47 | picture { 48 | max-width: 100%; 49 | display: block; 50 | } 51 | 52 | /* Inherit fonts for inputs and buttons */ 53 | input, 54 | button, 55 | textarea, 56 | select { 57 | font: inherit; 58 | } 59 | 60 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ 61 | @media (prefers-reduced-motion: reduce) { 62 | html:focus-within { 63 | scroll-behavior: auto; 64 | } 65 | 66 | *, 67 | *::before, 68 | *::after { 69 | animation-duration: 0.01ms !important; 70 | animation-iteration-count: 1 !important; 71 | transition-duration: 0.01ms !important; 72 | scroll-behavior: auto !important; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { CollaborationPlugin } from "@lexical/react/LexicalCollaborationPlugin"; 3 | import { LexicalComposer } from "@lexical/react/LexicalComposer"; 4 | import { ContentEditable } from "@lexical/react/LexicalContentEditable"; 5 | import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; 6 | import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; 7 | import useYProvider from "y-partyserver/react"; 8 | 9 | function Editor() { 10 | const initialConfig = { 11 | editorState: null, 12 | namespace: "oops", 13 | nodes: [], 14 | onError(error: Error) { 15 | throw error; 16 | } 17 | }; 18 | 19 | const docName = "yjs"; 20 | 21 | const yProvider = useYProvider({ 22 | room: docName, 23 | prefix: `/parties/lexical-document/${docName}` 24 | }); 25 | 26 | return ( 27 | 28 | } 30 | placeholder={
Enter some text...
} 31 | ErrorBoundary={LexicalErrorBoundary} 32 | /> 33 | { 38 | yjsDocMap.set(id, yProvider.doc); 39 | return yProvider; 40 | }} 41 | shouldBootstrap={true} 42 | /> 43 |
44 | ); 45 | } 46 | 47 | const root = createRoot(document.getElementById("root")!); 48 | root.render(); 49 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest } from "partyserver"; 2 | import { YServer } from "y-partyserver"; 3 | 4 | // import * as Y from "yjs"; 5 | 6 | type Env = { 7 | Document: DurableObjectNamespace; 8 | }; 9 | 10 | export { YServer as LexicalDocument }; 11 | 12 | export default { 13 | async fetch(request: Request, env: Env): Promise { 14 | return ( 15 | (await routePartykitRequest(request, env)) || 16 | new Response("Not Found", { status: 404 }) 17 | ); 18 | } 19 | } satisfies ExportedHandler; 20 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { cloudflare } from "@cloudflare/vite-plugin"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import react from "@vitejs/plugin-react"; 4 | import { defineConfig } from "vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [cloudflare(), react(), tailwindcss()] 8 | }); 9 | -------------------------------------------------------------------------------- /fixtures/lexical-yjs/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = 'partyserver-fixture-lexical-yjs' 2 | main = 'src/server/index.ts' 3 | compatibility_date = "2024-07-25" 4 | 5 | assets = {directory = "./public"} 6 | 7 | [[durable_objects.bindings]] 8 | name = "LexicalDocument" 9 | class_name = "LexicalDocument" 10 | 11 | [[migrations]] 12 | tag = "v1" 13 | new_classes = ["LexicalDocument"] 14 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-monaco-yjs", 3 | "version": "0.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "start": "wrangler dev" 8 | }, 9 | "dependencies": { 10 | "monaco-editor": "^0.52.2", 11 | "partysocket": "^1.1.2", 12 | "y-monaco": "^0.1.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yjs Monaco Example 6 | 7 | 33 | 34 | 35 | 36 |

37 |

38 | This is a demo of the Yjs ⇔ 39 | Monaco binding: 40 | y-monaco. 41 |

42 |

43 | The content of this editor is shared with every client that visits this 44 | domain. 45 |

46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | 3 | await esbuild.build({ 4 | entryPoints: [ 5 | "./src/client/index.tsx", 6 | "monaco-editor/esm/vs/language/typescript/ts.worker", 7 | "monaco-editor/esm/vs/editor/editor.worker.js", 8 | "monaco-editor/esm/vs/language/json/json.worker", 9 | "monaco-editor/esm/vs/language/css/css.worker", 10 | "monaco-editor/esm/vs/language/html/html.worker" 11 | ], 12 | loader: { 13 | ".ttf": "file" 14 | }, 15 | splitting: false, 16 | bundle: true, 17 | format: "iife", 18 | platform: "browser", 19 | outdir: "public/dist" 20 | }); 21 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | import { WebSocket as BetterWebSocket } from "partysocket"; 3 | import { MonacoBinding } from "y-monaco"; 4 | import YProvider from "y-partyserver/provider"; 5 | import * as Y from "yjs"; 6 | 7 | window.MonacoEnvironment = { 8 | getWorkerUrl: (moduleId, label) => { 9 | if (label === "json") { 10 | // return "/dist/json.worker.bundle.js"; 11 | return "/dist/monaco-editor/esm/vs/language/json/json.js"; 12 | } 13 | if (label === "css" || label === "scss" || label === "less") { 14 | return "/dist/monaco-editor/esm/vs/language/css/css.js"; 15 | // return "/dist/css.worker.bundle.js"; 16 | } 17 | if (label === "html" || label === "handlebars" || label === "razor") { 18 | return "/dist/monaco-editor/esm/vs/language/html/html.js"; 19 | // return "/dist/html.worker.bundle.js"; 20 | } 21 | if (label === "typescript" || label === "javascript") { 22 | // return "/dist/ts.worker.bundle.js"; 23 | return "/dist/monaco-editor/esm/vs/language/typescript/ts.js"; 24 | } 25 | return "/dist/monaco-editor/esm/vs/editor/editor.worker.js"; 26 | // return "/dist/editor.worker.bundle.js"; 27 | } 28 | }; 29 | 30 | window.addEventListener("load", () => { 31 | const ydoc = new Y.Doc(); 32 | const provider = new YProvider(window.location.origin, "monaco-demo", ydoc, { 33 | party: "monaco", 34 | // @ts-expect-error I don't know typescript 35 | WebSocketPolyfill: BetterWebSocket 36 | }); 37 | 38 | provider.ws?.send("do-the-thing"); 39 | 40 | const type = ydoc.getText("monaco"); 41 | 42 | const editor = monaco.editor.create( 43 | /** @type {HTMLElement} */ document.getElementById("monaco-editor")!, 44 | { 45 | value: "", 46 | language: "javascript", 47 | theme: "vs-dark" 48 | } 49 | ); 50 | new MonacoBinding( 51 | type, 52 | /** @type {monaco.editor.ITextModel} */ editor.getModel()!, 53 | new Set([editor]), 54 | provider.awareness 55 | ); 56 | 57 | const connectBtn = 58 | /** @type {HTMLElement} */ document.getElementById("y-connect-btn")!; 59 | connectBtn.addEventListener("click", () => { 60 | if (provider.shouldConnect) { 61 | provider.disconnect(); 62 | connectBtn.textContent = "Connect"; 63 | } else { 64 | provider.connect().catch(console.error); 65 | connectBtn.textContent = "Disconnect"; 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "lib": ["DOM"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest } from "partyserver"; 2 | import { YServer } from "y-partyserver"; 3 | 4 | type Env = { 5 | MonacoServer: DurableObjectNamespace; 6 | }; 7 | 8 | export { YServer as MonacoServer }; 9 | 10 | export default { 11 | async fetch(request: Request, env: Env): Promise { 12 | return ( 13 | (await routePartykitRequest(request, env)) || 14 | new Response("Not Found", { status: 404 }) 15 | ); 16 | } 17 | } satisfies ExportedHandler; 18 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/monaco-yjs/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = 'partyserver-fixture-monaco-yjs' 2 | main = 'src/server/index.ts' 3 | compatibility_date = "2024-07-25" 4 | 5 | assets = {directory = "./public"} 6 | 7 | [build] 8 | command = "npx tsx scripts/build.ts" 9 | 10 | [[durable_objects.bindings]] 11 | name = "Monaco" 12 | class_name = "MonacoServer" 13 | 14 | [[migrations]] 15 | tag = "v1" 16 | new_classes = ["MonacoServer"] 17 | -------------------------------------------------------------------------------- /fixtures/node/README.md: -------------------------------------------------------------------------------- 1 | ## node 2 | 3 | A simple fixture showing how you can connect to a PartyServer instance frnom node.js (and other environments that support WebSockets). Note in this example we use the `ws` package to connect to the PartyServer instance, but in newer versions of node.js you can use the built-in `WebSocket` class. 4 | -------------------------------------------------------------------------------- /fixtures/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-node", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "wrangler dev" 7 | }, 8 | "dependencies": { 9 | "partysocket": "^1.1.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/node/run-with-node.mjs: -------------------------------------------------------------------------------- 1 | import PartySocket from "partysocket"; 2 | import WS from "ws"; 3 | 4 | const ps = new PartySocket({ 5 | host: "127.0.0.1:8787", 6 | room: "test", 7 | party: "my-server", 8 | WebSocket: WS 9 | // debug: true, 10 | // debugLogger: (_arg, ...args) => { 11 | // console.log(...args); 12 | // } 13 | }); 14 | 15 | ps.addEventListener("open", () => { 16 | console.log("connected"); 17 | ps.send("hello"); 18 | }); 19 | 20 | ps.addEventListener("message", (e) => { 21 | console.log("message (aEL):", e.data); 22 | }); 23 | 24 | ps.onmessage = (e) => { 25 | console.log("message (om):", e.data); 26 | }; 27 | 28 | ps.addEventListener("close", () => { 29 | console.log("closed"); 30 | }); 31 | 32 | ps.addEventListener("error", (e) => { 33 | console.log("error:", e.message); 34 | }); 35 | 36 | console.log("connecting..."); 37 | -------------------------------------------------------------------------------- /fixtures/node/src/server.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest, Server } from "partyserver"; 2 | 3 | import type { Connection, ConnectionContext } from "partyserver"; 4 | 5 | type Env = { 6 | MyServer: DurableObjectNamespace; 7 | }; 8 | 9 | export class MyServer extends Server { 10 | onConnect(conn: Connection, ctx: ConnectionContext) { 11 | // A websocket just connected! 12 | console.log( 13 | `Connected: 14 | id: ${conn.id} 15 | room: ${this.name} 16 | url: ${new URL(ctx.request.url).pathname}` 17 | ); 18 | 19 | // let's send a message to the connection 20 | conn.send("hello from server"); 21 | } 22 | 23 | onMessage(sender: Connection, message: string) { 24 | // let's log the message 25 | console.log(`connection ${sender.id} sent message: ${message}`); 26 | // as well as broadcast it to all the other connections in the room... 27 | this.broadcast( 28 | `${sender.id}: ${message}`, 29 | // ...except for the connection it came from 30 | [sender.id] 31 | ); 32 | } 33 | } 34 | 35 | export default { 36 | async fetch(request: Request, env: Env) { 37 | return ( 38 | (await routePartykitRequest(request, env)) || 39 | new Response("Not Found", { status: 404 }) 40 | ); 41 | } 42 | } satisfies ExportedHandler; 43 | -------------------------------------------------------------------------------- /fixtures/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/node/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-node" 2 | main = 'src/server.ts' 3 | compatibility_date = "2024-07-25" 4 | 5 | 6 | [[durable_objects.bindings]] 7 | name = "MyServer" 8 | class_name = "MyServer" 9 | 10 | [[migrations]] 11 | tag = "v1" 12 | new_classes = ["MyServer"] 13 | -------------------------------------------------------------------------------- /fixtures/partytracks/README.md: -------------------------------------------------------------------------------- 1 | ## partytracks 2 | 3 | A fixture showing how to use the partytracks package. 4 | -------------------------------------------------------------------------------- /fixtures/partytracks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | PartyTracks fixture 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /fixtures/partytracks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-partytracks", 3 | "private": true, 4 | "version": "0.0.21", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev", 8 | "dev": "vite dev" 9 | }, 10 | "dependencies": { 11 | "hono": "^4.7.5", 12 | "partytracks": "^0.0.47", 13 | "rxjs": "^7.8.2" 14 | }, 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^4.20250327.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fixtures/partytracks/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/partytracks/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/partytracks/source-flow.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | flowchart TD 3 | SourceContent[Source Content] 4 | FallbackContent[Fallback Content] 5 | IsSourceEnabled$((isSourceEnabled$)) 6 | IsBroadcasting$((isBroadcasting$)) 7 | LocalMonitorTrack((localMonitorTrack$)) 8 | BroadcastTrack((broadcastTrack$)) 9 | Error[Error] 10 | Error$((error$)) 11 | EmitError[Emit Error] 12 | Cancelled[Cancelled] 13 | DisableSource[Disable Source] 14 | IsSourceEnabled$ -- No --> FallbackContent 15 | IsSourceEnabled$ -- Yes --> Error 16 | Error -- Yes --> EmitError 17 | Error$ ----> EmitError 18 | EmitError ----> DisableSource 19 | Error -- No --> Cancelled 20 | Cancelled -- Yes --> DisableSource 21 | Cancelled -- No --> SourceContent 22 | DisableSource ----> IsSourceEnabled$ 23 | LocalMonitorTrack ----> IsSourceEnabled$ 24 | BroadcastTrack ----> IsBroadcasting$ 25 | IsBroadcasting$ -- Yes --> IsSourceEnabled$ 26 | IsBroadcasting$ -- No --> FallbackContent 27 | ``` 28 | -------------------------------------------------------------------------------- /fixtures/partytracks/src/client/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | We've already included normalize.css. 3 | 4 | But we'd like a modern looking boilerplate. 5 | Clean type, sans-serif, and a nice color palette. 6 | 7 | */ 8 | 9 | body { 10 | font-family: sans-serif; 11 | font-size: 16px; 12 | color-scheme: light dark; 13 | line-height: 1.5; 14 | text-align: center; 15 | padding-top: 40px; 16 | } 17 | 18 | h1, 19 | h2, 20 | h3, 21 | h4, 22 | h5, 23 | h6 { 24 | font-family: sans-serif; 25 | font-weight: 600; 26 | line-height: 1.25; 27 | margin-top: 0; 28 | margin-bottom: 0.5rem; 29 | } 30 | 31 | #app { 32 | padding: 1rem; 33 | } 34 | 35 | video { 36 | width: 400px; 37 | } 38 | -------------------------------------------------------------------------------- /fixtures/partytracks/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/partytracks/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { routePartyTracksRequest } from "partytracks/server"; 3 | 4 | type Bindings = { 5 | REALTIME_SFU_APP_ID: string; 6 | REALTIME_SFU_APP_TOKEN: string; 7 | REALTIME_TURN_SERVER_APP_ID: string; 8 | REALTIME_TURN_SERVER_APP_TOKEN: string; 9 | }; 10 | 11 | const app = new Hono<{ Bindings: Bindings }>(); 12 | 13 | app.all("/partytracks/*", (c) => 14 | routePartyTracksRequest({ 15 | appId: c.env.REALTIME_SFU_APP_ID, 16 | token: c.env.REALTIME_SFU_APP_TOKEN, 17 | turnServerAppId: c.env.REALTIME_TURN_SERVER_APP_ID, 18 | turnServerAppToken: c.env.REALTIME_TURN_SERVER_APP_TOKEN, 19 | request: c.req.raw 20 | }) 21 | ); 22 | 23 | export default app; 24 | -------------------------------------------------------------------------------- /fixtures/partytracks/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/partytracks/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { cloudflare } from "@cloudflare/vite-plugin"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ plugins: [cloudflare(), react()] }); 6 | -------------------------------------------------------------------------------- /fixtures/partytracks/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-partytracks" 2 | main = "src/server/index.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = {directory = "./public"} 6 | -------------------------------------------------------------------------------- /fixtures/pubsub/README.md: -------------------------------------------------------------------------------- 1 | ## pubsub 2 | 3 | A simple pubsub server and client for Cloudflare Workers. 4 | -------------------------------------------------------------------------------- /fixtures/pubsub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PubSub 5 | 6 | 7 |

PubSub

8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fixtures/pubsub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-pubsub", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "nanoid": "^5.1.5", 11 | "partysocket": "^1.1.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/pubsub/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/pubsub/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/pubsub/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { nanoid } from "nanoid"; 4 | import { PartySocket } from "partysocket"; 5 | import { usePartySocket } from "partysocket/react"; 6 | 7 | function App() { 8 | const [messages, setMessages] = useState< 9 | { 10 | topic: string; 11 | data: string; 12 | }[] 13 | >([]); 14 | 15 | const [id] = useState(() => nanoid(8)); 16 | 17 | const socket = usePartySocket({ 18 | party: "pub-sub", 19 | room: "default", 20 | id, 21 | onMessage: (evt) => { 22 | setMessages((messages) => [ 23 | ...messages, 24 | JSON.parse(evt.data as string) as { 25 | topic: string; 26 | data: string; 27 | } 28 | ]); 29 | } 30 | }); 31 | 32 | useEffect(() => { 33 | setInterval(() => { 34 | socket.send( 35 | JSON.stringify({ 36 | topic: "example", 37 | data: `${socket.id} says hello!` 38 | }) 39 | ); 40 | 41 | PartySocket.fetch( 42 | { 43 | host: window.location.host, 44 | party: "pub-sub", // the name of the party, use the binding's lowercase form 45 | room: "default" // the name of the room/channel 46 | }, 47 | { 48 | method: "POST", 49 | body: JSON.stringify({ 50 | topic: "topic-abc", 51 | data: "hello from a post!" 52 | }) 53 | } 54 | ).catch(console.error); 55 | }, 2000); 56 | }, [socket]); 57 | 58 | return ( 59 |
60 | Messages: 61 | {messages.map((message, i) => ( 62 | // biome-ignore lint/suspicious/noArrayIndexKey: 63 |
{JSON.stringify(message)}
64 | ))} 65 |
66 | ); 67 | } 68 | 69 | createRoot(document.getElementById("root")!).render(); 70 | -------------------------------------------------------------------------------- /fixtures/pubsub/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/pubsub/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { createPubSubServer } from "partysub/server"; 2 | 3 | type Env = { 4 | PubSub: typeof PubSubServer; 5 | }; 6 | 7 | const { PubSubServer, routePubSubRequest } = createPubSubServer({ 8 | binding: "PubSub", 9 | nodes: 100 10 | }); 11 | 12 | export { PubSubServer }; 13 | 14 | export default { 15 | async fetch(req, env) { 16 | const pubsubResponse = await routePubSubRequest(req, env); 17 | return pubsubResponse || new Response("Not found", { status: 404 }); 18 | } 19 | } satisfies ExportedHandler; 20 | -------------------------------------------------------------------------------- /fixtures/pubsub/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/pubsub/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | import tailwindcss from "@tailwindcss/vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [cloudflare(), react(), tailwindcss()] 8 | }); 9 | -------------------------------------------------------------------------------- /fixtures/pubsub/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-node" 2 | main = 'src/server/index.ts' 3 | compatibility_date = "2024-07-25" 4 | 5 | assets = {directory = "./public"} 6 | 7 | 8 | [[durable_objects.bindings]] 9 | name = "PubSub" 10 | class_name = "PubSubServer" 11 | 12 | [[migrations]] 13 | tag = "v1" 14 | new_classes = ["PubSubServer"] 15 | -------------------------------------------------------------------------------- /fixtures/rpc-sanity/README.md: -------------------------------------------------------------------------------- 1 | ## rpc-sanity 2 | 3 | Just make sure rpc and/or getServerByName work as expected. 4 | -------------------------------------------------------------------------------- /fixtures/rpc-sanity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-rpc-sanity", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "start": "wrangler dev" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/rpc-sanity/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getServerByName, Server } from "partyserver"; 2 | 3 | type Env = { 4 | MyServer: DurableObjectNamespace; 5 | }; 6 | 7 | export class MyServer extends Server { 8 | async testMethod() { 9 | return this.name; 10 | } 11 | onRequest(request: Request): Response | Promise { 12 | const url = new URL(request.url); 13 | if (url.pathname !== "/test") { 14 | throw new Error("test onRequest"); 15 | } 16 | return new Response("test onRequest"); 17 | } 18 | } 19 | 20 | const SESSION_ID = "session-id"; 21 | 22 | export default { 23 | async fetch(request: Request, env: Env, _ctx: ExecutionContext) { 24 | const url = new URL(request.url); 25 | const stub = await getServerByName(env.MyServer, SESSION_ID); 26 | 27 | if (url.pathname === "/rpc") { 28 | const value = await stub.testMethod(); 29 | return new Response(`the value is ${value}`); 30 | } 31 | return stub.fetch(request); 32 | } 33 | } satisfies ExportedHandler; 34 | -------------------------------------------------------------------------------- /fixtures/rpc-sanity/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/rpc-sanity/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-rpc-sanity" 2 | main = 'src/index.ts' 3 | compatibility_date = "2024-07-25" 4 | 5 | 6 | [[durable_objects.bindings]] 7 | name = "MyServer" 8 | class_name = "MyServer" 9 | 10 | [[migrations]] 11 | tag = "v1" 12 | new_classes = ["MyServer"] 13 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-tiptap-yjs 2 | 3 | ## 0.0.8 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`d2335e8`](https://github.com/cloudflare/partykit/commit/d2335e80e96f15717ec0705cf768c2181081527c)]: 8 | - y-partyserver@0.0.44 9 | 10 | ## 0.0.7 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e), [`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 15 | - y-partyserver@0.0.43 16 | - partyserver@0.0.71 17 | 18 | ## 0.0.6 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 23 | - partyserver@0.0.70 24 | - y-partyserver@0.0.42 25 | 26 | ## 0.0.5 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 31 | - partyserver@0.0.69 32 | - y-partyserver@0.0.41 33 | 34 | ## 0.0.4 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 39 | - partyserver@0.0.68 40 | - y-partyserver@0.0.40 41 | 42 | ## 0.0.3 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 47 | - y-partyserver@0.0.39 48 | - partyserver@0.0.67 49 | 50 | ## 0.0.2 51 | 52 | ### Patch Changes 53 | 54 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 55 | - partyserver@0.0.66 56 | - y-partyserver@0.0.38 57 | 58 | ## 0.0.1 59 | 60 | ### Patch Changes 61 | 62 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 63 | - y-partyserver@0.0.37 64 | - partyserver@0.0.65 65 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/README.md: -------------------------------------------------------------------------------- 1 | ## tiptap-yjs 2 | 3 | A fixture showing [tiptap](https://www.tiptap.dev/) and [Yjs](https://yjs.dev/) integration with [partyserver](https://www.npmjs.com/package/partyserver)/[y-partyserver](https://www.npmjs.com/package/y-partyserver). 4 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Yjs editor 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-tiptap-yjs", 3 | "version": "0.0.8", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "@tiptap/extension-collaboration": "^2.11.5", 11 | "@tiptap/extension-collaboration-cursor": "^2.11.5", 12 | "@tiptap/react": "^2.11.5", 13 | "@tiptap/starter-kit": "^2.11.5", 14 | "partyserver": "^0.0.71", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "tailwindcss": "^4.0.17", 18 | "y-partyserver": "^0.0.44" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^19.0.12", 22 | "@types/react-dom": "^19.0.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/tiptap-yjs/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import Collaboration from "@tiptap/extension-collaboration"; 3 | import CollaborationCursor from "@tiptap/extension-collaboration-cursor"; 4 | import { EditorContent, useEditor } from "@tiptap/react"; 5 | import StarterKit from "@tiptap/starter-kit"; 6 | import useYProvider from "y-partyserver/react"; 7 | 8 | import "./styles.css"; 9 | 10 | // 5 pastel colors 11 | const colours = ["#FFC0CB", "#FFD700", "#98FB98", "#87CEFA", "#FFA07A"]; 12 | 13 | // Pick a random color from the list 14 | // This is just for demonstration purposes 15 | const MY_COLOR = colours[Math.floor(Math.random() * colours.length)]; 16 | 17 | function Tiptap() { 18 | const provider = useYProvider({ 19 | party: "document", 20 | room: "y-partyserver-text-editor-example" // replace with your own document name 21 | }); 22 | 23 | const editor = useEditor({ 24 | extensions: [ 25 | StarterKit.configure({ 26 | // The Collaboration extension comes with its own history handling 27 | history: false 28 | }), 29 | Collaboration.configure({ 30 | document: provider.doc 31 | }), 32 | // Register the collaboration cursor extension 33 | CollaborationCursor.configure({ 34 | provider: provider, 35 | user: { 36 | name: provider.id, 37 | color: MY_COLOR 38 | } 39 | }) 40 | ] 41 | }); 42 | 43 | return ( 44 |
45 |

A text editor

46 | 47 |
48 | ); 49 | } 50 | 51 | const root = createRoot(document.getElementById("root")!); 52 | root.render(); 53 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/src/client/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | /* @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; */ 6 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest } from "partyserver"; 2 | import { YServer } from "y-partyserver"; 3 | import * as Y from "yjs"; 4 | 5 | import type { CallbackOptions } from "y-partyserver"; 6 | 7 | type Env = { 8 | Document: DurableObjectNamespace; 9 | }; 10 | 11 | export class Document extends YServer { 12 | // This is optional, but it allows you to configure the callback options 13 | static callbackOptions: CallbackOptions = { 14 | debounceWait: 1000, 15 | debounceMaxWait: 10000, 16 | timeout: 10000 17 | }; 18 | async onStart() { 19 | this.ctx.storage.sql.exec( 20 | "CREATE TABLE IF NOT EXISTS documents (id TEXT PRIMARY KEY, content BLOB)" 21 | ); 22 | 23 | return super.onStart(); 24 | } 25 | async onLoad() { 26 | // load a document from a database, or some remote resource 27 | // and apply it on to the Yjs document instance at `this.document` 28 | const document = [ 29 | ...this.ctx.storage.sql.exec( 30 | "SELECT * FROM documents WHERE id = ? LIMIT 1", 31 | this.name 32 | ) 33 | ][0]; 34 | 35 | if (document) { 36 | Y.applyUpdate( 37 | this.document, 38 | new Uint8Array(document.content as ArrayBuffer) 39 | ); 40 | } 41 | return; 42 | } 43 | 44 | async onSave() { 45 | // called every few seconds after edits, and when the room empties 46 | // you can use this to write to a database or some external storage 47 | const update = Y.encodeStateAsUpdate(this.document); 48 | this.ctx.storage.sql.exec( 49 | "INSERT OR REPLACE INTO documents (id, content) VALUES (?, ?)", 50 | this.name, 51 | update 52 | ); 53 | } 54 | } 55 | 56 | export default { 57 | async fetch(request: Request, env: Env): Promise { 58 | return ( 59 | (await routePartykitRequest(request, env)) || 60 | new Response("Not Found", { status: 404 }) 61 | ); 62 | } 63 | } satisfies ExportedHandler; 64 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | import tailwindcss from "@tailwindcss/vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [cloudflare(), react(), tailwindcss()] 8 | }); 9 | -------------------------------------------------------------------------------- /fixtures/tiptap-yjs/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-tiptap-yjs" 2 | main = "src/server/index.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = { directory = "public" } 6 | 7 | 8 | [[durable_objects.bindings]] 9 | name = "Document" 10 | class_name = "Document" 11 | 12 | [[migrations]] 13 | tag = "v1" # Should be unique for each entry 14 | new_sqlite_classes = ["Document"] 15 | -------------------------------------------------------------------------------- /fixtures/tldraw/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-tldraw 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 8 | - partyserver@0.0.71 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 15 | - partyserver@0.0.70 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 22 | - partyserver@0.0.69 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 29 | - partyserver@0.0.68 30 | 31 | ## 0.0.3 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 36 | - partyserver@0.0.67 37 | 38 | ## 0.0.2 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 43 | - partyserver@0.0.66 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 50 | - partyserver@0.0.65 51 | -------------------------------------------------------------------------------- /fixtures/tldraw/README.md: -------------------------------------------------------------------------------- 1 | ## tldraw 2 | 3 | A fixture showing how to use [tldraw](https://www.tldraw.com/) with PartyServer. 4 | 5 | For a better well maintained version of a template for deploying tldraw and cloudflare workers, please visit https://github.com/tldraw/tldraw-sync-cloudflare 6 | -------------------------------------------------------------------------------- /fixtures/tldraw/client/App.tsx: -------------------------------------------------------------------------------- 1 | import { Tldraw, track, useEditor } from "tldraw"; 2 | 3 | import "tldraw/tldraw.css"; 4 | 5 | import { useSyncStore } from "./useSyncStore"; 6 | 7 | export default function SyncExample() { 8 | const store = useSyncStore({ 9 | roomId: "example", 10 | hostUrl: window.location.origin 11 | }); 12 | 13 | return ( 14 |
15 | 22 |
23 | ); 24 | } 25 | 26 | const NameEditor = track(() => { 27 | const editor = useEditor(); 28 | 29 | const { color, name } = editor.user.getUserPreferences(); 30 | 31 | return ( 32 |
33 | { 37 | editor.user.updateUserPreferences({ 38 | color: e.currentTarget.value 39 | }); 40 | }} 41 | /> 42 | { 45 | editor.user.updateUserPreferences({ 46 | name: e.currentTarget.value 47 | }); 48 | }} 49 | /> 50 |
51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /fixtures/tldraw/client/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); 2 | 3 | html, 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | font-family: "Inter", sans-serif; 8 | overscroll-behavior: none; 9 | touch-action: none; 10 | min-height: 100vh; 11 | font-size: 16px; 12 | /* mobile viewport bug fix */ 13 | min-height: -webkit-fill-available; 14 | height: 100%; 15 | } 16 | 17 | html, 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | .tldraw__editor { 23 | position: fixed; 24 | inset: 0px; 25 | overflow: hidden; 26 | } 27 | 28 | .examples { 29 | padding: 16px; 30 | } 31 | 32 | .examples__header { 33 | width: fit-content; 34 | padding-bottom: 32px; 35 | } 36 | 37 | .examples__lockup { 38 | height: 56px; 39 | width: auto; 40 | } 41 | 42 | .examples__list { 43 | display: flex; 44 | flex-direction: column; 45 | padding: 0; 46 | margin: 0; 47 | list-style: none; 48 | } 49 | 50 | .examples__list__item { 51 | padding: 8px 12px; 52 | margin: 0px -12px; 53 | } 54 | 55 | .examples__list__item a { 56 | padding: 8px 12px; 57 | margin: 0px -12px; 58 | text-decoration: none; 59 | color: inherit; 60 | } 61 | 62 | .examples__list__item a:hover { 63 | text-decoration: underline; 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/tldraw/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | 6 | import "./index.css"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /fixtures/tldraw/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/tldraw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fixtures/tldraw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-tldraw", 3 | "version": "0.0.7", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "partyserver": "^0.0.71", 11 | "partysocket": "^1.1.2", 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0", 14 | "tldraw": "^3.11.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^19.0.12", 18 | "@types/react-dom": "^19.0.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/tldraw/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/tldraw/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/tldraw/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/tldraw/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | 5 | export default defineConfig({ plugins: [cloudflare(), react()] }); 6 | -------------------------------------------------------------------------------- /fixtures/tldraw/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-tldraw" 2 | main = "src/index.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = {directory = "./public"} 6 | 7 | [[durable_objects.bindings]] 8 | name = "Tldraw" 9 | class_name = "Tldraw" 10 | 11 | [[migrations]] 12 | tag = "v1" # Should be unique for each entry 13 | new_classes = ["Tldraw"] 14 | -------------------------------------------------------------------------------- /fixtures/todo-sync/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @partyserver/fixture-todo-sync 2 | 3 | ## 0.0.7 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`20a68a8`](https://github.com/cloudflare/partykit/commit/20a68a841ef67464a41b55d500114cec6a8c6a6e)]: 8 | - partyserver@0.0.71 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`3f900b5`](https://github.com/cloudflare/partykit/commit/3f900b5f631ea3f8b8a70197890d1d551be3951d)]: 15 | - partyserver@0.0.70 16 | 17 | ## 0.0.5 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`b0bc59c`](https://github.com/cloudflare/partykit/commit/b0bc59c017484c02b4d9cb9313c92fb66b36941f), [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a)]: 22 | - partyserver@0.0.69 23 | 24 | ## 0.0.4 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`a5d2dde`](https://github.com/threepointone/partyserver/commit/a5d2dde164bd9d38e1bac87b2d32d24c06742d2f)]: 29 | - partyserver@0.0.68 30 | 31 | ## 0.0.3 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd)]: 36 | - partyserver@0.0.67 37 | 38 | ## 0.0.2 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`c41057b`](https://github.com/threepointone/partyserver/commit/c41057ba5c738496bc7e2a4968357f1f5b65707b), [`b3701a5`](https://github.com/threepointone/partyserver/commit/b3701a5f5eee278c96587d9e29e42992806733ac)]: 43 | - partyserver@0.0.66 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b)]: 50 | - partyserver@0.0.65 51 | -------------------------------------------------------------------------------- /fixtures/todo-sync/README.md: -------------------------------------------------------------------------------- 1 | ## todo-sync 2 | 3 | A fixture showing how to sync data from a Durable Object to a client with PartySync. 4 | -------------------------------------------------------------------------------- /fixtures/todo-sync/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Todo Sync 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Loading...
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /fixtures/todo-sync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-todo-sync", 3 | "private": true, 4 | "version": "0.0.7", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev" 8 | }, 9 | "dependencies": { 10 | "@types/react": "^19.0.12", 11 | "@types/react-dom": "^19.0.4", 12 | "nanoid": "^5.1.5", 13 | "partyserver": "^0.0.71", 14 | "partysocket": "^1.1.2", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "valibot": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "@cloudflare/workers-types": "^4.20250327.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/todo-sync/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/todo-sync/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/todo-sync/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/todo-sync/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/todo-sync/src/shared.ts: -------------------------------------------------------------------------------- 1 | export type TodoRecord = [ 2 | id: string, 3 | text: string, 4 | completed: 0 | 1, 5 | created_at: number, 6 | updated_at: number, 7 | deleted_at: number | null 8 | ]; 9 | 10 | export type TodoAction = 11 | | { 12 | type: "create"; 13 | payload: { 14 | id: string; 15 | text: string; 16 | completed: 0 | 1; 17 | }; 18 | } 19 | | { 20 | type: "update"; 21 | payload: { 22 | id: string; 23 | text: string; 24 | completed: 0 | 1; 25 | }; 26 | } 27 | | { 28 | type: "delete"; 29 | payload: { 30 | id: string; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /fixtures/todo-sync/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { cloudflare } from "@cloudflare/vite-plugin"; 4 | 5 | export default defineConfig({ plugins: [cloudflare(), react()] }); 6 | -------------------------------------------------------------------------------- /fixtures/todo-sync/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-globe" 2 | main = "src/server/index.ts" 3 | compatibility_date = "2024-04-19" 4 | 5 | assets = {directory = "./public"} 6 | 7 | 8 | [[durable_objects.bindings]] 9 | name = "Todos" 10 | class_name = "ToDos" 11 | 12 | [[migrations]] 13 | tag = "v1" # Should be unique for each entry 14 | new_sqlite_classes = ["ToDos"] 15 | -------------------------------------------------------------------------------- /fixtures/video-echo/README.md: -------------------------------------------------------------------------------- 1 | ## video-echo 2 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { startTransition, StrictMode } from "react"; 8 | import { hydrateRoot } from "react-dom/client"; 9 | import { RemixBrowser } from "@remix-run/react"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import { renderToReadableStream } from "react-dom/server"; 8 | import { RemixServer } from "@remix-run/react"; 9 | import { isbot } from "isbot"; 10 | 11 | import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare"; 12 | 13 | export default async function handleRequest( 14 | request: Request, 15 | responseStatusCode: number, 16 | responseHeaders: Headers, 17 | remixContext: EntryContext, 18 | // This is ignored so we can keep it in the template for visibility. Feel 19 | // free to delete this parameter in your app if you're not using it! 20 | loadContext: AppLoadContext 21 | ) { 22 | const body = await renderToReadableStream( 23 | , 24 | { 25 | signal: request.signal, 26 | onError(error: unknown) { 27 | // Log streaming rendering errors from inside the shell 28 | console.error(error); 29 | responseStatusCode = 500; 30 | } 31 | } 32 | ); 33 | 34 | if (isbot(request.headers.get("user-agent") || "")) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set("Content-Type", "text/html"); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/hooks/useIsServer.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from "react"; 2 | 3 | export function useIsServer() { 4 | return useSyncExternalStore( 5 | () => () => {}, 6 | () => false, 7 | () => true 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { cssBundleHref } from "@remix-run/css-bundle"; 2 | import { 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration 9 | } from "@remix-run/react"; 10 | 11 | import type { LinksFunction } from "@remix-run/cloudflare"; 12 | 13 | export const links: LinksFunction = () => [ 14 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []) 15 | ]; 16 | 17 | export function Layout({ children }: { children: React.ReactNode }) { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default function App() { 37 | return ; 38 | } 39 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { Demo } from "~/components/Demo.client"; 2 | import { useIsServer } from "~/hooks/useIsServer"; 3 | 4 | export default function Component() { 5 | const isServer = useIsServer(); 6 | if (isServer) return null; 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/routes/partytracks.$.tsx: -------------------------------------------------------------------------------- 1 | import { routePartyTracksRequest } from "partytracks/server"; 2 | 3 | import type { 4 | ActionFunctionArgs, 5 | LoaderFunctionArgs 6 | } from "@remix-run/server-runtime"; 7 | 8 | export const loader = ({ context, request }: LoaderFunctionArgs) => { 9 | return routePartyTracksRequest({ 10 | appId: context.env.CALLS_APP_ID, 11 | token: context.env.CALLS_APP_TOKEN, 12 | request 13 | }); 14 | }; 15 | 16 | export const action = ({ context, request }: ActionFunctionArgs) => { 17 | return routePartyTracksRequest({ 18 | appId: context.env.CALLS_APP_ID, 19 | token: context.env.CALLS_APP_TOKEN, 20 | request 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /fixtures/video-echo/app/server.ts: -------------------------------------------------------------------------------- 1 | import { createRequestHandler, logDevReady } from "@remix-run/cloudflare"; 2 | import * as build from "@remix-run/dev/server-build"; 3 | import { WorkerEntrypoint } from "cloudflare:workers"; 4 | import { routePartykitRequest, Server } from "partyserver"; 5 | 6 | // @ts-expect-error we haven't loaded node's types for this yet 7 | if (process.env.NODE_ENV === "development") { 8 | logDevReady(build); 9 | } 10 | 11 | type Env = { 12 | MyPartyServer: DurableObjectNamespace>; 13 | CALLS_APP_ID: string; 14 | CALLS_APP_TOKEN: string; 15 | }; 16 | 17 | declare module "@remix-run/cloudflare" { 18 | interface AppLoadContext { 19 | env: Env; 20 | MyPartyServer: DurableObjectNamespace>; 21 | } 22 | } 23 | 24 | const handleRemixRequest = createRequestHandler(build); 25 | 26 | // TODO: test the expiration stuff 27 | export class MyPartyServer extends Server { 28 | async fetch(request: Request) { 29 | return new Response("Hello from the party server"); 30 | } 31 | } 32 | 33 | export default class Worker extends WorkerEntrypoint { 34 | async fetch(request: Request) { 35 | // we need to do this dance just to get the session id 36 | // from the request to route it to the correct Party 37 | 38 | return ( 39 | (await routePartykitRequest(request, this.env)) || 40 | handleRemixRequest(request, { 41 | env: this.env, 42 | MyPartyServer: this.env.MyPartyServer 43 | }) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /fixtures/video-echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/fixture-video-echo", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "remix build", 7 | "deploy": "wrangler deploy", 8 | "start": "remix dev -c \"wrangler dev\"" 9 | }, 10 | "dependencies": { 11 | "@remix-run/cloudflare": "^2.16.2", 12 | "@remix-run/css-bundle": "^2.16.2", 13 | "@remix-run/react": "^2.16.2", 14 | "@remix-run/server-runtime": "^2.16.2", 15 | "isbot": "^5.1.25", 16 | "partysocket": "^1.1.4", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0" 19 | }, 20 | "devDependencies": { 21 | "@cloudflare/workers-types": "^4.20250327.0", 22 | "@remix-run/dev": "^2.16.2", 23 | "@types/react": "^19.0.12", 24 | "@types/react-dom": "^19.0.4" 25 | }, 26 | "engines": { 27 | "node": ">=20.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fixtures/video-echo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudflare/partykit/13c138bdfaa74b7ccb0bdbe58e68cefce0ff5a5a/fixtures/video-echo/public/favicon.ico -------------------------------------------------------------------------------- /fixtures/video-echo/remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | 3 | export default { 4 | ignoredRouteFiles: ["**/.*"], 5 | server: "./app/server.ts", 6 | serverConditions: ["workerd", "worker", "browser"], 7 | serverMainFields: ["workerd", "browser", "module", "main"], 8 | serverMinify: false, 9 | serverModuleFormat: "esm", 10 | serverPlatform: "neutral", 11 | future: { 12 | v3_fetcherPersist: true, 13 | v3_relativeSplatPath: true, 14 | v3_throwAbortReason: true 15 | }, 16 | assetsBuildDirectory: "public/dist", 17 | serverBuildPath: "dist/index.js", 18 | publicPath: "/dist/", 19 | dev: { 20 | port: 8002 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /fixtures/video-echo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "types": ["@remix-run/cloudflare", "@cloudflare/workers-types"], 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./app/*"] 9 | } 10 | }, 11 | "exclude": ["src/tests/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/video-echo/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partyserver-fixture-video-echo" 2 | main = "./dist/index.js" 3 | compatibility_date = "2024-05-30" 4 | 5 | assets = {directory = "./public"} 6 | 7 | [define] 8 | "process.env.REMIX_DEV_ORIGIN"= "'http://127.0.0.1:8002'" 9 | "process.env.REMIX_DEV_SERVER_WS_PORT"= "8002" 10 | 11 | [[durable_objects.bindings]] 12 | name = "MyPartyServer" 13 | class_name = "MyPartyServer" 14 | 15 | [[migrations]] 16 | tag = "v1" # Should be unique for each entry 17 | new_classes = ["MyPartyServer"] 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@partyserver/partyserver-root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Everything's better with friends", 6 | "scripts": { 7 | "build": "npm run build -w partyserver -w partysocket -w y-partyserver -w partysub -w partyfn -w partysync -w partywhen -w partytracks -w hono-party", 8 | "check": "concurrently \"npm run check:repo\" \"npm run check:format\" \"npm run check:lint\" \"npm run check:type\" \"npm run check:test\" --kill-others-on-fail", 9 | "check:format": "prettier . --check --ignore-unknown", 10 | "check:lint": "biome check", 11 | "check:repo": "manypkg check", 12 | "check:test": "npm run check:test -w partyserver -w partysocket -w partysub -w partywhen -w partytracks", 13 | "check:type": "tsx scripts/typecheck.ts", 14 | "all": "tsx i && npm run build && npm run check" 15 | }, 16 | "author": "Sunil Pai ", 17 | "license": "ISC", 18 | "workspaces": [ 19 | "packages/*", 20 | "examples/*", 21 | "fixtures/*" 22 | ], 23 | "devDependencies": { 24 | "@arethetypeswrong/cli": "^0.17.4", 25 | "@biomejs/biome": "^1.9.4", 26 | "@changesets/changelog-github": "^0.5.1", 27 | "@changesets/cli": "^2.28.1", 28 | "@cloudflare/vite-plugin": "^0.1.18", 29 | "@cloudflare/vitest-pool-workers": "^0.8.7", 30 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 31 | "@manypkg/cli": "^0.23.0", 32 | "@types/node": "22.13.14", 33 | "@vitejs/plugin-react": "^4.3.4", 34 | "@vitest/runner": "3.0.9", 35 | "@vitest/snapshot": "3.0.9", 36 | "concurrently": "^9.1.2", 37 | "cross-env": "^7.0.3", 38 | "jsdom": "^26.0.0", 39 | "prettier": "^3.5.3", 40 | "shx": "^0.4.0", 41 | "tsup": "^8.4.0", 42 | "tsx": "^4.19.3", 43 | "typescript": "^5.8.2", 44 | "vite": "^6.2.3", 45 | "vitest": "3.0.9", 46 | "wrangler": "^4.5.1" 47 | }, 48 | "overrides": { 49 | "esbuild": "0.25.0", 50 | "@types/node": "22.13.14", 51 | "prosemirror-model": "1.22.2" 52 | }, 53 | "trustedDependencies": [ 54 | "@biomejs/biome", 55 | "core-js", 56 | "esbuild", 57 | "workerd" 58 | ], 59 | "packageManager": "npm@11.2.0", 60 | "type": "module" 61 | } 62 | -------------------------------------------------------------------------------- /packages/hono-party/README.md: -------------------------------------------------------------------------------- 1 | # hono-party 2 | 3 | 🔥 [Hono](https://hono.dev) ⨉ 🎈 [PartyServer](https://github.com/cloudflare/partykit) 4 | 5 | Websockets from the future, now in Hono. Add collaborative editing, multiplayer games, local-first apps, ai agents, (or whatever!) into your Hono app today. 6 | 7 | ```bash 8 | npm install hono-party hono partyserver 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```tsx 14 | import { Hono } from "hono"; 15 | import { partyserverMiddleware } from "hono-party"; 16 | import { Server } from "partyserver"; 17 | 18 | // Multiple party servers 19 | export class Chat extends Server {} 20 | export class Game extends Server {} 21 | export class Document extends Server {} 22 | 23 | // Basic setup 24 | const app = new Hono(); 25 | app.use("*", partyserverMiddleware()); 26 | 27 | // or with authentication 28 | app.use( 29 | "*", 30 | partyserverMiddleware({ 31 | options: { 32 | onBeforeConnect: async (req) => { 33 | const token = req.headers.get("authorization"); 34 | // validate token 35 | if (!token) return new Response("Unauthorized", { status: 401 }); 36 | } 37 | } 38 | }) 39 | ); 40 | 41 | // With error handling 42 | app.use( 43 | "*", 44 | partyserverMiddleware({ onError: (error) => console.error(error) }) 45 | ); 46 | 47 | // With custom routing 48 | app.use( 49 | "*", 50 | partyserverMiddleware({ 51 | options: { 52 | prefix: "/party" // Handles /party/* routes only 53 | } 54 | }) 55 | ); 56 | 57 | export default app; 58 | ``` 59 | 60 | ## React Usage 61 | 62 | ```tsx 63 | import { usePartySocket } from "partysocket/react"; 64 | 65 | // Basic connection 66 | const socket = usePartySocket({ party: "chat", room: "general" }); 67 | 68 | // game connection 69 | const socket = usePartySocket({ party: "game", room: "uuid" }); 70 | 71 | // document connection 72 | const socket = usePartySocket({ party: "document", room: "id" }); 73 | 74 | // With auth 75 | const socket = usePartySocket({ 76 | party: "chat", 77 | room: "general", 78 | headers: { authorization: `Bearer ${token}` } 79 | }); 80 | ``` 81 | 82 | ## Configuration 83 | 84 | ```jsonc 85 | // wrangler.json 86 | { 87 | "durable_objects": { 88 | "bindings": [ 89 | { "name": "Chat", "class_name": "Chat" }, 90 | { "name": "Game", "class_name": "Game" }, 91 | { "name": "Document", "class_name": "Document" } 92 | ] 93 | }, 94 | "migrations": [{ "tag": "v1", "new_classes": ["Chat", "Game", "Document"] }] 95 | } 96 | ``` 97 | 98 | ## Thanks 99 | 100 | Thanks to [Thomas Osmonson](https://x.com/aulneau_) for building this! 101 | -------------------------------------------------------------------------------- /packages/hono-party/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-party", 3 | "version": "0.0.12", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/hono-party", 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "require": "./dist/index.js", 14 | "import": "./dist/index.js" 15 | } 16 | }, 17 | "scripts": { 18 | "build": "tsx scripts/build.ts" 19 | }, 20 | "files": [ 21 | "dist", 22 | "README.md" 23 | ], 24 | "keywords": [ 25 | "hono", 26 | "partykit", 27 | "partyserver" 28 | ], 29 | "author": "Thomas Osmonson ", 30 | "license": "ISC", 31 | "description": "Use PartyServer with Hono", 32 | "peerDependencies": { 33 | "@cloudflare/workers-types": "^4.20240729.0", 34 | "hono": "^4.6.17", 35 | "partyserver": "^0.0.71" 36 | }, 37 | "devDependencies": { 38 | "@cloudflare/workers-types": "^4.20250327.0", 39 | "hono": "^4.7.5", 40 | "partyserver": "^0.0.71" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/hono-party/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: ["src/index.ts"], 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ["cloudflare:workers", "partyserver", "react", "nanoid", "hono"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | // then run prettier on the generated .d.ts files 15 | execSync("prettier --write ./dist/**/*.d.ts"); 16 | 17 | process.exit(0); 18 | -------------------------------------------------------------------------------- /packages/hono-party/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/hono-party/src/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from "hono/adapter"; 2 | import { createMiddleware } from "hono/factory"; 3 | import { routePartykitRequest } from "partyserver"; 4 | 5 | import type { Context, Env } from "hono"; 6 | import type { PartyServerOptions } from "partyserver"; 7 | 8 | /** 9 | * Configuration options for the PartyServer middleware 10 | */ 11 | type PartyServerMiddlewareContext = { 12 | /** PartyServer-specific configuration options */ 13 | options?: PartyServerOptions; 14 | /** Optional error handler for caught errors */ 15 | onError?: (error: Error) => void; 16 | }; 17 | 18 | /** 19 | * Creates a middleware for handling PartyServer WebSocket and HTTP requests 20 | * Processes both WebSocket upgrades and standard HTTP requests, delegating them to PartyServer 21 | */ 22 | export function partyserverMiddleware( 23 | ctx?: PartyServerMiddlewareContext 24 | ) { 25 | return createMiddleware(async (c, next) => { 26 | try { 27 | const handler = isWebSocketUpgrade(c) 28 | ? handleWebSocketUpgrade 29 | : handleHttpRequest; 30 | const response = await handler(c, ctx?.options); 31 | 32 | return response === null ? await next() : response; 33 | } catch (error) { 34 | if (ctx?.onError) { 35 | ctx.onError(error as Error); 36 | return next(); 37 | } 38 | throw error; 39 | } 40 | }); 41 | } 42 | 43 | /** 44 | * Checks if the incoming request is a WebSocket upgrade request 45 | * Looks for the 'upgrade' header with a value of 'websocket' (case-insensitive) 46 | */ 47 | function isWebSocketUpgrade(c: Context): boolean { 48 | return c.req.header("upgrade")?.toLowerCase() === "websocket"; 49 | } 50 | 51 | /** 52 | * Creates a new Request object from the Hono context 53 | * Preserves the original request's URL, method, headers, and body 54 | */ 55 | function createRequestFromContext(c: Context) { 56 | return new Request(c.req.url, { 57 | method: c.req.method, 58 | headers: c.req.header(), 59 | body: c.req.raw.body 60 | }); 61 | } 62 | 63 | /** 64 | * Handles WebSocket upgrade requests 65 | * Returns a WebSocket upgrade response if successful, null otherwise 66 | */ 67 | async function handleWebSocketUpgrade( 68 | c: Context, 69 | options?: PartyServerOptions 70 | ) { 71 | const req = createRequestFromContext(c); 72 | const response = await routePartykitRequest(req, env(c), options); 73 | 74 | if (!response?.webSocket) { 75 | return null; 76 | } 77 | 78 | return new Response(null, { 79 | status: 101, 80 | webSocket: response.webSocket 81 | }); 82 | } 83 | 84 | /** 85 | * Handles standard HTTP requests 86 | * Forwards the request to PartyServer and returns the response 87 | */ 88 | async function handleHttpRequest( 89 | c: Context, 90 | options?: PartyServerOptions 91 | ) { 92 | const req = createRequestFromContext(c); 93 | return routePartykitRequest(req, env(c), options); 94 | } 95 | -------------------------------------------------------------------------------- /packages/hono-party/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | }, 6 | "exclude": ["src/tests/**/*.ts", "scripts/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/partyagent/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partyagent 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | -------------------------------------------------------------------------------- /packages/partyagent/README.md: -------------------------------------------------------------------------------- 1 | # partyagent 2 | 3 | Autonomous agents powered by Durable Objects. Extends PartySync for state synchronization. 4 | 5 | ## Features 6 | 7 | - **Natural Language Processing** - Agents understand and respond to natural language 8 | - **Customizable Personalities** - Define agent behavior and characteristics 9 | - **Tool Usage** - Access to functions with input/output schemas 10 | - **Task Hand-off** - Agents can delegate tasks to other agents 11 | - **Observability** - Monitor agent actions 12 | 13 | ## Related Resources 14 | 15 | - [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents) 16 | - [Agents Architecture](https://huyenchip.com/2025/01/07/agents.html) 17 | 18 | ## License 19 | 20 | ISC 21 | -------------------------------------------------------------------------------- /packages/partyagent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyagent", 3 | "version": "0.0.2", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "durable-objects", 15 | "ai-agents" 16 | ], 17 | "author": "Sunil Pai ", 18 | "license": "ISC", 19 | "description": "AI agents on Durable Objects" 20 | } 21 | -------------------------------------------------------------------------------- /packages/partybase/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partybase 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | 15 | ## 0.0.1 16 | 17 | ### Patch Changes 18 | 19 | - [`6030aee`](https://github.com/threepointone/partyserver/commit/6030aee16f8d96839a215cf5ce06eb123997d373) Thanks [@threepointone](https://github.com/threepointone)! - partybase stub 20 | -------------------------------------------------------------------------------- /packages/partybase/README.md: -------------------------------------------------------------------------------- 1 | # partybase 2 | 3 | Database solution for PartyServer built on Durable Objects. 4 | 5 | ## Features 6 | 7 | - **Migration System** - Built-in database migration capabilities 8 | - **Admin Interface** - Web interface for database management 9 | - **Starbase Integration** - Potential integration with [Starbase](https://starbasedb.com/) 10 | 11 | ## License 12 | 13 | ISC 14 | -------------------------------------------------------------------------------- /packages/partybase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partybase", 3 | "version": "0.0.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "durable-objects", 15 | "database", 16 | "sqlite" 17 | ], 18 | "author": "Sunil Pai ", 19 | "license": "ISC", 20 | "description": "Something with databases?" 21 | } 22 | -------------------------------------------------------------------------------- /packages/partyflow/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partyflow 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | -------------------------------------------------------------------------------- /packages/partyflow/README.md: -------------------------------------------------------------------------------- 1 | # partyflow 2 | 3 | Lightweight workflow engine for PartyServer built on Durable Objects. 4 | 5 | ## License 6 | 7 | ISC 8 | -------------------------------------------------------------------------------- /packages/partyflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyflow", 3 | "version": "0.0.2", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "durable-objects", 15 | "ai-agents", 16 | "workflows" 17 | ], 18 | "author": "Sunil Pai ", 19 | "license": "ISC", 20 | "description": "Lightweight workflows on Durable Objects" 21 | } 22 | -------------------------------------------------------------------------------- /packages/partyfn/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partyfn 2 | 3 | ## 0.0.9 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.8 10 | 11 | ### Patch Changes 12 | 13 | - [#205](https://github.com/threepointone/partyserver/pull/205) [`b1baf6c`](https://github.com/threepointone/partyserver/commit/b1baf6cdda4c7684a4663a1281070ab1762670fd) Thanks [@threepointone](https://github.com/threepointone)! - update deps 14 | 15 | ## 0.0.7 16 | 17 | ### Patch Changes 18 | 19 | - [#203](https://github.com/threepointone/partyserver/pull/203) [`c7646a1`](https://github.com/threepointone/partyserver/commit/c7646a11d0fc83035f83f786d9125271a5b51686) Thanks [@threepointone](https://github.com/threepointone)! - fix partysocket exports 20 | 21 | - Updated dependencies [[`c7646a1`](https://github.com/threepointone/partyserver/commit/c7646a11d0fc83035f83f786d9125271a5b51686)]: 22 | - partysocket@1.1.3 23 | 24 | ## 0.0.6 25 | 26 | ### Patch Changes 27 | 28 | - [#201](https://github.com/threepointone/partyserver/pull/201) [`e3a7c23`](https://github.com/threepointone/partyserver/commit/e3a7c231ef33e471b0d476e46f09787ece3b05a0) Thanks [@threepointone](https://github.com/threepointone)! - move partysocket into the partyserver repo 29 | 30 | - Updated dependencies [[`e3a7c23`](https://github.com/threepointone/partyserver/commit/e3a7c231ef33e471b0d476e46f09787ece3b05a0)]: 31 | - partysocket@1.1.1 32 | 33 | ## 0.0.5 34 | 35 | ### Patch Changes 36 | 37 | - [#181](https://github.com/threepointone/partyserver/pull/181) [`3e56cce`](https://github.com/threepointone/partyserver/commit/3e56cceca2c253d7b4368299e018b73af6deb42b) Thanks [@threepointone](https://github.com/threepointone)! - update dependencies 38 | 39 | ## 0.0.4 40 | 41 | ### Patch Changes 42 | 43 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 44 | 45 | ## 0.0.3 46 | 47 | ### Patch Changes 48 | 49 | - [`5e6cf60`](https://github.com/threepointone/partyserver/commit/5e6cf60e5321dfdda1fba8acbb7b3c4047736a12) Thanks [@threepointone](https://github.com/threepointone)! - pull out some stuff for partyfn, start work on the rpc agent, etc 50 | 51 | ## 0.0.2 52 | 53 | ### Patch Changes 54 | 55 | - [`84436f6`](https://github.com/threepointone/partyserver/commit/84436f66104e9c8e67520bfa15418aa329d98aeb) Thanks [@threepointone](https://github.com/threepointone)! - stubs 56 | -------------------------------------------------------------------------------- /packages/partyfn/README.md: -------------------------------------------------------------------------------- 1 | # partyfn 2 | 3 | RPC (Remote Procedure Call) system for PartyServer. Enables typesafe function calls between client and server, with support for bidirectional RPC. 4 | 5 | ## Features 6 | 7 | - **Typesafe RPC** - Function calls from client to server 8 | - **Bidirectional** - Supports server-to-client RPC calls 9 | 10 | ## License 11 | 12 | ISC 13 | -------------------------------------------------------------------------------- /packages/partyfn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyfn", 3 | "version": "0.0.9", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "require": "./dist/index.js", 13 | "import": "./dist/index.js" 14 | } 15 | }, 16 | "files": [ 17 | "dist", 18 | "README.md" 19 | ], 20 | "dependencies": { 21 | "nanoid": "^5.1.5", 22 | "partysocket": "^1.1.3" 23 | }, 24 | "scripts": { 25 | "build": "tsx scripts/build.ts" 26 | }, 27 | "keywords": [ 28 | "durable-objects", 29 | "rpc" 30 | ], 31 | "author": "Sunil Pai ", 32 | "license": "ISC", 33 | "description": "RPC on Durable Objects" 34 | } 35 | -------------------------------------------------------------------------------- /packages/partyfn/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: ["src/index.ts"], 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ["cloudflare:workers", "partyserver", "react", "nanoid"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | // then run prettier on the generated .d.ts files 15 | execSync("prettier --write ./dist/**/*.d.ts"); 16 | 17 | process.exit(0); 18 | -------------------------------------------------------------------------------- /packages/partyfn/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partyfn/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partyhard/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partyhard 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | 15 | ## 0.0.2 16 | 17 | ### Patch Changes 18 | 19 | - [`84436f6`](https://github.com/threepointone/partyserver/commit/84436f66104e9c8e67520bfa15418aa329d98aeb) Thanks [@threepointone](https://github.com/threepointone)! - stubs 20 | -------------------------------------------------------------------------------- /packages/partyhard/README.md: -------------------------------------------------------------------------------- 1 | # partyhard 2 | 3 | A component of the PartyServer ecosystem. 4 | 5 | ## License 6 | 7 | ISC 8 | -------------------------------------------------------------------------------- /packages/partyhard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyhard", 3 | "version": "0.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "durable-objects" 15 | ], 16 | "author": "Sunil Pai ", 17 | "license": "ISC", 18 | "description": "" 19 | } 20 | -------------------------------------------------------------------------------- /packages/partyserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partyserver", 3 | "version": "0.0.71", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/partyserver", 9 | "main": "dist/index.js", 10 | "module": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "type": "module", 13 | "scripts": { 14 | "check:test": "vitest -r src/tests --watch false", 15 | "test": "vitest -r src/tests", 16 | "build": "tsx scripts/build.ts" 17 | }, 18 | "files": [ 19 | "dist", 20 | "README.md" 21 | ], 22 | "keywords": [ 23 | "stateful", 24 | "serverless", 25 | "durable objects", 26 | "partykit", 27 | "cloudflare", 28 | "workerd", 29 | "real-time", 30 | "websockets", 31 | "collaboration", 32 | "pubsub", 33 | "multiplayer", 34 | "ai agents", 35 | "game server" 36 | ], 37 | "author": "Sunil Pai ", 38 | "license": "ISC", 39 | "description": "", 40 | "dependencies": { 41 | "nanoid": "^5.1.5" 42 | }, 43 | "peerDependencies": { 44 | "@cloudflare/workers-types": "^4.20240729.0" 45 | }, 46 | "devDependencies": { 47 | "@cloudflare/workers-types": "^4.20250327.0", 48 | "ws": "^8.18.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/partyserver/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: ["src/index.ts"], 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ["cloudflare:workers"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | // then run prettier on the generated .d.ts files 15 | execSync("prettier --write ./dist/*.d.ts"); 16 | 17 | process.exit(0); 18 | -------------------------------------------------------------------------------- /packages/partyserver/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partyserver/src/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@cloudflare/workers-types/experimental", 6 | "@cloudflare/vitest-pool-workers" 7 | ] 8 | }, 9 | "include": ["./**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/partyserver/src/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | isolatedStorage: false, 8 | wrangler: { 9 | configPath: "./wrangler.toml" 10 | } 11 | } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /packages/partyserver/src/tests/worker.ts: -------------------------------------------------------------------------------- 1 | import { routePartykitRequest, Server } from "../index"; 2 | 3 | import type { Connection, ConnectionContext } from "../index"; 4 | 5 | function assert(condition: unknown, message: string): asserts condition { 6 | if (!condition) { 7 | throw new Error(message); 8 | } 9 | } 10 | 11 | export type Env = { 12 | Stateful: DurableObjectNamespace; 13 | OnStartServer: DurableObjectNamespace; 14 | }; 15 | 16 | export class Stateful extends Server { 17 | static options = { 18 | hibernate: true 19 | }; 20 | 21 | onConnect( 22 | connection: Connection, 23 | _ctx: ConnectionContext 24 | ): void | Promise { 25 | connection.send( 26 | JSON.stringify({ 27 | name: this.name 28 | }) 29 | ); 30 | } 31 | 32 | onRequest( 33 | _request: Request> 34 | ): Response | Promise { 35 | return Response.json({ 36 | name: this.name 37 | }); 38 | } 39 | } 40 | 41 | export class OnStartServer extends Server { 42 | counter = 0; 43 | async onStart() { 44 | // this stray assert is simply to make sure .name is available 45 | // inside onStart, it should throw if not 46 | assert(this.name, "name is not available inside onStart"); 47 | await new Promise((resolve) => { 48 | setTimeout(() => { 49 | this.counter++; 50 | resolve(); 51 | }, 300); 52 | }); 53 | } 54 | onConnect(connection: Connection) { 55 | connection.send(this.counter.toString()); 56 | } 57 | onRequest( 58 | _request: Request> 59 | ): Response | Promise { 60 | return new Response(this.counter.toString()); 61 | } 62 | } 63 | 64 | export default { 65 | async fetch(request: Request, env: Env, _ctx: ExecutionContext) { 66 | return ( 67 | (await routePartykitRequest(request, env, { 68 | onBeforeConnect: async (_request, { party, name }) => { 69 | if (party === "on-start-server") { 70 | if (name === "is-error") { 71 | return new Response("Error", { status: 503 }); 72 | } else if (name === "is-redirect") { 73 | return new Response("Redirect", { 74 | status: 302, 75 | headers: { Location: "https://example2.com" } 76 | }); 77 | } 78 | } 79 | }, 80 | onBeforeRequest: async (_request, { party, name }) => { 81 | if (party === "on-start-server") { 82 | if (name === "is-error") { 83 | return new Response("Error", { status: 504 }); 84 | } else if (name === "is-redirect") { 85 | return new Response("Redirect", { 86 | status: 302, 87 | headers: { Location: "https://example3.com" } 88 | }); 89 | } 90 | } 91 | } 92 | })) || new Response("Not Found", { status: 404 }) 93 | ); 94 | } 95 | } satisfies ExportedHandler; 96 | -------------------------------------------------------------------------------- /packages/partyserver/src/tests/wrangler.toml: -------------------------------------------------------------------------------- 1 | main = "/worker.ts" 2 | compatibility_date = "2024-05-12" 3 | compatibility_flags = ["nodejs_compat"] 4 | 5 | [[durable_objects.bindings]] 6 | name = "Stateful" 7 | class_name = "Stateful" 8 | 9 | [[durable_objects.bindings]] 10 | name = "OnStartServer" 11 | class_name = "OnStartServer" 12 | 13 | [[migrations]] 14 | tag = "v1" # Should be unique for each entry 15 | new_classes = ["Stateful", "OnStartServer"] 16 | -------------------------------------------------------------------------------- /packages/partyserver/src/types.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/58993872 2 | type ImmutablePrimitive = undefined | null | boolean | string | number; 3 | type Immutable = T extends ImmutablePrimitive 4 | ? T 5 | : T extends Array 6 | ? ImmutableArray 7 | : T extends Map 8 | ? ImmutableMap 9 | : T extends Set 10 | ? ImmutableSet 11 | : ImmutableObject; 12 | type ImmutableArray = ReadonlyArray>; 13 | type ImmutableMap = ReadonlyMap, Immutable>; 14 | type ImmutableSet = ReadonlySet>; 15 | type ImmutableObject = { readonly [K in keyof T]: Immutable }; 16 | 17 | export type ConnectionState = ImmutableObject | null; 18 | export type ConnectionSetStateFn = (prevState: ConnectionState) => T; 19 | 20 | export type ConnectionContext = { 21 | request: Request; 22 | }; 23 | 24 | /** A WebSocket connected to the Server */ 25 | export type Connection = WebSocket & { 26 | /** Connection identifier */ 27 | id: string; 28 | 29 | /** 30 | * Arbitrary state associated with this connection. 31 | * Read-only, use Connection.setState to update the state. 32 | */ 33 | state: ConnectionState; 34 | 35 | setState( 36 | state: TState | ConnectionSetStateFn | null 37 | ): ConnectionState; 38 | 39 | /** @deprecated use Connection.setState instead */ 40 | serializeAttachment(attachment: T): void; 41 | 42 | /** @deprecated use Connection.state instead */ 43 | deserializeAttachment(): T | null; 44 | 45 | /** 46 | * Server's name 47 | */ 48 | server: string; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/partyserver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | }, 6 | "exclude": ["src/tests/**/*.ts", "scripts/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/partysession/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partysession 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | 15 | ## 0.0.2 16 | 17 | ### Patch Changes 18 | 19 | - [`1da2079`](https://github.com/threepointone/partyserver/commit/1da2079d49468b0fb5d5725e748076b6c66e7ca8) Thanks [@threepointone](https://github.com/threepointone)! - stubs 20 | -------------------------------------------------------------------------------- /packages/partysession/README.md: -------------------------------------------------------------------------------- 1 | # partysession 2 | 3 | One Durable Object per user model for PartyServer. Designed for managing user-specific state like shopping carts. 4 | 5 | ## Features 6 | 7 | - **Per-User State** - One Durable Object instance per user 8 | - **Session Management** - Handles user session data 9 | 10 | ## License 11 | 12 | ISC 13 | -------------------------------------------------------------------------------- /packages/partysession/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partysession", 3 | "version": "0.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "sessions", 15 | "durable-objects" 16 | ], 17 | "author": "Sunil Pai ", 18 | "license": "ISC", 19 | "description": "Sessions, with Durable Objects" 20 | } 21 | -------------------------------------------------------------------------------- /packages/partysmart/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # partysmart 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - [`7ec1568`](https://github.com/cloudflare/partykit/commit/7ec15680fd1dcb257263d52d2c9cd5088e2f7c0a) Thanks [@threepointone](https://github.com/threepointone)! - replace url in package.json to point to cloudflare/partykit 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - [#161](https://github.com/threepointone/partyserver/pull/161) [`c73b724`](https://github.com/threepointone/partyserver/commit/c73b724685581fe381bcb34d5944e9d4bfa1b17a) Thanks [@joelhooks](https://github.com/joelhooks)! - feat(docs): spruce up readmes 14 | 15 | ## 0.0.2 16 | 17 | ### Patch Changes 18 | 19 | - [`1da2079`](https://github.com/threepointone/partyserver/commit/1da2079d49468b0fb5d5725e748076b6c66e7ca8) Thanks [@threepointone](https://github.com/threepointone)! - stubs 20 | -------------------------------------------------------------------------------- /packages/partysmart/README.md: -------------------------------------------------------------------------------- 1 | # partysmart 2 | 3 | Alternative name for PartyAgent. See [partyagent](../partyagent/README.md) for details. 4 | 5 | ## License 6 | 7 | ISC 8 | -------------------------------------------------------------------------------- /packages/partysmart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partysmart", 3 | "version": "0.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "keywords": [ 14 | "ai agents", 15 | "smart", 16 | "durable-objects" 17 | ], 18 | "author": "Sunil Pai ", 19 | "license": "ISC", 20 | "description": "Something for AI agents to do" 21 | } 22 | -------------------------------------------------------------------------------- /packages/partysocket/.gitignore: -------------------------------------------------------------------------------- 1 | /*.d.ts 2 | /*.d.mts 3 | /*.d.ts.map 4 | 5 | /event-target-polyfill.* 6 | -------------------------------------------------------------------------------- /packages/partysocket/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (c) 2010-2012, Joe Walnes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /packages/partysocket/TODO.md: -------------------------------------------------------------------------------- 1 | - https://github.com/pladaria/reconnecting-websocket/pull/166 Fix: handle error if getNextUrl throws (TODO: add test for this one ) 2 | - https://github.com/pladaria/reconnecting-websocket/pull/132 feat: make protocols updatable 3 | - https://github.com/pladaria/reconnecting-websocket/pull/141 [Fix] Socket doesn't connect again after closing while connecting 4 | 5 | (TODO: more) 6 | 7 | - https://github.com/pladaria/reconnecting-websocket/pull/163 Support for Dynamic Protocols 8 | - https://github.com/pladaria/reconnecting-websocket/pull/47 reconnecting and reconnectscheduled custom events 9 | -------------------------------------------------------------------------------- /packages/partysocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partysocket", 3 | "version": "1.1.4", 4 | "description": "A better WebSocket that Just Works™", 5 | "homepage": "https://docs.partykit.io/reference/partysocket-api", 6 | "bugs": "https://github.com/threepointone/partyserver/issues", 7 | "main": "./dist/index.js", 8 | "module": "./dist/index.mjs", 9 | "exports": { 10 | ".": { 11 | "types": "./index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.js" 14 | }, 15 | "./ws": { 16 | "types": "./ws.d.ts", 17 | "import": "./dist/ws.mjs", 18 | "require": "./dist/ws.js" 19 | }, 20 | "./react": { 21 | "types": "./react.d.ts", 22 | "import": "./dist/react.mjs", 23 | "require": "./dist/react.js" 24 | }, 25 | "./use-ws": { 26 | "types": "./use-ws.d.ts", 27 | "import": "./dist/use-ws.mjs", 28 | "require": "./dist/use-ws.js" 29 | }, 30 | "./event-target-polyfill": { 31 | "types": "./event-target-polyfill.d.ts", 32 | "import": "./dist/event-target-polyfill.mjs", 33 | "require": "./dist/event-target-polyfill.js" 34 | } 35 | }, 36 | "scripts": { 37 | "clean": "shx rm -rf dist *.d.ts *.d.mts event-target-polyfill.*", 38 | "post-build": "shx mv dist/*.d.ts dist/*.d.mts* . && shx mv dist/event-target-polyfill.* .", 39 | "build": "npm run clean && npx tsx scripts/build.mts && npm run post-build", 40 | "check:test": "vitest --no-watch" 41 | }, 42 | "files": [ 43 | "dist", 44 | "*.d.ts", 45 | "event-target-polyfill.*" 46 | ], 47 | "keywords": [ 48 | "websocket", 49 | "client", 50 | "reconnecting", 51 | "reconnection", 52 | "reconnect", 53 | "forever", 54 | "persistent", 55 | "forever", 56 | "automatic" 57 | ], 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/threepointone/partyserver.git", 61 | "directory": "packages/partysocket" 62 | }, 63 | "author": "", 64 | "license": "ISC", 65 | "dependencies": { 66 | "event-target-polyfill": "^0.0.4" 67 | }, 68 | "devDependencies": { 69 | "@types/ws": "^8.18.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/partysocket/scripts/build.mts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: [ 6 | "src/index.ts", 7 | "src/react.ts", 8 | "src/ws.ts", 9 | "src/use-ws.ts", 10 | "src/event-target-polyfill.ts" 11 | ], 12 | splitting: true, 13 | sourcemap: true, 14 | clean: true, 15 | external: ["cloudflare:workers", "partyserver", "react"], 16 | format: ["esm", "cjs"], 17 | dts: true 18 | }); 19 | 20 | // then run prettier on the generated files 21 | execSync("prettier --write ./dist/**/*.d.ts"); 22 | execSync("prettier --write ./dist/**/*.d.mts"); 23 | execSync("prettier --write ./dist/**/*.js"); 24 | execSync("prettier --write ./dist/**/*.mjs"); 25 | 26 | process.exit(0); 27 | -------------------------------------------------------------------------------- /packages/partysocket/src/event-target-polyfill.ts: -------------------------------------------------------------------------------- 1 | import "event-target-polyfill"; 2 | -------------------------------------------------------------------------------- /packages/partysocket/src/react.ts: -------------------------------------------------------------------------------- 1 | import PartySocket from "./index"; 2 | import { useAttachWebSocketEventHandlers } from "./use-handlers"; 3 | import { 4 | getOptionsThatShouldCauseRestartWhenChanged, 5 | useStableSocket 6 | } from "./use-socket"; 7 | 8 | import type { PartySocketOptions } from "./index"; 9 | import type { EventHandlerOptions } from "./use-handlers"; 10 | 11 | type UsePartySocketOptions = Omit & 12 | EventHandlerOptions & { 13 | host?: string | undefined; 14 | }; 15 | 16 | // A React hook that wraps PartySocket 17 | export default function usePartySocket(options: UsePartySocketOptions) { 18 | const { host, ...otherOptions } = options; 19 | 20 | const socket = useStableSocket({ 21 | options: { 22 | host: 23 | host || 24 | (typeof window !== "undefined" 25 | ? window.location.host 26 | : "dummy-domain.com"), 27 | ...otherOptions 28 | }, 29 | createSocket: (options) => new PartySocket(options), 30 | createSocketMemoKey: (options) => 31 | JSON.stringify([ 32 | // NOTE: if query is defined as a function, the socket 33 | // won't reconnect when you change the function identity 34 | options.query, 35 | options.id, 36 | options.host, 37 | options.room, 38 | options.party, 39 | options.path, 40 | options.protocol, 41 | options.protocols, 42 | options.basePath, 43 | options.prefix, 44 | ...getOptionsThatShouldCauseRestartWhenChanged(options) 45 | ]) 46 | }); 47 | 48 | useAttachWebSocketEventHandlers(socket, options); 49 | 50 | return socket; 51 | } 52 | 53 | export { default as useWebSocket } from "./use-ws"; 54 | 55 | // TODO: remove the default export in a future breaking change 56 | export { usePartySocket }; 57 | -------------------------------------------------------------------------------- /packages/partysocket/src/tests/fixture.js: -------------------------------------------------------------------------------- 1 | export default { 2 | onConnect(ws, _room) { 3 | ws.onmessage = function incoming(evt) { 4 | if (evt.data === "ping") { 5 | ws.send("pong"); 6 | } else { 7 | ws.send("unknown"); 8 | } 9 | }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/partysocket/src/tests/partysocket.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment jsdom 3 | */ 4 | 5 | import { expect, test } from "vitest"; 6 | 7 | import PartySocket from "../index"; 8 | 9 | const PORT = 50121; 10 | const URL = `localhost:${PORT}/`; 11 | 12 | test("throws if host or room is not set", () => { 13 | expect(() => { 14 | const partySocket = new PartySocket({ host: "" }); 15 | partySocket.reconnect(); 16 | }).toThrow(); 17 | }); 18 | 19 | test("if the room is correctly updated after calling updateProperties", () => { 20 | const partySocket = new PartySocket({ host: URL, room: "first-room" }); 21 | partySocket.updateProperties({ room: "second-room" }); 22 | 23 | expect(partySocket.room).toBe("second-room"); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/partysocket/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["ESNext", "DOM"], 5 | "types": ["@cloudflare/workers-types", "node"] 6 | }, 7 | "exclude": ["tests"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/partysocket/src/type-helper.ts: -------------------------------------------------------------------------------- 1 | export type TypedEventTarget = { 2 | new (): IntermediateEventTarget; 3 | }; 4 | 5 | // internal helper type 6 | interface IntermediateEventTarget extends EventTarget { 7 | addEventListener( 8 | type: K, 9 | callback: ( 10 | event: EventMap[K] extends Event ? EventMap[K] : never 11 | ) => EventMap[K] extends Event ? void : never, 12 | options?: boolean | AddEventListenerOptions 13 | ): void; 14 | 15 | addEventListener( 16 | type: string, 17 | callback: EventListenerOrEventListenerObject | null, 18 | options?: EventListenerOptions | boolean 19 | ): void; 20 | 21 | removeEventListener( 22 | type: K, 23 | callback: ( 24 | event: EventMap[K] extends Event ? EventMap[K] : never 25 | ) => EventMap[K] extends Event ? void : never, 26 | options?: boolean | AddEventListenerOptions 27 | ): void; 28 | 29 | removeEventListener( 30 | type: string, 31 | callback: EventListenerOrEventListenerObject | null, 32 | options?: EventListenerOptions | boolean 33 | ): void; 34 | } 35 | -------------------------------------------------------------------------------- /packages/partysocket/src/use-handlers.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | import type WebSocket from "./ws"; 4 | 5 | export type EventHandlerOptions = { 6 | onOpen?: (event: WebSocketEventMap["open"]) => void; 7 | onMessage?: (event: WebSocketEventMap["message"]) => void; 8 | onClose?: (event: WebSocketEventMap["close"]) => void; 9 | onError?: (event: WebSocketEventMap["error"]) => void; 10 | }; 11 | 12 | /** Attaches event handlers to a WebSocket in a React Lifecycle-friendly way */ 13 | export const useAttachWebSocketEventHandlers = ( 14 | socket: WebSocket, 15 | options: EventHandlerOptions 16 | ) => { 17 | const handlersRef = useRef(options); 18 | handlersRef.current = options; 19 | 20 | useEffect(() => { 21 | const onOpen: EventHandlerOptions["onOpen"] = (event) => 22 | handlersRef.current?.onOpen?.(event); 23 | const onMessage: EventHandlerOptions["onMessage"] = (event) => 24 | handlersRef.current?.onMessage?.(event); 25 | const onClose: EventHandlerOptions["onClose"] = (event) => 26 | handlersRef.current?.onClose?.(event); 27 | const onError: EventHandlerOptions["onError"] = (event) => 28 | handlersRef.current?.onError?.(event); 29 | 30 | socket.addEventListener("open", onOpen); 31 | socket.addEventListener("close", onClose); 32 | socket.addEventListener("error", onError); 33 | socket.addEventListener("message", onMessage); 34 | 35 | return () => { 36 | socket.removeEventListener("open", onOpen); 37 | socket.removeEventListener("close", onClose); 38 | socket.removeEventListener("error", onError); 39 | socket.removeEventListener("message", onMessage); 40 | }; 41 | }, [socket]); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/partysocket/src/use-socket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from "react"; 2 | 3 | import type WebSocket from "./ws"; 4 | import type { Options } from "./ws"; 5 | 6 | /** When any of the option values are changed, we should reinitialize the socket */ 7 | export const getOptionsThatShouldCauseRestartWhenChanged = ( 8 | options: Options 9 | ) => [ 10 | options.startClosed, 11 | options.minUptime, 12 | options.maxRetries, 13 | options.connectionTimeout, 14 | options.maxEnqueuedMessages, 15 | options.maxReconnectionDelay, 16 | options.minReconnectionDelay, 17 | options.reconnectionDelayGrowFactor, 18 | options.debug 19 | ]; 20 | 21 | /** 22 | * Initializes a PartySocket (or WebSocket) and keeps it stable across renders, 23 | * but reconnects and updates the reference when any of the connection args change. 24 | */ 25 | export function useStableSocket({ 26 | options, 27 | createSocket, 28 | createSocketMemoKey: createOptionsMemoKey 29 | }: { 30 | options: TOpts; 31 | createSocket: (options: TOpts) => T; 32 | createSocketMemoKey: (options: TOpts) => string; 33 | }) { 34 | // ensure we only reconnect when necessary 35 | const shouldReconnect = createOptionsMemoKey(options); 36 | // biome-ignore lint/correctness/useExhaustiveDependencies: 37 | const socketOptions = useMemo(() => { 38 | return options; 39 | }, [shouldReconnect]); 40 | 41 | // this is the socket we return 42 | const [socket, setSocket] = useState(() => 43 | // only connect on first mount 44 | createSocket({ ...socketOptions, startClosed: true }) 45 | ); 46 | 47 | // keep track of the socket we initialized 48 | const socketInitializedRef = useRef(null); 49 | 50 | // allow changing the socket factory without reconnecting 51 | const createSocketRef = useRef(createSocket); 52 | createSocketRef.current = createSocket; 53 | 54 | // finally, initialize the socket 55 | useEffect(() => { 56 | // we haven't yet restarted the socket 57 | if (socketInitializedRef.current === socket) { 58 | // create new socket 59 | const newSocket = createSocketRef.current({ 60 | ...socketOptions, 61 | // when reconnecting because of options change, we always reconnect 62 | // (startClosed only applies to initial mount) 63 | startClosed: false 64 | }); 65 | 66 | // update socket reference (this will cause the effect to run again) 67 | setSocket(newSocket); 68 | } else { 69 | // if this is the first time we are running the hook, connect... 70 | if (!socketInitializedRef.current && socketOptions.startClosed !== true) { 71 | socket.reconnect(); 72 | } 73 | // track initialized socket so we know not to do it again 74 | socketInitializedRef.current = socket; 75 | // close the old socket the next time the socket changes or we unmount 76 | return () => { 77 | socket.close(); 78 | }; 79 | } 80 | }, [socket, socketOptions]); 81 | 82 | return socket; 83 | } 84 | -------------------------------------------------------------------------------- /packages/partysocket/src/use-ws.ts: -------------------------------------------------------------------------------- 1 | import { useAttachWebSocketEventHandlers } from "./use-handlers"; 2 | import { 3 | getOptionsThatShouldCauseRestartWhenChanged, 4 | useStableSocket 5 | } from "./use-socket"; 6 | import WebSocket from "./ws"; 7 | 8 | import type { EventHandlerOptions } from "./use-handlers"; 9 | import type { Options, ProtocolsProvider, UrlProvider } from "./ws"; 10 | 11 | type UseWebSocketOptions = Options & EventHandlerOptions; 12 | 13 | // A React hook that wraps PartySocket 14 | export default function useWebSocket( 15 | url: UrlProvider, 16 | protocols?: ProtocolsProvider, 17 | options: UseWebSocketOptions = {} 18 | ) { 19 | const socket = useStableSocket({ 20 | options, 21 | createSocket: (options) => new WebSocket(url, protocols, options), 22 | createSocketMemoKey: (options) => 23 | JSON.stringify([ 24 | // will reconnect if url or protocols are specified as a string. 25 | // if they are functions, the WebSocket will handle reconnection 26 | url, 27 | protocols, 28 | ...getOptionsThatShouldCauseRestartWhenChanged(options) 29 | ]) 30 | }); 31 | 32 | useAttachWebSocketEventHandlers(socket, options); 33 | 34 | return socket; 35 | } 36 | -------------------------------------------------------------------------------- /packages/partysub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partysub", 3 | "version": "0.0.29", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/partysub", 9 | "type": "module", 10 | "exports": { 11 | "./server": { 12 | "types": "./dist/server/index.d.ts", 13 | "require": "./dist/server/index.js", 14 | "import": "./dist/server/index.js" 15 | }, 16 | "./client": { 17 | "types": "./dist/client/index.d.ts", 18 | "require": "./dist/client/index.js", 19 | "import": "./dist/client/index.js" 20 | }, 21 | "./react": { 22 | "types": "./dist/client/react.d.ts", 23 | "require": "./dist/client/react.js", 24 | "import": "./dist/client/react.js" 25 | } 26 | }, 27 | "scripts": { 28 | "build": "tsx scripts/build.ts", 29 | "check:test": "vitest -r src/server/tests --watch false" 30 | }, 31 | "files": [ 32 | "dist", 33 | "README.md" 34 | ], 35 | "keywords": [ 36 | "pubsub", 37 | "websockets" 38 | ], 39 | "author": "Sunil Pai ", 40 | "license": "ISC", 41 | "description": "", 42 | "dependencies": {}, 43 | "peerDependencies": { 44 | "@cloudflare/workers-types": "^4.20240729.0", 45 | "partyserver": "^0.0.71", 46 | "partysocket": "^1.1.3" 47 | }, 48 | "devDependencies": { 49 | "@cloudflare/workers-types": "^4.20250327.0", 50 | "partyserver": "^0.0.71", 51 | "partysocket": "^1.1.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/partysub/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: ["src/server/index.ts", "src/client/index.ts", "src/client/react.tsx"], 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ["cloudflare:workers", "partyserver", "react"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | // then run prettier on the generated .d.ts files 15 | execSync("prettier --write ./dist/**/*.d.ts"); 16 | 17 | process.exit(0); 18 | -------------------------------------------------------------------------------- /packages/partysub/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysub/src/client/index.ts: -------------------------------------------------------------------------------- 1 | console.error("To be implemented"); 2 | -------------------------------------------------------------------------------- /packages/partysub/src/client/react.tsx: -------------------------------------------------------------------------------- 1 | console.error("To be implemented"); 2 | -------------------------------------------------------------------------------- /packages/partysub/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysub/src/server/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@cloudflare/workers-types/experimental", 6 | "@cloudflare/vitest-pool-workers" 7 | ] 8 | }, 9 | "include": ["./**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/partysub/src/server/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | isolatedStorage: false, 8 | wrangler: { 9 | configPath: "./wrangler.toml" 10 | } 11 | } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /packages/partysub/src/server/tests/worker.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | fetch() { 3 | return new Response("Hello, world!"); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /packages/partysub/src/server/tests/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "partysub-tests" 2 | main = "./worker.ts" 3 | compatibility_date = "2024-07-25" 4 | compatibility_flags = ["nodejs_compat"] 5 | -------------------------------------------------------------------------------- /packages/partysub/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | }, 6 | "exclude": ["tests/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/partysync/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partysync", 3 | "version": "0.0.25", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/partysync", 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "require": "./dist/index.js", 14 | "import": "./dist/index.js" 15 | }, 16 | "./rpc": { 17 | "types": "./dist/rpc.d.ts", 18 | "require": "./dist/rpc.js", 19 | "import": "./dist/rpc.js" 20 | }, 21 | "./agent": { 22 | "types": "./dist/agent/index.d.ts", 23 | "require": "./dist/agent/index.js", 24 | "import": "./dist/agent/index.js" 25 | }, 26 | "./client": { 27 | "types": "./dist/client/index.d.ts", 28 | "require": "./dist/client/index.js", 29 | "import": "./dist/client/index.js" 30 | }, 31 | "./server": { 32 | "types": "./dist/server/index.d.ts", 33 | "require": "./dist/server/index.js", 34 | "import": "./dist/server/index.js" 35 | }, 36 | "./react": { 37 | "types": "./dist/react/index.d.ts", 38 | "require": "./dist/react/index.js", 39 | "import": "./dist/react/index.js" 40 | } 41 | }, 42 | "scripts": { 43 | "build": "tsx scripts/build.ts" 44 | }, 45 | "files": [ 46 | "dist", 47 | "README.md" 48 | ], 49 | "keywords": [ 50 | "sync", 51 | "local-first", 52 | "durable-objects" 53 | ], 54 | "author": "Sunil Pai ", 55 | "license": "ISC", 56 | "description": "", 57 | "dependencies": { 58 | "idb": "^8.0.2", 59 | "nanoid": "^5.1.5" 60 | }, 61 | "peerDependencies": { 62 | "@cloudflare/workers-types": "^4.20240729.0", 63 | "partyserver": "^0.0.71" 64 | }, 65 | "devDependencies": { 66 | "@cloudflare/workers-types": "^4.20250327.0", 67 | "partyserver": "^0.0.71" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/partysync/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: [ 6 | "src/index.ts", 7 | "src/server/index.ts", 8 | "src/client/index.ts", 9 | "src/react/index.tsx", 10 | "src/agent/index.ts", 11 | "src/rpc.ts" 12 | ], 13 | splitting: true, 14 | sourcemap: true, 15 | clean: true, 16 | external: ["cloudflare:workers", "partyserver", "react"], 17 | format: "esm", 18 | dts: true 19 | }); 20 | 21 | // then run prettier on the generated .d.ts files 22 | execSync("prettier --write ./dist/**/*.d.ts"); 23 | 24 | process.exit(0); 25 | -------------------------------------------------------------------------------- /packages/partysync/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysync/src/agent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysync/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export class SyncClient {} 2 | -------------------------------------------------------------------------------- /packages/partysync/src/client/persist.ts: -------------------------------------------------------------------------------- 1 | // This is a simple client-side persistence layer for PartySync. 2 | // We use a indexeddb database to store the data. 3 | 4 | // In this database, we store a table for each channel 5 | // And the record is stored as 6 | // key: record[0] // the id 7 | // value: record // the record 8 | 9 | import * as idb from "idb"; 10 | 11 | export class Persist { 12 | private readonly dbName = "partysync"; 13 | private readonly version = 1; 14 | 15 | constructor(private readonly channel: string) {} 16 | 17 | private async getDb() { 18 | const channel = this.channel; 19 | return idb.openDB(`${this.dbName}-${channel}`, this.version, { 20 | upgrade(db) { 21 | if (!db.objectStoreNames.contains(channel)) { 22 | db.createObjectStore(channel, { keyPath: "key" }); 23 | } 24 | } 25 | }); 26 | } 27 | 28 | async getAll(): Promise { 29 | const db = await this.getDb(); 30 | const records = await db.getAll(this.channel); 31 | 32 | const values = []; 33 | for (const record of records) { 34 | values.push(record.value); 35 | } 36 | values.sort( 37 | (a, b) => new Date(a.at(-3)).getTime() - new Date(b.at(-3)).getTime() 38 | ); // sort by created_at 39 | return values; 40 | } 41 | 42 | async set(records: RecordType[]): Promise { 43 | const db = await this.getDb(); 44 | const tx = db.transaction(this.channel, "readwrite"); 45 | const store = tx.objectStore(this.channel); 46 | 47 | for (const record of records) { 48 | await store.put({ 49 | key: record[0], 50 | value: record 51 | }); 52 | } 53 | 54 | await tx.done; 55 | } 56 | 57 | // async delete(id: string): Promise { 58 | // const db = await this.getDb(); 59 | // await db.delete(this.channel, id); 60 | // } 61 | 62 | async deleteDeletedRecordsBefore(date: Date): Promise { 63 | const db = await this.getDb(); 64 | const records = await db.getAll(this.channel); 65 | const tx = db.transaction(this.channel, "readwrite"); 66 | const store = tx.objectStore(this.channel); 67 | 68 | const cutoff = date.getTime(); 69 | for (const record of records) { 70 | if ( 71 | record.value.at(-1) !== null && 72 | new Date(record.value.at(-1)).getTime() < cutoff 73 | ) { 74 | await store.delete(record.key); 75 | } 76 | } 77 | await tx.done; 78 | } 79 | 80 | // async deleteAll(): Promise { 81 | // const db = await this.getDb(); 82 | // await db.clear(this.channel); 83 | // } 84 | } 85 | -------------------------------------------------------------------------------- /packages/partysync/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysync/src/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysync/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partysync/src/types.ts: -------------------------------------------------------------------------------- 1 | export type BroadcastMessage = 2 | | { 3 | broadcast: true; 4 | channel: string; 5 | type: "update"; 6 | payload: T[]; 7 | } 8 | | { 9 | broadcast: true; 10 | channel: string; 11 | type: "delete-all"; 12 | }; 13 | 14 | export type SyncRequest = { 15 | channel: string; 16 | sync: true; 17 | from: number | null; 18 | }; 19 | 20 | export type SyncResponse = { 21 | channel: string; 22 | sync: true; 23 | payload: T[]; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/partytracks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partytracks", 3 | "version": "0.0.47", 4 | "scripts": { 5 | "check:test": "(cd tests; vitest --no-watch)", 6 | "prebuild": "rm -rf dist", 7 | "build": "tsx scripts/build.ts", 8 | "postbuild": "prettier --write ./dist/*/**.d.ts" 9 | }, 10 | "exports": { 11 | "./client": { 12 | "import": { 13 | "types": "./dist/client/index.d.ts", 14 | "default": "./dist/client/index.js" 15 | } 16 | }, 17 | "./react": { 18 | "import": { 19 | "types": "./dist/react/index.d.ts", 20 | "default": "./dist/react/index.js" 21 | } 22 | }, 23 | "./server": { 24 | "import": { 25 | "types": "./dist/server/index.d.ts", 26 | "default": "./dist/server/index.js" 27 | } 28 | } 29 | }, 30 | "type": "module", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/cloudflare/partykit.git" 34 | }, 35 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/partytracks", 36 | "files": [ 37 | "dist", 38 | "README.md" 39 | ], 40 | "keywords": [ 41 | "webrtc", 42 | "calls", 43 | "durable-objects" 44 | ], 45 | "author": "Kevin Kipp ", 46 | "license": "ISC", 47 | "description": "Integrate Cloudflare Realtime into your PartyServer app", 48 | "dependencies": { 49 | "cookie": "^1.0.2", 50 | "jose": "^6.0.10", 51 | "rxjs": "^7.8.2", 52 | "tiny-invariant": "^1.3.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/partytracks/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { build } from "tsup"; 2 | 3 | await build({ 4 | entry: ["src/client/index.ts", "src/react/index.ts", "src/server/index.ts"], 5 | splitting: true, 6 | sourcemap: true, 7 | clean: true, 8 | noExternal: ["cookie", "jose", "tiny-invariant"], 9 | external: ["react"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | process.exit(0); 15 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/History.ts: -------------------------------------------------------------------------------- 1 | export class History extends EventTarget { 2 | entries: T[] = []; 3 | private limit: number; 4 | 5 | constructor(limit: number = Number.POSITIVE_INFINITY) { 6 | super(); 7 | this.limit = Math.max(1, limit); // Ensure the limit is at least 1 8 | } 9 | 10 | addEventListener( 11 | type: "logentry", 12 | callback: 13 | | { handleEvent: (event: Event & { entry: T }) => void } 14 | | ((event: Event & { entry: T }) => void) 15 | | null, 16 | options?: boolean | AddEventListenerOptions | undefined 17 | ): void { 18 | super.addEventListener( 19 | type, 20 | // @ts-expect-error 21 | callback, 22 | options 23 | ); 24 | return; 25 | } 26 | 27 | removeEventListener( 28 | type: "logentry", 29 | callback: 30 | | { handleEvent: (event: Event & { entry: T }) => void } 31 | | ((event: Event & { entry: T }) => void) 32 | | null, 33 | options?: boolean | AddEventListenerOptions | undefined 34 | ): void { 35 | super.removeEventListener( 36 | type, 37 | // @ts-expect-error 38 | callback, 39 | options 40 | ); 41 | return; 42 | } 43 | 44 | log(entry: T) { 45 | if (this.entries.length >= this.limit) { 46 | this.entries.shift(); // Remove the oldest entry 47 | } 48 | this.entries.push(entry); 49 | this.dispatchEvent(new CustomEvent("logentry")); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/Peer.utils.ts: -------------------------------------------------------------------------------- 1 | type Task = () => Promise; 2 | 3 | const DefaultBatchSizeLimit = 64; 4 | 5 | export class FIFOScheduler { 6 | #schedulerChain: Promise; 7 | constructor() { 8 | this.#schedulerChain = Promise.resolve(); 9 | } 10 | 11 | schedule(task: Task): Promise { 12 | return new Promise((resolve, reject) => { 13 | this.#schedulerChain = this.#schedulerChain.then(async () => { 14 | try { 15 | resolve(await task()); 16 | } catch (error) { 17 | reject(error as unknown); 18 | } 19 | }); 20 | }); 21 | } 22 | } 23 | 24 | export class BulkRequestDispatcher { 25 | #currentBatch: RequestEntryParams[]; 26 | #currentBulkResponse: Promise | null; 27 | #batchSizeLimit: number; 28 | constructor(batchSizeLimit: number = DefaultBatchSizeLimit) { 29 | this.#currentBatch = []; 30 | this.#currentBulkResponse = null; 31 | this.#batchSizeLimit = batchSizeLimit; 32 | } 33 | 34 | // doBulkRequest will return a bulk response promise. 35 | // At the event loop iteration end, the accumulated entries will be dispatched as a bulk request 36 | doBulkRequest( 37 | params: RequestEntryParams, 38 | bulkRequestFunc: (bulkCopy: RequestEntryParams[]) => Promise 39 | ): Promise { 40 | if (this.#currentBatch.length >= this.#batchSizeLimit) { 41 | // if it reaches the batch size limit, we make another bulk request 42 | this.#currentBatch = []; 43 | this.#currentBulkResponse = null; 44 | } 45 | this.#currentBatch.push(params); 46 | if (this.#currentBulkResponse != null) { 47 | return this.#currentBulkResponse; 48 | } 49 | // save the current batch list reference in the function scope because this.#currentBatch could be reset if 50 | // the batch limit is reached 51 | const batch = this.#currentBatch; 52 | this.#currentBulkResponse = new Promise((resolve, reject) => { 53 | // script 54 | // | 55 | // V 56 | // microtasks (Promise) 57 | // | 58 | // V 59 | // macrotasks (setTimeout) 60 | // 61 | // macrotasks are ran in the event loop iteration end, so 62 | // we use that moment to make the bulkRequestFunc call 63 | setTimeout(() => { 64 | // When the bulk request happens, the batch list and 65 | // the response is reset to start another batch. Coming 66 | // callers will wait for a new response promise 67 | this.#currentBulkResponse = null; 68 | // we cut here to make the bulk request 69 | const batchCopy = batch.splice(0, batch.length); 70 | const p = bulkRequestFunc(batchCopy); 71 | p.then((r) => { 72 | resolve(r); 73 | }).catch((err) => { 74 | reject(err); 75 | }); 76 | }, 0); 77 | }); 78 | // This bulk response needs to be processed to extract the the result for the entry 79 | return this.#currentBulkResponse; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/audioSink.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, map } from "rxjs"; 2 | import type { Observable, Subscription } from "rxjs"; 3 | import { devices$ } from "./resilientTrack$"; 4 | 5 | export interface CreateSinkOptions { 6 | audioElement: HTMLAudioElement; 7 | sinkId?: string; 8 | } 9 | 10 | export interface SinkApi { 11 | attach: (pulledAudioTrack$: Observable) => Subscription; 12 | setSinkId: (sinkId: string) => void; 13 | devices$: Observable; 14 | cleanup: () => void; 15 | } 16 | 17 | export const createAudioSink = ({ 18 | audioElement, 19 | sinkId = "default" 20 | }: CreateSinkOptions): SinkApi => { 21 | audioElement.setSinkId(sinkId); 22 | const sinkId$ = new BehaviorSubject(sinkId); 23 | const mediaStream = new MediaStream(); 24 | audioElement.srcObject = mediaStream; 25 | const resetSrcObject = () => { 26 | audioElement.addEventListener("canplay", () => audioElement.play(), { 27 | once: true 28 | }); 29 | audioElement.srcObject = mediaStream; 30 | }; 31 | const subs: Subscription[] = []; 32 | 33 | const attach = (pulledAudioTrack$: Observable) => { 34 | const sub = pulledAudioTrack$.subscribe((track) => { 35 | mediaStream.addTrack(track); 36 | resetSrcObject(); 37 | sub.add(() => { 38 | mediaStream.removeTrack(track); 39 | resetSrcObject(); 40 | }); 41 | }); 42 | subs.push(sub); 43 | return sub; 44 | }; 45 | 46 | const setSinkId = (sinkId: string) => { 47 | audioElement.setSinkId(sinkId); 48 | sinkId$.next(sinkId); 49 | }; 50 | 51 | const cleanup = () => { 52 | subs.forEach((s) => s.unsubscribe()); 53 | audioElement.srcObject = null; 54 | }; 55 | 56 | return { 57 | attach, 58 | setSinkId, 59 | devices$: devices$.pipe( 60 | map((devices) => devices.filter((d) => d.kind === "audiooutput")) 61 | ), 62 | cleanup 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/blackCanvasTrack$.ts: -------------------------------------------------------------------------------- 1 | import { Observable, shareReplay } from "rxjs"; 2 | import invariant from "tiny-invariant"; 3 | 4 | export const blackCanvasTrack$ = new Observable( 5 | (subscriber) => { 6 | const canvas = document.createElement("canvas"); 7 | canvas.height = 720; 8 | canvas.width = 1280; 9 | const ctx = canvas.getContext("2d"); 10 | invariant(ctx); 11 | ctx.fillStyle = "black"; 12 | ctx.fillRect(0, 0, canvas.width, canvas.height); 13 | // we need to draw to the canvas in order for video 14 | // frames to be sent on the video track 15 | const i = setInterval(() => { 16 | ctx.fillStyle = "black"; 17 | ctx.fillRect(0, 0, canvas.width, canvas.height); 18 | }, 1000); 19 | const track = canvas.captureStream().getVideoTracks()[0]; 20 | track.addEventListener("ended", () => { 21 | subscriber.complete(); 22 | }); 23 | subscriber.add(() => { 24 | track.stop(); 25 | clearInterval(i); 26 | }); 27 | subscriber.next(track); 28 | } 29 | ).pipe(shareReplay({ refCount: true, bufferSize: 1 })); 30 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/callsTypes.ts: -------------------------------------------------------------------------------- 1 | export interface SessionDescription { 2 | type: "offer" | "answer"; 3 | sdp: string; 4 | } 5 | 6 | export interface ErrorResponse { 7 | errorCode?: string; 8 | errorDescription?: string; 9 | } 10 | 11 | export type NewSessionRequest = { 12 | sessionDescription: SessionDescription; 13 | }; 14 | 15 | export interface NewSessionResponse extends ErrorResponse { 16 | sessionDescription: SessionDescription; 17 | sessionId: string; 18 | } 19 | 20 | export type TrackMetadata = { 21 | location?: "local" | "remote"; 22 | trackName?: string; 23 | sessionId?: string; 24 | mid?: string | null; 25 | simulcast?: { 26 | preferredRid: string; 27 | }; 28 | }; 29 | 30 | export type TracksRequest = { 31 | tracks: TrackMetadata[]; 32 | sessionDescription?: SessionDescription; 33 | }; 34 | 35 | export interface TracksResponse extends ErrorResponse { 36 | sessionDescription: SessionDescription; 37 | requiresImmediateRenegotiation: boolean; 38 | tracks?: (TrackMetadata & ErrorResponse)[]; 39 | } 40 | 41 | export type RenegotiateRequest = { 42 | sessionDescription: SessionDescription; 43 | }; 44 | 45 | export interface RenegotiationResponse extends ErrorResponse {} 46 | 47 | export type CloseTracksRequest = TracksRequest & { 48 | force: boolean; 49 | }; 50 | 51 | export interface EmptyResponse extends ErrorResponse {} 52 | 53 | export type CallsRequest = 54 | | NewSessionRequest 55 | | TracksRequest 56 | | RenegotiateRequest 57 | | CloseTracksRequest; 58 | export type CallsResponse = EmptyResponse | TracksResponse; 59 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/debug.ts: -------------------------------------------------------------------------------- 1 | import { tap } from "rxjs"; 2 | 3 | import { logger } from "./logging"; 4 | 5 | export function debugTap(message: string) { 6 | return tap({ 7 | next: (...args) => logger.debug(message, ...args), 8 | complete: () => logger.debug("COMPLETED ", message) 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/inaudibleTrack$.ts: -------------------------------------------------------------------------------- 1 | import { Observable, shareReplay } from "rxjs"; 2 | 3 | const userGestureEvents = [ 4 | "click", 5 | "contextmenu", 6 | "auxclick", 7 | "dblclick", 8 | "mousedown", 9 | "mouseup", 10 | "pointerup", 11 | "touchend", 12 | "keydown", 13 | "keyup" 14 | ]; 15 | 16 | let audioContextStartedPreviously = false; 17 | 18 | export const inaudibleAudioTrack$ = new Observable( 19 | (subscriber) => { 20 | const audioContext = new AudioContext(); 21 | 22 | const oscillator = audioContext.createOscillator(); 23 | oscillator.type = "triangle"; 24 | // roughly sounds like a box fan 25 | oscillator.frequency.setValueAtTime(20, audioContext.currentTime); 26 | 27 | const gainNode = audioContext.createGain(); 28 | // even w/ gain at 0 some packets are sent 29 | gainNode.gain.setValueAtTime(0, audioContext.currentTime); 30 | 31 | oscillator.connect(gainNode); 32 | 33 | const destination = audioContext.createMediaStreamDestination(); 34 | gainNode.connect(destination); 35 | 36 | const track = destination.stream.getAudioTracks()[0]; 37 | subscriber.next(track); 38 | 39 | let oscillatorStarted = false; 40 | const ensureOscillatorStarted = () => { 41 | if (oscillatorStarted) return; 42 | oscillator.start(); 43 | oscillatorStarted = true; 44 | }; 45 | 46 | const stateChangeHandler = () => { 47 | if (audioContext.state === "running") { 48 | audioContextStartedPreviously = true; 49 | ensureOscillatorStarted(); 50 | } 51 | if ( 52 | audioContext.state === "suspended" || 53 | (audioContext.state as string) === "interrupted" 54 | ) { 55 | resumeAudioContext(); 56 | } 57 | }; 58 | 59 | audioContext.addEventListener("statechange", stateChangeHandler); 60 | 61 | const resumeAudioContext = () => { 62 | audioContext.resume().then(() => { 63 | cleanUpUserGestureListeners(); 64 | }); 65 | }; 66 | 67 | const cleanUpUserGestureListeners = () => { 68 | userGestureEvents.forEach((gesture) => { 69 | document.removeEventListener(gesture, resumeAudioContext, { 70 | capture: true 71 | }); 72 | }); 73 | }; 74 | 75 | if (audioContextStartedPreviously) { 76 | resumeAudioContext(); 77 | } else { 78 | userGestureEvents.forEach((gesture) => { 79 | document.addEventListener(gesture, resumeAudioContext, { 80 | capture: true 81 | }); 82 | }); 83 | } 84 | 85 | return () => { 86 | track.stop(); 87 | audioContext.close(); 88 | audioContext.removeEventListener("statechange", stateChangeHandler); 89 | cleanUpUserGestureListeners(); 90 | }; 91 | } 92 | ).pipe(shareReplay({ refCount: true, bufferSize: 1 })); 93 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export { PartyTracks } from "./PartyTracks"; 2 | export { 3 | resilientTrack$, 4 | DevicesExhaustedError, 5 | devices$ 6 | } from "./resilientTrack$"; 7 | export { screenshare$ } from "./screenshare$"; 8 | export { getScreenshare } from "./getScreenshare.ts"; 9 | export type { Screenshare } from "./getScreenshare.ts"; 10 | export { setLogLevel } from "./logging"; 11 | export type { PartyTracksConfig, ApiHistoryEntry } from "./PartyTracks"; 12 | export type { TrackMetadata } from "./callsTypes"; 13 | export { createAudioSink } from "./audioSink"; 14 | export { getMic, getCamera } from "./getDevices"; 15 | export type { MediaDeviceOptions, MediaDevice } from "./getDevices"; 16 | export type { CreateSinkOptions, SinkApi } from "./audioSink"; 17 | export type { SafePermissionState } from "./permission$.ts"; 18 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/logging.ts: -------------------------------------------------------------------------------- 1 | const logLevels = ["none", "error", "warn", "info", "debug"] as const; 2 | export type LogLevel = (typeof logLevels)[number]; 3 | export type Logger = typeof logger; 4 | 5 | const levelRef = { 6 | current: "warn" as LogLevel 7 | }; 8 | export const setLogLevel = (newLogLevel: LogLevel) => { 9 | levelRef.current = newLogLevel; 10 | }; 11 | 12 | export const logger = { 13 | error: (...data: unknown[]) => { 14 | if ( 15 | logLevels.indexOf(levelRef.current) >= logLevels.indexOf("error" as const) 16 | ) { 17 | console.error(...data); 18 | } 19 | }, 20 | warn: (...data: unknown[]) => { 21 | if ( 22 | logLevels.indexOf(levelRef.current) >= logLevels.indexOf("warn" as const) 23 | ) { 24 | console.warn(...data); 25 | } 26 | }, 27 | info: (...data: unknown[]) => { 28 | if ( 29 | logLevels.indexOf(levelRef.current) >= logLevels.indexOf("info" as const) 30 | ) { 31 | console.info(...data); 32 | } 33 | }, 34 | log: (...data: unknown[]) => { 35 | if ( 36 | logLevels.indexOf(levelRef.current) >= logLevels.indexOf("info" as const) 37 | ) { 38 | console.log(...data); 39 | } 40 | }, 41 | debug: (...data: unknown[]) => { 42 | if ( 43 | logLevels.indexOf(levelRef.current) >= logLevels.indexOf("debug" as const) 44 | ) { 45 | console.debug(...data); 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/permission$.ts: -------------------------------------------------------------------------------- 1 | import { 2 | catchError, 3 | combineLatest, 4 | concat, 5 | defer, 6 | from, 7 | fromEvent, 8 | map, 9 | of, 10 | switchMap 11 | } from "rxjs"; 12 | import type { Observable } from "rxjs"; 13 | 14 | export type SafePermissionState = PermissionState | "unknown"; 15 | 16 | export const permission$ = ( 17 | name: "camera" | "microphone" 18 | ): Observable => { 19 | return concat( 20 | defer(() => 21 | from(navigator.permissions.query({ name }).then((ps) => ps.state)) 22 | ), 23 | defer(() => 24 | from(navigator.permissions.query({ name })).pipe( 25 | switchMap((permissionStatus) => 26 | combineLatest([ 27 | of(permissionStatus), 28 | fromEvent(permissionStatus, "change") 29 | ]) 30 | ), 31 | map(([permissionStatus]) => permissionStatus.state) 32 | ) 33 | ) 34 | ).pipe(catchError(() => of("unknown" as const))); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/rxjs-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Observable } from "rxjs"; 2 | import { timer } from "rxjs"; 3 | import { retry } from "rxjs/operators"; 4 | 5 | interface BackoffConfig { 6 | maxRetries?: number; 7 | initialDelay?: number; 8 | maxDelay?: number; 9 | backoffFactor?: number; 10 | resetOnSuccess?: boolean; 11 | } 12 | 13 | const configDefaults: Required = { 14 | maxRetries: Number.POSITIVE_INFINITY, 15 | initialDelay: 250, 16 | maxDelay: 10000, 17 | backoffFactor: 2, 18 | resetOnSuccess: true 19 | }; 20 | 21 | export function retryWithBackoff(config: BackoffConfig = {}) { 22 | const mergedConfig = { 23 | ...configDefaults, 24 | ...config 25 | }; 26 | const { 27 | maxRetries = Number.POSITIVE_INFINITY, 28 | initialDelay = 250, 29 | maxDelay = 10000, 30 | backoffFactor = 2, 31 | resetOnSuccess = true 32 | } = mergedConfig; 33 | 34 | return (source: Observable): Observable => 35 | source.pipe( 36 | retry({ 37 | count: maxRetries, 38 | resetOnSuccess, 39 | delay: (_err, count) => { 40 | // Calculate delay with exponential backoff 41 | const delay = Math.min( 42 | initialDelay * backoffFactor ** (count - 1), 43 | maxDelay 44 | ); 45 | 46 | // Return an observable that emits after the calculated delay 47 | return timer(delay); 48 | } 49 | }) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/screenshare$.ts: -------------------------------------------------------------------------------- 1 | import { Observable, ReplaySubject, share } from "rxjs"; 2 | import invariant from "tiny-invariant"; 3 | 4 | export function screenshare$(options: DisplayMediaStreamOptions) { 5 | return new Observable((subscriber) => { 6 | navigator.mediaDevices 7 | .getDisplayMedia(options) 8 | .then((ms) => { 9 | ms.getTracks().forEach((t) => { 10 | subscriber.add(() => t.stop()); 11 | t.addEventListener("ended", () => { 12 | return subscriber.complete(); 13 | }); 14 | }); 15 | subscriber.next(ms); 16 | }) 17 | .catch((err) => { 18 | invariant(err instanceof Error); 19 | // user cancelled the screenshare request 20 | if (err.name === "NotAllowedError") { 21 | subscriber.complete(); 22 | return; 23 | } 24 | subscriber.error(err); 25 | }); 26 | }).pipe( 27 | // We basically want shareReplay({refCount: true, bufferSize:1}) 28 | // but that doesn't allow for resetting on complete/error, so we 29 | // do this instead 30 | share({ 31 | resetOnComplete: true, 32 | resetOnError: true, 33 | connector: () => new ReplaySubject(1) 34 | }) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/partytracks/src/client/trackIsHealthy.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "./logging"; 2 | 3 | export async function trackIsHealthy( 4 | track: MediaStreamTrack 5 | ): Promise { 6 | logger.info("👩🏻‍⚕️ Checking track health..."); 7 | 8 | if (track.enabled) { 9 | // TODO: 10 | // if (track.kind === "audio") { 11 | // test audio stream with web audio api 12 | // } 13 | // 14 | // if (track.kind === "video") { 15 | // draw to canvas and check if all black pixels 16 | // } 17 | } 18 | 19 | const randomFailuresEnabled = 20 | localStorage.getItem("flags.randomTrackFailuresEnabled") === "true"; 21 | 22 | const randomFailure = randomFailuresEnabled && Math.random() < 0.2; 23 | 24 | if (randomFailure) { 25 | logger.info("🎲 Random track failure!"); 26 | } 27 | 28 | const healthy = !track.muted && track.readyState === "live" && !randomFailure; 29 | 30 | try { 31 | if (!healthy) { 32 | const devices = await navigator.mediaDevices.enumerateDevices(); 33 | const deviceFromTrack = devices.find( 34 | (device) => device.deviceId === track.getSettings().deviceId 35 | ); 36 | logger.info( 37 | `👩🏻‍⚕️ Track from ${deviceFromTrack?.label ?? "unkonwn device (enumerateDevices didn't find a matching device id)"} is unhealthy!` 38 | ); 39 | logger.info( 40 | `👩🏻‍⚕️ track.readyState: ${track.readyState} and track.muted: ${track.muted}`, 41 | track 42 | ); 43 | } 44 | } catch (e) { 45 | logger.error("Error getting device info for unhealthy track", e, track); 46 | } 47 | 48 | logger.info(`👩🏻‍⚕️ track is ${healthy ? "healthy" : "unhealthy"}!`); 49 | return healthy; 50 | } 51 | -------------------------------------------------------------------------------- /packages/partytracks/src/react/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./rxjsHooks"; 2 | -------------------------------------------------------------------------------- /packages/partytracks/src/server/index.ts: -------------------------------------------------------------------------------- 1 | export { routePartyTracksRequest } from "./routePartyTracksRequest"; 2 | -------------------------------------------------------------------------------- /packages/partytracks/src/ts-utils.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = { 2 | [K in keyof T]: T[K]; 3 | } & {}; 4 | -------------------------------------------------------------------------------- /packages/partytracks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "esnext"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partywhen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "partywhen", 3 | "version": "0.0.15", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/partywhen", 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "module": "dist/index.js", 13 | "scripts": { 14 | "build": "tsx scripts/build.ts", 15 | "check:test": "(cd tests; vitest --no-watch)" 16 | }, 17 | "files": [ 18 | "dist", 19 | "README.md" 20 | ], 21 | "keywords": [ 22 | "scheduling", 23 | "cron", 24 | "ai agents", 25 | "durable-objects" 26 | ], 27 | "author": "Sunil Pai ", 28 | "license": "ISC", 29 | "description": "A library for scheduling and running tasks in Cloudflare Workers", 30 | "dependencies": { 31 | "cron-parser": "^4.9.0", 32 | "partyserver": "^0.0.71" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/partywhen/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: ["src/index.ts"], 6 | splitting: true, 7 | sourcemap: true, 8 | clean: true, 9 | external: ["cloudflare:workers"], 10 | format: "esm", 11 | dts: true 12 | }); 13 | 14 | // then run prettier on the generated .d.ts files 15 | execSync("prettier --write ./dist/**/*.d.ts"); 16 | 17 | process.exit(0); 18 | -------------------------------------------------------------------------------- /packages/partywhen/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partywhen/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/partywhen/tests/env.d.ts: -------------------------------------------------------------------------------- 1 | import { Scheduler } from "../src/index"; 2 | 3 | type Env = { 4 | SCHEDULER: DurableObjectNamespace>; 5 | }; 6 | 7 | declare module "cloudflare:test" { 8 | // Controls the type of `import("cloudflare:test").env` 9 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 10 | interface ProvidedEnv extends Env {} 11 | } 12 | -------------------------------------------------------------------------------- /packages/partywhen/tests/index.ts: -------------------------------------------------------------------------------- 1 | import type { Env } from "./env"; 2 | 3 | export { Scheduler } from "../src/index"; 4 | 5 | export default { 6 | async fetch(request: Request, env: Env, _ctx: ExecutionContext) { 7 | const id = env.SCHEDULER.idFromName("example"); 8 | const stub = env.SCHEDULER.get(id); 9 | return stub.fetch(request); 10 | } 11 | } satisfies ExportedHandler; 12 | -------------------------------------------------------------------------------- /packages/partywhen/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | 4 | "compilerOptions": { 5 | "types": [ 6 | "@cloudflare/workers-types/experimental", 7 | "@cloudflare/vitest-pool-workers" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/partywhen/tests/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" } 8 | } 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /packages/partywhen/tests/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "durable-scheduler-tests" 2 | compatibility_date = "2024-11-06" 3 | main = "./index.ts" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | [[durable_objects.bindings]] 7 | name = "SCHEDULER" 8 | class_name = "Scheduler" 9 | 10 | [[migrations]] 11 | tag = "v1" 12 | new_sqlite_classes = [ "Scheduler" ] 13 | 14 | -------------------------------------------------------------------------------- /packages/y-partyserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "y-partyserver", 3 | "version": "0.0.44", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/cloudflare/partykit.git" 7 | }, 8 | "homepage": "https://github.com/cloudflare/partykit/tree/main/packages/y-partyserver", 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/server/index.d.ts", 13 | "require": "./dist/server/index.js", 14 | "import": "./dist/server/index.js" 15 | }, 16 | "./provider": { 17 | "types": "./dist/provider/index.d.ts", 18 | "require": "./dist/provider/index.js", 19 | "import": "./dist/provider/index.js" 20 | }, 21 | "./react": { 22 | "types": "./dist/provider/react.d.ts", 23 | "require": "./dist/provider/react.js", 24 | "import": "./dist/provider/react.js" 25 | } 26 | }, 27 | "scripts": { 28 | "build": "tsx scripts/build.ts" 29 | }, 30 | "files": [ 31 | "dist", 32 | "README.md" 33 | ], 34 | "keywords": [ 35 | "yjs", 36 | "collaboration", 37 | "text-editors" 38 | ], 39 | "author": "Sunil Pai ", 40 | "license": "ISC", 41 | "description": "", 42 | "dependencies": { 43 | "lib0": "^0.2.101", 44 | "lodash.debounce": "^4.0.8", 45 | "nanoid": "^5.1.5", 46 | "y-protocols": "^1.0.6" 47 | }, 48 | "peerDependencies": { 49 | "@cloudflare/workers-types": "^4.20240729.0", 50 | "partyserver": "^0.0.71", 51 | "yjs": "^13.6.14" 52 | }, 53 | "devDependencies": { 54 | "@cloudflare/workers-types": "^4.20250327.0", 55 | "@types/lodash.debounce": "^4.0.9", 56 | "partyserver": "^0.0.71", 57 | "ws": "^8.18.1", 58 | "yjs": "^13.6.24" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/y-partyserver/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import { build } from "tsup"; 3 | 4 | await build({ 5 | entry: [ 6 | "src/server/index.ts", 7 | "src/provider/index.ts", 8 | "src/provider/react.tsx" 9 | ], 10 | splitting: true, 11 | sourcemap: true, 12 | clean: true, 13 | external: ["cloudflare:workers", "partyserver", "react"], 14 | format: "esm", 15 | dts: true 16 | }); 17 | 18 | // then run prettier on the generated .d.ts files 19 | execSync("prettier --write ./dist/**/*.d.ts"); 20 | 21 | process.exit(0); 22 | -------------------------------------------------------------------------------- /packages/y-partyserver/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/y-partyserver/src/provider/react.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import YProvider from "./index"; 4 | 5 | import type * as Y from "yjs"; 6 | 7 | type UseYProviderOptions = { 8 | host?: string | undefined; 9 | room: string; 10 | party?: string; 11 | doc?: Y.Doc; 12 | prefix?: string; 13 | options?: ConstructorParameters[3]; 14 | }; 15 | 16 | export default function useYProvider(yProviderOptions: UseYProviderOptions) { 17 | const { host, room, party, doc, options, prefix } = yProviderOptions; 18 | const [provider] = useState( 19 | () => 20 | new YProvider( 21 | host || 22 | (typeof window !== "undefined" 23 | ? window.location.host 24 | : "dummy-domain.com"), 25 | room, 26 | doc, 27 | { 28 | connect: false, 29 | party, 30 | prefix, 31 | ...options 32 | } 33 | ) 34 | ); 35 | 36 | useEffect(() => { 37 | void provider.connect(); 38 | return () => provider.disconnect(); 39 | }, [provider]); 40 | return provider; 41 | } 42 | -------------------------------------------------------------------------------- /packages/y-partyserver/src/provider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/y-partyserver/src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/y-partyserver/src/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /random/check-export-types.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | function checkIfExportMatchesType(fileName: string, typeName: string): void { 4 | // Create a TypeScript program with the file 5 | const program = ts.createProgram([fileName], {}); 6 | const checker = program.getTypeChecker(); 7 | 8 | // Get the source file 9 | const sourceFile = program.getSourceFile(fileName); 10 | if (!sourceFile) { 11 | throw new Error(`Source file ${fileName} not found`); 12 | } 13 | 14 | // Traverse the AST to find export declarations 15 | ts.forEachChild(sourceFile, (node) => { 16 | if (ts.isExportDeclaration(node) || ts.isExportAssignment(node)) { 17 | // Handle export assignments (e.g., export default) 18 | if (ts.isExportAssignment(node)) { 19 | const expr = node.expression; 20 | if (expr) { 21 | checkNodeType(expr); 22 | } 23 | } else if (node.exportClause && ts.isNamedExports(node.exportClause)) { 24 | // Handle named exports 25 | for (const element of node.exportClause.elements) { 26 | const symbol = checker.getSymbolAtLocation(element.name); 27 | if (symbol) { 28 | const declaredType = checker.getTypeOfSymbolAtLocation( 29 | symbol, 30 | symbol.valueDeclaration! 31 | ); 32 | const matchingType = checker.getTypeFromTypeNode( 33 | createTypeReference(typeName) 34 | ); 35 | if (checker.isTypeAssignableTo(declaredType, matchingType)) { 36 | console.log( 37 | `Export ${element.name.getText()} matches the type ${typeName}` 38 | ); 39 | } 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | 46 | function checkNodeType(node: ts.Node): void { 47 | const type = checker.getTypeAtLocation(node); 48 | const matchingType = checker.getTypeFromTypeNode( 49 | createTypeReference(typeName) 50 | ); 51 | if (checker.isTypeAssignableTo(type, matchingType)) { 52 | console.log(`Export matches the type ${typeName}`); 53 | } 54 | } 55 | 56 | function createTypeReference(name: string): ts.TypeNode { 57 | return ts.factory.createTypeReferenceNode(name, []); 58 | } 59 | } 60 | 61 | // Example usage: 62 | const fileName = "src/server/index.ts"; // Replace with your file 63 | const typeName = "DurableObject"; // Replace with the type you want to match 64 | 65 | checkIfExportMatchesType(fileName, typeName); 66 | -------------------------------------------------------------------------------- /random/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/typecheck.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import fg from "fast-glob"; 3 | 4 | const tsconfigs: string[] = []; 5 | 6 | for await (const file of await fg.glob("**/tsconfig.json")) { 7 | if (file.includes("node_modules")) continue; 8 | tsconfigs.push(file); 9 | } 10 | 11 | console.log(`Typechecking ${tsconfigs.length} projects...`); 12 | 13 | const failed = ( 14 | await Promise.allSettled( 15 | tsconfigs.map((tsconfig) => execSync(`tsc -p ${tsconfig}`)) 16 | ) 17 | ).filter((r) => r.status === "rejected"); 18 | 19 | if (failed.length > 0) { 20 | console.error("Some projects failed to typecheck!"); 21 | process.exit(1); 22 | } 23 | 24 | console.log("All projects typecheck successfully!"); 25 | --------------------------------------------------------------------------------