durable-actors 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +206 -0
- package/dist/actor/actor.d.ts +66 -0
- package/dist/actor/actor.d.ts.map +1 -0
- package/dist/actor/actor.js +144 -0
- package/dist/actor/actor.js.map +1 -0
- package/dist/actor/decorators.d.ts +6 -0
- package/dist/actor/decorators.d.ts.map +1 -0
- package/dist/actor/decorators.js +24 -0
- package/dist/actor/decorators.js.map +1 -0
- package/dist/actor/identity.d.ts +16 -0
- package/dist/actor/identity.d.ts.map +1 -0
- package/dist/actor/identity.js +29 -0
- package/dist/actor/identity.js.map +1 -0
- package/dist/actor/invocationContext.d.ts +4 -0
- package/dist/actor/invocationContext.d.ts.map +1 -0
- package/dist/actor/invocationContext.js +10 -0
- package/dist/actor/invocationContext.js.map +1 -0
- package/dist/actor/schema.d.ts +22 -0
- package/dist/actor/schema.d.ts.map +1 -0
- package/dist/actor/schema.js +8 -0
- package/dist/actor/schema.js.map +1 -0
- package/dist/actor/socket.d.ts +88 -0
- package/dist/actor/socket.d.ts.map +1 -0
- package/dist/actor/socket.js +216 -0
- package/dist/actor/socket.js.map +1 -0
- package/dist/actor/socketProtocol.d.ts +85 -0
- package/dist/actor/socketProtocol.d.ts.map +1 -0
- package/dist/actor/socketProtocol.js +95 -0
- package/dist/actor/socketProtocol.js.map +1 -0
- package/dist/actor/socketValidation.d.ts +29 -0
- package/dist/actor/socketValidation.d.ts.map +1 -0
- package/dist/actor/socketValidation.js +73 -0
- package/dist/actor/socketValidation.js.map +1 -0
- package/dist/backend.d.ts +13 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +45 -0
- package/dist/backend.js.map +1 -0
- package/dist/cli/actor-source-watcher.d.ts +11 -0
- package/dist/cli/actor-source-watcher.d.ts.map +1 -0
- package/dist/cli/actor-source-watcher.js +55 -0
- package/dist/cli/actor-source-watcher.js.map +1 -0
- package/dist/cli/connection.d.ts +14 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +16 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/control-plane.d.ts +34 -0
- package/dist/cli/control-plane.d.ts.map +1 -0
- package/dist/cli/control-plane.js +99 -0
- package/dist/cli/control-plane.js.map +1 -0
- package/dist/cli/deploy.d.ts +4 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +39 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/dev.d.ts +4 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +121 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/generate.d.ts +4 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +59 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/objects.d.ts +4 -0
- package/dist/cli/objects.d.ts.map +1 -0
- package/dist/cli/objects.js +68 -0
- package/dist/cli/objects.js.map +1 -0
- package/dist/cli/observe.d.ts +23 -0
- package/dist/cli/observe.d.ts.map +1 -0
- package/dist/cli/observe.js +245 -0
- package/dist/cli/observe.js.map +1 -0
- package/dist/cli/rust-runtime.d.ts +10 -0
- package/dist/cli/rust-runtime.d.ts.map +1 -0
- package/dist/cli/rust-runtime.js +62 -0
- package/dist/cli/rust-runtime.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +131 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/actorHostGrpc.d.ts +50 -0
- package/dist/client/actorHostGrpc.d.ts.map +1 -0
- package/dist/client/actorHostGrpc.js +111 -0
- package/dist/client/actorHostGrpc.js.map +1 -0
- package/dist/client/client.d.ts +12 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +20 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/clientSettings.d.ts +8 -0
- package/dist/client/clientSettings.d.ts.map +1 -0
- package/dist/client/clientSettings.js +45 -0
- package/dist/client/clientSettings.js.map +1 -0
- package/dist/client/remoteClient.d.ts +49 -0
- package/dist/client/remoteClient.d.ts.map +1 -0
- package/dist/client/remoteClient.js +286 -0
- package/dist/client/remoteClient.js.map +1 -0
- package/dist/client/socketConnection.d.ts +18 -0
- package/dist/client/socketConnection.d.ts.map +1 -0
- package/dist/client/socketConnection.js +48 -0
- package/dist/client/socketConnection.js.map +1 -0
- package/dist/client/telemetry.d.ts +14 -0
- package/dist/client/telemetry.d.ts.map +1 -0
- package/dist/client/telemetry.js +23 -0
- package/dist/client/telemetry.js.map +1 -0
- package/dist/compiler/actor-build.d.ts +4 -0
- package/dist/compiler/actor-build.d.ts.map +1 -0
- package/dist/compiler/actor-build.js +36 -0
- package/dist/compiler/actor-build.js.map +1 -0
- package/dist/compiler/actor-compiler.d.ts +30 -0
- package/dist/compiler/actor-compiler.d.ts.map +1 -0
- package/dist/compiler/actor-compiler.js +300 -0
- package/dist/compiler/actor-compiler.js.map +1 -0
- package/dist/compiler/deployment-build.d.ts +2 -0
- package/dist/compiler/deployment-build.d.ts.map +1 -0
- package/dist/compiler/deployment-build.js +23 -0
- package/dist/compiler/deployment-build.js.map +1 -0
- package/dist/compiler/features/persistence.d.ts +11 -0
- package/dist/compiler/features/persistence.d.ts.map +1 -0
- package/dist/compiler/features/persistence.js +102 -0
- package/dist/compiler/features/persistence.js.map +1 -0
- package/dist/compiler/features/reentrancy.d.ts +9 -0
- package/dist/compiler/features/reentrancy.d.ts.map +1 -0
- package/dist/compiler/features/reentrancy.js +44 -0
- package/dist/compiler/features/reentrancy.js.map +1 -0
- package/dist/compiler/generators/backend-generator.d.ts +5 -0
- package/dist/compiler/generators/backend-generator.d.ts.map +1 -0
- package/dist/compiler/generators/backend-generator.js +181 -0
- package/dist/compiler/generators/backend-generator.js.map +1 -0
- package/dist/compiler/generators/client-generator.d.ts +5 -0
- package/dist/compiler/generators/client-generator.d.ts.map +1 -0
- package/dist/compiler/generators/client-generator.js +28 -0
- package/dist/compiler/generators/client-generator.js.map +1 -0
- package/dist/compiler/generators/typescript-generator.d.ts +8 -0
- package/dist/compiler/generators/typescript-generator.d.ts.map +1 -0
- package/dist/compiler/generators/typescript-generator.js +154 -0
- package/dist/compiler/generators/typescript-generator.js.map +1 -0
- package/dist/compiler/generators/usage-comment.d.ts +3 -0
- package/dist/compiler/generators/usage-comment.d.ts.map +1 -0
- package/dist/compiler/generators/usage-comment.js +6 -0
- package/dist/compiler/generators/usage-comment.js.map +1 -0
- package/dist/compiler/json-schema.d.ts +7 -0
- package/dist/compiler/json-schema.d.ts.map +1 -0
- package/dist/compiler/json-schema.js +122 -0
- package/dist/compiler/json-schema.js.map +1 -0
- package/dist/compiler/public-schema.d.ts +4 -0
- package/dist/compiler/public-schema.d.ts.map +1 -0
- package/dist/compiler/public-schema.js +46 -0
- package/dist/compiler/public-schema.js.map +1 -0
- package/dist/compiler/rpc-contract.d.ts +5 -0
- package/dist/compiler/rpc-contract.d.ts.map +1 -0
- package/dist/compiler/rpc-contract.js +95 -0
- package/dist/compiler/rpc-contract.js.map +1 -0
- package/dist/compiler/socket-contract.d.ts +6 -0
- package/dist/compiler/socket-contract.d.ts.map +1 -0
- package/dist/compiler/socket-contract.js +36 -0
- package/dist/compiler/socket-contract.js.map +1 -0
- package/dist/compiler/types.d.ts +51 -0
- package/dist/compiler/types.d.ts.map +1 -0
- package/dist/compiler/types.js +9 -0
- package/dist/compiler/types.js.map +1 -0
- package/dist/compiler/validate-public-contract.d.ts +4 -0
- package/dist/compiler/validate-public-contract.d.ts.map +1 -0
- package/dist/compiler/validate-public-contract.js +155 -0
- package/dist/compiler/validate-public-contract.js.map +1 -0
- package/dist/environment.d.ts +2 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +18 -0
- package/dist/environment.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +52 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/durable_object/v1/ActivateActorReply.d.ts +8 -0
- package/dist/generated/durable_object/v1/ActivateActorReply.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActivateActorReply.js +3 -0
- package/dist/generated/durable_object/v1/ActivateActorReply.js.map +1 -0
- package/dist/generated/durable_object/v1/ActivateActorRequest.d.ts +8 -0
- package/dist/generated/durable_object/v1/ActivateActorRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActivateActorRequest.js +3 -0
- package/dist/generated/durable_object/v1/ActivateActorRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/ActorCompleted.d.ts +9 -0
- package/dist/generated/durable_object/v1/ActorCompleted.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActorCompleted.js +3 -0
- package/dist/generated/durable_object/v1/ActorCompleted.js.map +1 -0
- package/dist/generated/durable_object/v1/ActorControlPlaneService.d.ts +21 -0
- package/dist/generated/durable_object/v1/ActorControlPlaneService.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActorControlPlaneService.js +3 -0
- package/dist/generated/durable_object/v1/ActorControlPlaneService.js.map +1 -0
- package/dist/generated/durable_object/v1/ActorFailed.d.ts +9 -0
- package/dist/generated/durable_object/v1/ActorFailed.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActorFailed.js +3 -0
- package/dist/generated/durable_object/v1/ActorFailed.js.map +1 -0
- package/dist/generated/durable_object/v1/ActorHostService.d.ts +56 -0
- package/dist/generated/durable_object/v1/ActorHostService.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActorHostService.js +3 -0
- package/dist/generated/durable_object/v1/ActorHostService.js.map +1 -0
- package/dist/generated/durable_object/v1/ActorKey.d.ts +11 -0
- package/dist/generated/durable_object/v1/ActorKey.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ActorKey.js +3 -0
- package/dist/generated/durable_object/v1/ActorKey.js.map +1 -0
- package/dist/generated/durable_object/v1/ControlPlaneReply.d.ts +7 -0
- package/dist/generated/durable_object/v1/ControlPlaneReply.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ControlPlaneReply.js +3 -0
- package/dist/generated/durable_object/v1/ControlPlaneReply.js.map +1 -0
- package/dist/generated/durable_object/v1/ControlPlaneRequest.d.ts +7 -0
- package/dist/generated/durable_object/v1/ControlPlaneRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ControlPlaneRequest.js +3 -0
- package/dist/generated/durable_object/v1/ControlPlaneRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/Empty.d.ts +5 -0
- package/dist/generated/durable_object/v1/Empty.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/Empty.js +3 -0
- package/dist/generated/durable_object/v1/Empty.js.map +1 -0
- package/dist/generated/durable_object/v1/HostInvokeActorRequest.d.ts +11 -0
- package/dist/generated/durable_object/v1/HostInvokeActorRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/HostInvokeActorRequest.js +3 -0
- package/dist/generated/durable_object/v1/HostInvokeActorRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/HostSocketEventRequest.d.ts +17 -0
- package/dist/generated/durable_object/v1/HostSocketEventRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/HostSocketEventRequest.js +3 -0
- package/dist/generated/durable_object/v1/HostSocketEventRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/InvokeActorReply.d.ts +16 -0
- package/dist/generated/durable_object/v1/InvokeActorReply.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/InvokeActorReply.js +3 -0
- package/dist/generated/durable_object/v1/InvokeActorReply.js.map +1 -0
- package/dist/generated/durable_object/v1/InvokeActorRequest.d.ts +14 -0
- package/dist/generated/durable_object/v1/InvokeActorRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/InvokeActorRequest.js +3 -0
- package/dist/generated/durable_object/v1/InvokeActorRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/PublishSocketEffectsRequest.d.ts +13 -0
- package/dist/generated/durable_object/v1/PublishSocketEffectsRequest.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/PublishSocketEffectsRequest.js +3 -0
- package/dist/generated/durable_object/v1/PublishSocketEffectsRequest.js.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaService.d.ts +42 -0
- package/dist/generated/durable_object/v1/ReplicaService.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaService.js +3 -0
- package/dist/generated/durable_object/v1/ReplicaService.js.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaSessionHead.d.ts +14 -0
- package/dist/generated/durable_object/v1/ReplicaSessionHead.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaSessionHead.js +3 -0
- package/dist/generated/durable_object/v1/ReplicaSessionHead.js.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaSnapshotRef.d.ts +14 -0
- package/dist/generated/durable_object/v1/ReplicaSnapshotRef.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaSnapshotRef.js +3 -0
- package/dist/generated/durable_object/v1/ReplicaSnapshotRef.js.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaStreamHead.d.ts +11 -0
- package/dist/generated/durable_object/v1/ReplicaStreamHead.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaStreamHead.js +3 -0
- package/dist/generated/durable_object/v1/ReplicaStreamHead.js.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaStreamIdentity.d.ts +14 -0
- package/dist/generated/durable_object/v1/ReplicaStreamIdentity.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/ReplicaStreamIdentity.js +3 -0
- package/dist/generated/durable_object/v1/ReplicaStreamIdentity.js.map +1 -0
- package/dist/generated/durable_object/v1/Reroute.d.ts +5 -0
- package/dist/generated/durable_object/v1/Reroute.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/Reroute.js +3 -0
- package/dist/generated/durable_object/v1/Reroute.js.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotData.d.ts +7 -0
- package/dist/generated/durable_object/v1/SnapshotData.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotData.js +3 -0
- package/dist/generated/durable_object/v1/SnapshotData.js.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotService.d.ts +32 -0
- package/dist/generated/durable_object/v1/SnapshotService.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotService.js +3 -0
- package/dist/generated/durable_object/v1/SnapshotService.js.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotWriteReply.d.ts +7 -0
- package/dist/generated/durable_object/v1/SnapshotWriteReply.d.ts.map +1 -0
- package/dist/generated/durable_object/v1/SnapshotWriteReply.js +3 -0
- package/dist/generated/durable_object/v1/SnapshotWriteReply.js.map +1 -0
- package/dist/generated/durable_object.d.ts +69 -0
- package/dist/generated/durable_object.d.ts.map +1 -0
- package/dist/generated/durable_object.js +2 -0
- package/dist/generated/durable_object.js.map +1 -0
- package/dist/generated/durable_object.proto +117 -0
- package/dist/generated.browser.d.ts +6 -0
- package/dist/generated.browser.d.ts.map +1 -0
- package/dist/generated.browser.js +10 -0
- package/dist/generated.browser.js.map +1 -0
- package/dist/generated.d.ts +3 -0
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +3 -0
- package/dist/generated.js.map +1 -0
- package/dist/host/actor-host.d.ts +22 -0
- package/dist/host/actor-host.d.ts.map +1 -0
- package/dist/host/actor-host.js +392 -0
- package/dist/host/actor-host.js.map +1 -0
- package/dist/host/actor-runtime.d.ts +34 -0
- package/dist/host/actor-runtime.d.ts.map +1 -0
- package/dist/host/actor-runtime.js +279 -0
- package/dist/host/actor-runtime.js.map +1 -0
- package/dist/host/actor-worker.d.ts +2 -0
- package/dist/host/actor-worker.d.ts.map +1 -0
- package/dist/host/actor-worker.js +154 -0
- package/dist/host/actor-worker.js.map +1 -0
- package/dist/host/generic-host.d.ts +3 -0
- package/dist/host/generic-host.d.ts.map +1 -0
- package/dist/host/generic-host.js +87 -0
- package/dist/host/generic-host.js.map +1 -0
- package/dist/host/protocol.d.ts +347 -0
- package/dist/host/protocol.d.ts.map +1 -0
- package/dist/host/protocol.js +76 -0
- package/dist/host/protocol.js.map +1 -0
- package/dist/host/types.d.ts +42 -0
- package/dist/host/types.d.ts.map +1 -0
- package/dist/host/types.js +2 -0
- package/dist/host/types.js.map +1 -0
- package/dist/host/worker-supervisor.d.ts +85 -0
- package/dist/host/worker-supervisor.d.ts.map +1 -0
- package/dist/host/worker-supervisor.js +423 -0
- package/dist/host/worker-supervisor.js.map +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +3 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +13 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +34 -0
- package/dist/json.js.map +1 -0
- package/dist/localRuntime.d.ts +27 -0
- package/dist/localRuntime.d.ts.map +1 -0
- package/dist/localRuntime.js +112 -0
- package/dist/localRuntime.js.map +1 -0
- package/dist/proxy.d.ts +43 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +76 -0
- package/dist/proxy.js.map +1 -0
- package/dist/runtimeInstaller.d.ts +16 -0
- package/dist/runtimeInstaller.d.ts.map +1 -0
- package/dist/runtimeInstaller.js +95 -0
- package/dist/runtimeInstaller.js.map +1 -0
- package/dist/templates/actor/LICENSE.md +9 -0
- package/dist/templates/actor/README.md +33 -0
- package/dist/templates/actor/gitignore +7 -0
- package/dist/templates/actor/package.json +20 -0
- package/dist/templates/actor/src/durable-objects.ts +13 -0
- package/dist/templates/actor/tsconfig.json +11 -0
- package/dist/templates/ai-chat/.env.example +1 -0
- package/dist/templates/ai-chat/LICENSE.md +9 -0
- package/dist/templates/ai-chat/README.md +41 -0
- package/dist/templates/ai-chat/gitignore +8 -0
- package/dist/templates/ai-chat/index.html +12 -0
- package/dist/templates/ai-chat/package.json +30 -0
- package/dist/templates/ai-chat/src/Chat.tsx +37 -0
- package/dist/templates/ai-chat/src/backend.ts +41 -0
- package/dist/templates/ai-chat/src/durable-objects.ts +15 -0
- package/dist/templates/ai-chat/tsconfig.json +13 -0
- package/dist/templates/chat/LICENSE.md +9 -0
- package/dist/templates/chat/README.md +29 -0
- package/dist/templates/chat/gitignore +8 -0
- package/dist/templates/chat/index.html +12 -0
- package/dist/templates/chat/package.json +27 -0
- package/dist/templates/chat/src/Chat.tsx +63 -0
- package/dist/templates/chat/src/backend.ts +20 -0
- package/dist/templates/chat/src/durable-objects.ts +18 -0
- package/dist/templates/chat/tsconfig.json +13 -0
- package/dist/templates/documents/LICENSE.md +9 -0
- package/dist/templates/documents/README.md +38 -0
- package/dist/templates/documents/gitignore +8 -0
- package/dist/templates/documents/index.html +12 -0
- package/dist/templates/documents/package.json +35 -0
- package/dist/templates/documents/src/App.tsx +81 -0
- package/dist/templates/documents/src/Editor.tsx +69 -0
- package/dist/templates/documents/src/backend.ts +18 -0
- package/dist/templates/documents/src/collaboration.ts +46 -0
- package/dist/templates/documents/src/durable-objects.ts +48 -0
- package/dist/templates/documents/src/style.css +145 -0
- package/dist/templates/documents/tsconfig.json +13 -0
- package/dist/wire/contract.d.ts +9 -0
- package/dist/wire/contract.d.ts.map +1 -0
- package/dist/wire/contract.js +2 -0
- package/dist/wire/contract.js.map +1 -0
- package/dist/wire/public-contract.d.ts +36 -0
- package/dist/wire/public-contract.d.ts.map +1 -0
- package/dist/wire/public-contract.js +2 -0
- package/dist/wire/public-contract.js.map +1 -0
- package/package.json +111 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { openai } from "@ai-sdk/openai"
|
|
2
|
+
import { convertToModelMessages, generateId, pipeUIMessageStreamToResponse, streamText, toUIMessageStream, validateUIMessages } from "ai"
|
|
3
|
+
import express from "express"
|
|
4
|
+
import { createServer } from "vite"
|
|
5
|
+
|
|
6
|
+
import { ChatHistory } from "./durable-objects.js"
|
|
7
|
+
|
|
8
|
+
const app = express()
|
|
9
|
+
app.use(express.json())
|
|
10
|
+
|
|
11
|
+
app.get("/api/chat/:id", async (request, response) => {
|
|
12
|
+
response.json(await ChatHistory.get(request.params.id).load())
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
app.post("/api/chat", async (request, response) => {
|
|
16
|
+
const [message] = await validateUIMessages({ messages: [request.body.messages.at(-1)] })
|
|
17
|
+
if (message.role !== "user") return response.sendStatus(400)
|
|
18
|
+
const chat = ChatHistory.get(request.body.id)
|
|
19
|
+
const messages = await chat.append(message)
|
|
20
|
+
const result = streamText({
|
|
21
|
+
model: openai("gpt-5-mini"),
|
|
22
|
+
messages: await convertToModelMessages(messages)
|
|
23
|
+
})
|
|
24
|
+
await pipeUIMessageStreamToResponse({
|
|
25
|
+
response,
|
|
26
|
+
stream: toUIMessageStream({
|
|
27
|
+
stream: result.stream,
|
|
28
|
+
originalMessages: messages,
|
|
29
|
+
generateMessageId: generateId,
|
|
30
|
+
onEnd: async ({ responseMessage, outcome }) => {
|
|
31
|
+
if (outcome.status === "completed") await chat.append(responseMessage)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const vite = await createServer({
|
|
38
|
+
server: { middlewareMode: true, fs: { deny: [".env", ".env.*", "**/.durable-actors/**", "**/.little-actors/**", "**/.git/**"] } }
|
|
39
|
+
})
|
|
40
|
+
app.use(vite.middlewares)
|
|
41
|
+
app.listen(3000, "127.0.0.1", () => console.log("AI chat: http://127.0.0.1:3000"))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UIMessage } from "ai"
|
|
2
|
+
import { Actor, Persisted } from "durable-actors"
|
|
3
|
+
|
|
4
|
+
export class ChatHistory extends Actor {
|
|
5
|
+
@Persisted private messages: UIMessage[] = []
|
|
6
|
+
|
|
7
|
+
async load() {
|
|
8
|
+
return this.messages
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async append(message: UIMessage) {
|
|
12
|
+
this.messages.push(message)
|
|
13
|
+
return this.messages
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terse
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Express + React chat
|
|
2
|
+
|
|
3
|
+
Requires Node.js 20+ and Bun 1.4.2+ on your PATH; Bun executes the actors.
|
|
4
|
+
|
|
5
|
+
Create this sample with `npx durable-actors init chat-example --template chat`.
|
|
6
|
+
|
|
7
|
+
## Run it
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
npx durable-actors generate
|
|
12
|
+
npx durable-actors dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Wait for `Local actors ready`. In another terminal, run the printed export command, then:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open [the chat](http://127.0.0.1:3000) in two tabs. Send a message, then reload to see the saved history.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
Everyone is a guest in this demo. In your app, authenticate the request before `actors.ChatRoom.prepareWebsocket` and derive metadata from the signed-in user.
|
|
26
|
+
|
|
27
|
+
The frontend fetches a signed URL from the backend and passes it to `new WebSocket()`. The actor sends history explicitly on connection and after each message. Reload to reconnect if the socket closes.
|
|
28
|
+
|
|
29
|
+
After changing the actor's types, rerun `npx durable-actors generate` and restart the actor server.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Chat · Durable Actors</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/Chat.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "durable-actors-example-chat",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.19.0"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsx src/backend.ts",
|
|
10
|
+
"build": "tsc --noEmit && vite build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "^5.2.1",
|
|
14
|
+
"durable-actors": "0.2.4",
|
|
15
|
+
"react": "^19.3.0",
|
|
16
|
+
"react-dom": "^19.3.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/express": "^5.0.0",
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"@types/react": "^19.0.0",
|
|
22
|
+
"@types/react-dom": "^19.0.0",
|
|
23
|
+
"tsx": "^4.0.0",
|
|
24
|
+
"typescript": "~5.9.3",
|
|
25
|
+
"vite": "^8.3.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react"
|
|
2
|
+
import { createRoot } from "react-dom/client"
|
|
3
|
+
|
|
4
|
+
function Chat() {
|
|
5
|
+
const room = useRef<WebSocket | undefined>(undefined)
|
|
6
|
+
const [history, setHistory] = useState<{ name: string; text: string }[]>([])
|
|
7
|
+
const [connected, setConnected] = useState(false)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let disposed = false
|
|
11
|
+
async function open() {
|
|
12
|
+
const response = await fetch("/api/socket/ChatRoom/lobby", { method: "POST" })
|
|
13
|
+
if (!response.ok) throw new Error(`Connection denied (${response.status})`)
|
|
14
|
+
const { websocketUrl } = await response.json()
|
|
15
|
+
if (disposed) return
|
|
16
|
+
const socket = new WebSocket(websocketUrl)
|
|
17
|
+
room.current = socket
|
|
18
|
+
socket.onopen = () => {
|
|
19
|
+
if (!disposed) setConnected(true)
|
|
20
|
+
}
|
|
21
|
+
socket.onmessage = event => {
|
|
22
|
+
if (!disposed) setHistory(JSON.parse(event.data))
|
|
23
|
+
}
|
|
24
|
+
socket.onclose = () => {
|
|
25
|
+
if (!disposed) setConnected(false)
|
|
26
|
+
}
|
|
27
|
+
socket.onerror = () => {
|
|
28
|
+
if (!disposed) setConnected(false)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
void open().catch(console.error)
|
|
32
|
+
return () => {
|
|
33
|
+
disposed = true
|
|
34
|
+
room.current?.close()
|
|
35
|
+
}
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
function send(form: FormData) {
|
|
39
|
+
if (room.current?.readyState === WebSocket.OPEN) room.current.send(JSON.stringify(String(form.get("message"))))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<main>
|
|
44
|
+
<h1>The lobby</h1>
|
|
45
|
+
<ul role="log" aria-label="Messages">
|
|
46
|
+
{history.map((item, index) => (
|
|
47
|
+
<li key={index}>
|
|
48
|
+
<strong>{item.name}:</strong> {item.text}
|
|
49
|
+
</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
<form action={send}>
|
|
53
|
+
<label>
|
|
54
|
+
Message <input name="message" required disabled={!connected} />
|
|
55
|
+
</label>
|
|
56
|
+
<button disabled={!connected}>Send</button>
|
|
57
|
+
</form>
|
|
58
|
+
{!connected && <p>Connecting or disconnected. Reload to retry.</p>}
|
|
59
|
+
</main>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
createRoot(document.getElementById("root")!).render(<Chat />)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import { createServer } from "vite"
|
|
3
|
+
|
|
4
|
+
import { actors } from "../generated/index.js"
|
|
5
|
+
|
|
6
|
+
const app = express()
|
|
7
|
+
|
|
8
|
+
app.post("/api/socket/ChatRoom/:actorId", async (request, response) => {
|
|
9
|
+
const grant = await actors.ChatRoom.prepareWebsocket({
|
|
10
|
+
actorId: request.params.actorId,
|
|
11
|
+
metadata: { name: "Guest" }
|
|
12
|
+
})
|
|
13
|
+
response.set("Cache-Control", "no-store").json(grant)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const vite = await createServer({
|
|
17
|
+
server: { middlewareMode: true, fs: { deny: [".env", ".env.*", "**/.durable-actors/**", "**/.little-actors/**", "**/.git/**"] } }
|
|
18
|
+
})
|
|
19
|
+
app.use(vite.middlewares)
|
|
20
|
+
app.listen(3000, "127.0.0.1", () => console.log("Chat: http://127.0.0.1:3000"))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Actor, Persisted } from "durable-actors"
|
|
2
|
+
import type { ActorSocket } from "durable-actors"
|
|
3
|
+
|
|
4
|
+
export class ChatRoom extends Actor<Member, string, ChatMessage[]> {
|
|
5
|
+
@Persisted history: ChatMessage[] = []
|
|
6
|
+
|
|
7
|
+
async onConnect(socket: ActorSocket<Member, ChatMessage[]>) {
|
|
8
|
+
socket.send(this.history)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async onMessage(socket: ActorSocket<Member, ChatMessage[]>, text: string) {
|
|
12
|
+
this.history.push({ name: socket.metadata.name, text })
|
|
13
|
+
this.broadcast(this.history)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Member = { name: string }
|
|
18
|
+
type ChatMessage = { name: string; text: string }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terse
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Shared documents
|
|
2
|
+
|
|
3
|
+
Requires Node.js 20+ and Bun 1.4.2+ on your PATH; Bun executes the actors.
|
|
4
|
+
|
|
5
|
+
A small Tiptap editor with Yjs for concurrent edits and durable actors for saved documents.
|
|
6
|
+
|
|
7
|
+
## Run it
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx durable-actors init documents-example --template documents
|
|
11
|
+
cd documents-example
|
|
12
|
+
npm install
|
|
13
|
+
npx durable-actors generate
|
|
14
|
+
npx durable-actors dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Wait for `Local actors ready`. In another terminal, run the printed export command, then:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open [the editor](http://127.0.0.1:3000) in two tabs. Type in Welcome, format some text, and edit from both tabs. Add another document and switch between them. Reload after changes arrive in the other tab to see the saved content.
|
|
24
|
+
|
|
25
|
+
If you already have this directory, start at `npm install`. No external service is needed.
|
|
26
|
+
|
|
27
|
+
## The code
|
|
28
|
+
|
|
29
|
+
- [src/durable-objects.ts](src/durable-objects.ts): a workspace actor stores the document list; one document actor per ID merges and saves Yjs state. The actors send initial data in `onConnect` and broadcast updates explicitly.
|
|
30
|
+
- [src/backend.ts](src/backend.ts): issues WebSocket credentials using the generated `prepareWebsocket` helpers and local defaults.
|
|
31
|
+
- [src/collaboration.ts](src/collaboration.ts): connects Yjs to a native WebSocket. It sends local updates and applies remote state without echoing it.
|
|
32
|
+
- [src/App.tsx](src/App.tsx) and [src/Editor.tsx](src/Editor.tsx): document navigation and Tiptap’s editor, formatting controls, and collaborative undo/redo.
|
|
33
|
+
|
|
34
|
+
Yjs updates are encoded as base64 strings to fit the SDK’s JSON protocol. The actor saves the Yjs state, preserving the information needed to merge concurrent edits. Clients receive the complete merged document after each edit; this keeps the sample small rather than optimizing for large documents.
|
|
35
|
+
|
|
36
|
+
Editing pauses while disconnected. Reload to request a fresh connection. There is no automatic reconnect or browser storage for unsaved edits. Keep `.durable-actors/` and restart both servers to restore saved documents.
|
|
37
|
+
|
|
38
|
+
This demo has one shared workspace and no authentication. In an application, authenticate the proxy route and authorize access to the requested workspace or document. Presence cursors, document deletion, and permissions UI are left out.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Shared documents</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/App.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "durable-actors-example-documents",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.19.0"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsx src/backend.ts",
|
|
10
|
+
"build": "tsc --noEmit && vite build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@tiptap/core": "^3.31.3",
|
|
14
|
+
"@tiptap/extension-collaboration": "^3.31.3",
|
|
15
|
+
"@tiptap/pm": "^3.31.3",
|
|
16
|
+
"@tiptap/react": "^3.31.3",
|
|
17
|
+
"@tiptap/starter-kit": "^3.31.3",
|
|
18
|
+
"@tiptap/y-tiptap": "^3.0.7",
|
|
19
|
+
"express": "^5.2.1",
|
|
20
|
+
"lib0": "^0.2.117",
|
|
21
|
+
"durable-actors": "0.2.4",
|
|
22
|
+
"react": "^19.3.0",
|
|
23
|
+
"react-dom": "^19.3.0",
|
|
24
|
+
"yjs": "^13.6.32"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/express": "^5.0.0",
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
"@types/react-dom": "^19.0.0",
|
|
31
|
+
"tsx": "^4.0.0",
|
|
32
|
+
"typescript": "~5.9.3",
|
|
33
|
+
"vite": "^8.3.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react"
|
|
2
|
+
import { createRoot } from "react-dom/client"
|
|
3
|
+
|
|
4
|
+
import { DocumentEditor } from "./Editor.js"
|
|
5
|
+
import "./style.css"
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
const workspace = useRef<WebSocket | undefined>(undefined)
|
|
9
|
+
const [documents, setDocuments] = useState<{ id: string; title: string }[]>([])
|
|
10
|
+
const [selected, select] = useState("welcome")
|
|
11
|
+
const [status, setStatus] = useState("connecting")
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let disposed = false
|
|
15
|
+
async function open() {
|
|
16
|
+
const response = await fetch("/api/socket/Workspace/demo", { method: "POST" })
|
|
17
|
+
if (!response.ok) throw new Error(`Connection denied (${response.status})`)
|
|
18
|
+
const { websocketUrl } = await response.json()
|
|
19
|
+
if (disposed) return
|
|
20
|
+
const socket = new WebSocket(websocketUrl)
|
|
21
|
+
workspace.current = socket
|
|
22
|
+
socket.onopen = () => {
|
|
23
|
+
if (!disposed) setStatus("open")
|
|
24
|
+
}
|
|
25
|
+
socket.onmessage = event => {
|
|
26
|
+
if (!disposed) setDocuments(JSON.parse(event.data))
|
|
27
|
+
}
|
|
28
|
+
socket.onclose = () => {
|
|
29
|
+
if (!disposed) setStatus("closed")
|
|
30
|
+
}
|
|
31
|
+
socket.onerror = () => {
|
|
32
|
+
if (!disposed) setStatus("error")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
void open().catch(error => {
|
|
36
|
+
if (disposed) return
|
|
37
|
+
setStatus("error")
|
|
38
|
+
console.error(error)
|
|
39
|
+
})
|
|
40
|
+
return () => {
|
|
41
|
+
disposed = true
|
|
42
|
+
workspace.current?.close()
|
|
43
|
+
}
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
function create(form: FormData) {
|
|
47
|
+
const title = String(form.get("title")).trim()
|
|
48
|
+
if (!title || workspace.current?.readyState !== WebSocket.OPEN) return
|
|
49
|
+
const id = crypto.randomUUID()
|
|
50
|
+
workspace.current.send(JSON.stringify({ id, title }))
|
|
51
|
+
select(id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="app">
|
|
56
|
+
<header>
|
|
57
|
+
<h1>Documents</h1>
|
|
58
|
+
<p>Open another tab to write together.</p>
|
|
59
|
+
</header>
|
|
60
|
+
<main>
|
|
61
|
+
<aside aria-label="Document list">
|
|
62
|
+
<form action={create}>
|
|
63
|
+
<input name="title" aria-label="New document title" placeholder="Document title" required disabled={status !== "open"} />
|
|
64
|
+
<button disabled={status !== "open"}>Add document</button>
|
|
65
|
+
</form>
|
|
66
|
+
<nav aria-label="Documents">
|
|
67
|
+
{documents.map(document => (
|
|
68
|
+
<button key={document.id} aria-current={selected === document.id ? "page" : undefined} onClick={() => select(document.id)}>
|
|
69
|
+
{document.title}
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</nav>
|
|
73
|
+
{(status === "error" || status === "closed") && <p role="alert">Disconnected. Reload to retry.</p>}
|
|
74
|
+
</aside>
|
|
75
|
+
<DocumentEditor key={selected} id={selected} title={documents.find(document => document.id === selected)?.title ?? "New document"} />
|
|
76
|
+
</main>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createRoot(document.getElementById("root")!).render(<App />)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import Collaboration from "@tiptap/extension-collaboration"
|
|
4
|
+
import { EditorContent, useEditor, useEditorState } from "@tiptap/react"
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit"
|
|
6
|
+
import type { Doc } from "yjs"
|
|
7
|
+
|
|
8
|
+
import { openDocument } from "./collaboration.js"
|
|
9
|
+
import type { ConnectionStatus } from "./collaboration.js"
|
|
10
|
+
|
|
11
|
+
export function DocumentEditor({ id, title }: DocumentProps) {
|
|
12
|
+
const [document, setDocument] = useState<Doc>()
|
|
13
|
+
const [status, setStatus] = useState<ConnectionStatus>("connecting")
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const session = openDocument(id, status => {
|
|
17
|
+
setStatus(status)
|
|
18
|
+
if (status === "open") setDocument(session.document)
|
|
19
|
+
})
|
|
20
|
+
return session.close
|
|
21
|
+
}, [id])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section aria-label={title}>
|
|
25
|
+
<div className="document-heading">
|
|
26
|
+
<h2>{title}</h2>
|
|
27
|
+
<span role="status">{status === "open" ? "Connected" : status === "connecting" ? "Connecting…" : "Disconnected. Reload to retry."}</span>
|
|
28
|
+
</div>
|
|
29
|
+
{document ? <RichText document={document} connected={status === "open"} /> : <p>Loading document…</p>}
|
|
30
|
+
</section>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RichText({ document, connected }: EditorProps) {
|
|
35
|
+
const editor = useEditor({
|
|
36
|
+
extensions: [StarterKit.configure({ undoRedo: false }), Collaboration.configure({ document })],
|
|
37
|
+
editorProps: { attributes: { role: "textbox", "aria-label": "Document content", "aria-multiline": "true" } }
|
|
38
|
+
})
|
|
39
|
+
const active = useEditorState({
|
|
40
|
+
editor,
|
|
41
|
+
selector: ({ editor }) => ({ bold: editor?.isActive("bold"), italic: editor?.isActive("italic"), heading: editor?.isActive("heading"), list: editor?.isActive("bulletList") })
|
|
42
|
+
})
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
editor?.setEditable(connected)
|
|
45
|
+
}, [editor, connected])
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div className="formatting" role="group" aria-label="Formatting">
|
|
50
|
+
<button disabled={!connected} aria-pressed={active?.bold} onClick={() => editor?.chain().focus().toggleBold().run()}>
|
|
51
|
+
Bold
|
|
52
|
+
</button>
|
|
53
|
+
<button disabled={!connected} aria-pressed={active?.italic} onClick={() => editor?.chain().focus().toggleItalic().run()}>
|
|
54
|
+
Italic
|
|
55
|
+
</button>
|
|
56
|
+
<button disabled={!connected} aria-pressed={active?.heading} onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()}>
|
|
57
|
+
Heading
|
|
58
|
+
</button>
|
|
59
|
+
<button disabled={!connected} aria-pressed={active?.list} onClick={() => editor?.chain().focus().toggleBulletList().run()}>
|
|
60
|
+
List
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<EditorContent editor={editor} />
|
|
64
|
+
</>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type DocumentProps = { id: string; title: string }
|
|
69
|
+
type EditorProps = { document: Doc; connected: boolean }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import { createServer } from "vite"
|
|
3
|
+
|
|
4
|
+
import { actors } from "../generated/index.js"
|
|
5
|
+
|
|
6
|
+
const app = express()
|
|
7
|
+
|
|
8
|
+
app.post("/api/socket/:actorName/:actorId", async (request, response) => {
|
|
9
|
+
const { actorName, actorId } = request.params
|
|
10
|
+
if (actorName !== "Workspace" && actorName !== "Document") return response.sendStatus(404)
|
|
11
|
+
response.set("Cache-Control", "no-store").json(await actors[actorName].prepareWebsocket({ actorId, metadata: null }))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const vite = await createServer({
|
|
15
|
+
server: { middlewareMode: true, fs: { deny: [".env", ".env.*", "**/.durable-actors/**", "**/.little-actors/**", "**/.git/**"] } }
|
|
16
|
+
})
|
|
17
|
+
app.use(vite.middlewares)
|
|
18
|
+
app.listen(3000, "127.0.0.1", () => console.log("Documents: http://127.0.0.1:3000"))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from "lib0/buffer"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
|
|
4
|
+
export type ConnectionStatus = "connecting" | "open" | "closed" | "error"
|
|
5
|
+
|
|
6
|
+
export function openDocument(id: string, onStatus: (status: ConnectionStatus) => void) {
|
|
7
|
+
let socket: WebSocket | undefined
|
|
8
|
+
let disposed = false
|
|
9
|
+
const document = new Y.Doc()
|
|
10
|
+
const send = (update: Uint8Array, origin: unknown) => {
|
|
11
|
+
if (origin !== socket && socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(toBase64(update)))
|
|
12
|
+
}
|
|
13
|
+
document.on("update", send)
|
|
14
|
+
async function open() {
|
|
15
|
+
const response = await fetch(`/api/socket/Document/${encodeURIComponent(id)}`, { method: "POST" })
|
|
16
|
+
if (!response.ok) throw new Error(`Connection denied (${response.status})`)
|
|
17
|
+
const { websocketUrl } = await response.json()
|
|
18
|
+
if (disposed) return
|
|
19
|
+
socket = new WebSocket(websocketUrl)
|
|
20
|
+
socket.onmessage = event => {
|
|
21
|
+
if (disposed) return
|
|
22
|
+
Y.applyUpdate(document, fromBase64(JSON.parse(event.data)), socket)
|
|
23
|
+
onStatus("open")
|
|
24
|
+
}
|
|
25
|
+
socket.onclose = () => {
|
|
26
|
+
if (!disposed) onStatus("closed")
|
|
27
|
+
}
|
|
28
|
+
socket.onerror = () => {
|
|
29
|
+
if (!disposed) onStatus("error")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
void open().catch(error => {
|
|
33
|
+
if (disposed) return
|
|
34
|
+
onStatus("error")
|
|
35
|
+
console.error(error)
|
|
36
|
+
})
|
|
37
|
+
return {
|
|
38
|
+
document,
|
|
39
|
+
close() {
|
|
40
|
+
document.off("update", send)
|
|
41
|
+
disposed = true
|
|
42
|
+
socket?.close()
|
|
43
|
+
document.destroy()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from "lib0/buffer"
|
|
2
|
+
import { Actor, Persisted } from "durable-actors"
|
|
3
|
+
import type { ActorSocket } from "durable-actors"
|
|
4
|
+
import * as Y from "yjs"
|
|
5
|
+
|
|
6
|
+
export class Workspace extends Actor<null, DocumentInfo, DocumentInfo[]> {
|
|
7
|
+
@Persisted documents: DocumentInfo[] = [{ id: "welcome", title: "Welcome" }]
|
|
8
|
+
|
|
9
|
+
async onConnect(socket: ActorSocket<null, DocumentInfo[]>) {
|
|
10
|
+
socket.send(this.documents)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async onMessage(_socket: ActorSocket<null, DocumentInfo[]>, document: DocumentInfo) {
|
|
14
|
+
if (!this.documents.some(item => item.id === document.id)) this.documents.push(document)
|
|
15
|
+
this.broadcast(this.documents)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Document extends Actor<null, string, string> {
|
|
20
|
+
@Persisted content = emptyDocument()
|
|
21
|
+
|
|
22
|
+
async onConnect(socket: ActorSocket<null, string>) {
|
|
23
|
+
socket.send(this.content)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async onMessage(_socket: ActorSocket<null, string>, update: string) {
|
|
27
|
+
const document = new Y.Doc()
|
|
28
|
+
try {
|
|
29
|
+
Y.applyUpdate(document, fromBase64(this.content))
|
|
30
|
+
Y.applyUpdate(document, fromBase64(update))
|
|
31
|
+
this.content = toBase64(Y.encodeStateAsUpdate(document))
|
|
32
|
+
this.broadcast(this.content)
|
|
33
|
+
} finally {
|
|
34
|
+
document.destroy()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function emptyDocument() {
|
|
40
|
+
const document = new Y.Doc()
|
|
41
|
+
// Seed on the server so simultaneous first joins share the same paragraph.
|
|
42
|
+
document.getXmlFragment("default").push([new Y.XmlElement("paragraph")])
|
|
43
|
+
const content = toBase64(Y.encodeStateAsUpdate(document))
|
|
44
|
+
document.destroy()
|
|
45
|
+
return content
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type DocumentInfo = { id: string; title: string }
|