@livestore/sync-cf 0.4.0-dev.1 → 0.4.0-dev.10

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.
Files changed (136) hide show
  1. package/README.md +60 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cf-worker/do/durable-object.d.ts +45 -0
  4. package/dist/cf-worker/do/durable-object.d.ts.map +1 -0
  5. package/dist/cf-worker/do/durable-object.js +150 -0
  6. package/dist/cf-worker/do/durable-object.js.map +1 -0
  7. package/dist/cf-worker/do/layer.d.ts +34 -0
  8. package/dist/cf-worker/do/layer.d.ts.map +1 -0
  9. package/dist/cf-worker/do/layer.js +91 -0
  10. package/dist/cf-worker/do/layer.js.map +1 -0
  11. package/dist/cf-worker/do/pull.d.ts +6 -0
  12. package/dist/cf-worker/do/pull.d.ts.map +1 -0
  13. package/dist/cf-worker/do/pull.js +47 -0
  14. package/dist/cf-worker/do/pull.js.map +1 -0
  15. package/dist/cf-worker/do/push.d.ts +14 -0
  16. package/dist/cf-worker/do/push.d.ts.map +1 -0
  17. package/dist/cf-worker/do/push.js +131 -0
  18. package/dist/cf-worker/do/push.js.map +1 -0
  19. package/dist/cf-worker/{durable-object.d.ts → do/sqlite.d.ts} +77 -70
  20. package/dist/cf-worker/do/sqlite.d.ts.map +1 -0
  21. package/dist/cf-worker/do/sqlite.js +27 -0
  22. package/dist/cf-worker/do/sqlite.js.map +1 -0
  23. package/dist/cf-worker/do/sync-storage.d.ts +25 -0
  24. package/dist/cf-worker/do/sync-storage.d.ts.map +1 -0
  25. package/dist/cf-worker/do/sync-storage.js +190 -0
  26. package/dist/cf-worker/do/sync-storage.js.map +1 -0
  27. package/dist/cf-worker/do/transport/do-rpc-server.d.ts +9 -0
  28. package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -0
  29. package/dist/cf-worker/do/transport/do-rpc-server.js +45 -0
  30. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -0
  31. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +7 -0
  32. package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -0
  33. package/dist/cf-worker/do/transport/http-rpc-server.js +24 -0
  34. package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -0
  35. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +4 -0
  36. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -0
  37. package/dist/cf-worker/do/transport/ws-rpc-server.js +21 -0
  38. package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -0
  39. package/dist/cf-worker/mod.d.ts +4 -2
  40. package/dist/cf-worker/mod.d.ts.map +1 -1
  41. package/dist/cf-worker/mod.js +3 -2
  42. package/dist/cf-worker/mod.js.map +1 -1
  43. package/dist/cf-worker/shared.d.ts +147 -0
  44. package/dist/cf-worker/shared.d.ts.map +1 -0
  45. package/dist/cf-worker/shared.js +32 -0
  46. package/dist/cf-worker/shared.js.map +1 -0
  47. package/dist/cf-worker/worker.d.ts +45 -45
  48. package/dist/cf-worker/worker.d.ts.map +1 -1
  49. package/dist/cf-worker/worker.js +51 -39
  50. package/dist/cf-worker/worker.js.map +1 -1
  51. package/dist/client/mod.d.ts +4 -0
  52. package/dist/client/mod.d.ts.map +1 -0
  53. package/dist/client/mod.js +4 -0
  54. package/dist/client/mod.js.map +1 -0
  55. package/dist/client/transport/do-rpc-client.d.ts +40 -0
  56. package/dist/client/transport/do-rpc-client.d.ts.map +1 -0
  57. package/dist/client/transport/do-rpc-client.js +117 -0
  58. package/dist/client/transport/do-rpc-client.js.map +1 -0
  59. package/dist/client/transport/http-rpc-client.d.ts +43 -0
  60. package/dist/client/transport/http-rpc-client.d.ts.map +1 -0
  61. package/dist/client/transport/http-rpc-client.js +103 -0
  62. package/dist/client/transport/http-rpc-client.js.map +1 -0
  63. package/dist/client/transport/ws-rpc-client.d.ts +45 -0
  64. package/dist/client/transport/ws-rpc-client.d.ts.map +1 -0
  65. package/dist/client/transport/ws-rpc-client.js +108 -0
  66. package/dist/client/transport/ws-rpc-client.js.map +1 -0
  67. package/dist/common/constants.d.ts +7 -0
  68. package/dist/common/constants.d.ts.map +1 -0
  69. package/dist/common/constants.js +17 -0
  70. package/dist/common/constants.js.map +1 -0
  71. package/dist/common/do-rpc-schema.d.ts +76 -0
  72. package/dist/common/do-rpc-schema.d.ts.map +1 -0
  73. package/dist/common/do-rpc-schema.js +48 -0
  74. package/dist/common/do-rpc-schema.js.map +1 -0
  75. package/dist/common/http-rpc-schema.d.ts +58 -0
  76. package/dist/common/http-rpc-schema.d.ts.map +1 -0
  77. package/dist/common/http-rpc-schema.js +37 -0
  78. package/dist/common/http-rpc-schema.js.map +1 -0
  79. package/dist/common/mod.d.ts +8 -1
  80. package/dist/common/mod.d.ts.map +1 -1
  81. package/dist/common/mod.js +7 -1
  82. package/dist/common/mod.js.map +1 -1
  83. package/dist/common/{ws-message-types.d.ts → sync-message-types.d.ts} +119 -153
  84. package/dist/common/sync-message-types.d.ts.map +1 -0
  85. package/dist/common/sync-message-types.js +60 -0
  86. package/dist/common/sync-message-types.js.map +1 -0
  87. package/dist/common/ws-rpc-schema.d.ts +55 -0
  88. package/dist/common/ws-rpc-schema.d.ts.map +1 -0
  89. package/dist/common/ws-rpc-schema.js +32 -0
  90. package/dist/common/ws-rpc-schema.js.map +1 -0
  91. package/package.json +7 -8
  92. package/src/cf-worker/do/durable-object.ts +237 -0
  93. package/src/cf-worker/do/layer.ts +128 -0
  94. package/src/cf-worker/do/pull.ts +77 -0
  95. package/src/cf-worker/do/push.ts +205 -0
  96. package/src/cf-worker/do/sqlite.ts +28 -0
  97. package/src/cf-worker/do/sync-storage.ts +321 -0
  98. package/src/cf-worker/do/transport/do-rpc-server.ts +84 -0
  99. package/src/cf-worker/do/transport/http-rpc-server.ts +37 -0
  100. package/src/cf-worker/do/transport/ws-rpc-server.ts +34 -0
  101. package/src/cf-worker/mod.ts +4 -2
  102. package/src/cf-worker/shared.ts +112 -0
  103. package/src/cf-worker/worker.ts +91 -105
  104. package/src/client/mod.ts +3 -0
  105. package/src/client/transport/do-rpc-client.ts +191 -0
  106. package/src/client/transport/http-rpc-client.ts +225 -0
  107. package/src/client/transport/ws-rpc-client.ts +202 -0
  108. package/src/common/constants.ts +18 -0
  109. package/src/common/do-rpc-schema.ts +54 -0
  110. package/src/common/http-rpc-schema.ts +40 -0
  111. package/src/common/mod.ts +10 -1
  112. package/src/common/sync-message-types.ts +117 -0
  113. package/src/common/ws-rpc-schema.ts +36 -0
  114. package/dist/cf-worker/cf-types.d.ts +0 -2
  115. package/dist/cf-worker/cf-types.d.ts.map +0 -1
  116. package/dist/cf-worker/cf-types.js +0 -2
  117. package/dist/cf-worker/cf-types.js.map +0 -1
  118. package/dist/cf-worker/durable-object.d.ts.map +0 -1
  119. package/dist/cf-worker/durable-object.js +0 -317
  120. package/dist/cf-worker/durable-object.js.map +0 -1
  121. package/dist/common/ws-message-types.d.ts.map +0 -1
  122. package/dist/common/ws-message-types.js +0 -57
  123. package/dist/common/ws-message-types.js.map +0 -1
  124. package/dist/sync-impl/mod.d.ts +0 -2
  125. package/dist/sync-impl/mod.d.ts.map +0 -1
  126. package/dist/sync-impl/mod.js +0 -2
  127. package/dist/sync-impl/mod.js.map +0 -1
  128. package/dist/sync-impl/ws-impl.d.ts +0 -7
  129. package/dist/sync-impl/ws-impl.d.ts.map +0 -1
  130. package/dist/sync-impl/ws-impl.js +0 -175
  131. package/dist/sync-impl/ws-impl.js.map +0 -1
  132. package/src/cf-worker/cf-types.ts +0 -12
  133. package/src/cf-worker/durable-object.ts +0 -478
  134. package/src/common/ws-message-types.ts +0 -114
  135. package/src/sync-impl/mod.ts +0 -1
  136. package/src/sync-impl/ws-impl.ts +0 -274
@@ -0,0 +1,55 @@
1
+ import { InvalidPullError, InvalidPushError } from '@livestore/common';
2
+ import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect';
3
+ declare const SyncWsRpc_base: RpcGroup.RpcGroup<Rpc.Rpc<"SyncWsRpc.Pull", Schema.Struct<{
4
+ cursor: Schema.Option<Schema.Struct<{
5
+ backendId: Schema.SchemaClass<string, string, never>;
6
+ eventSequenceNumber: Schema.BrandSchema<number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">, number, never>;
7
+ }>>;
8
+ storeId: typeof Schema.String;
9
+ payload: Schema.optional<Schema.Schema<Schema.JsonValue, Schema.JsonValue, never>>;
10
+ /** Whether to keep the pull stream alive and wait for more events */
11
+ live: typeof Schema.Boolean;
12
+ }>, import("@effect/rpc/RpcSchema").Stream<Schema.Struct<{
13
+ batch: Schema.Array$<Schema.Struct<{
14
+ eventEncoded: Schema.Struct<{
15
+ name: typeof Schema.String;
16
+ args: typeof Schema.Any;
17
+ seqNum: Schema.BrandSchema<number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">, number, never>;
18
+ parentSeqNum: Schema.BrandSchema<number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">, number, never>;
19
+ clientId: typeof Schema.String;
20
+ sessionId: typeof Schema.String;
21
+ }>;
22
+ metadata: Schema.Option<Schema.Struct<{
23
+ _tag: Schema.tag<"SyncMessage.SyncMetadata">;
24
+ } & {
25
+ createdAt: typeof Schema.String;
26
+ }>>;
27
+ }>>;
28
+ pageInfo: Schema.Union<[Schema.TaggedStruct<"MoreUnknown", {}>, Schema.TaggedStruct<"MoreKnown", {
29
+ remaining: typeof Schema.Number;
30
+ }>, Schema.TaggedStruct<"NoMore", {}>]>;
31
+ backendId: Schema.SchemaClass<string, string, never>;
32
+ }>, typeof InvalidPullError>, typeof Schema.Never, never> | Rpc.Rpc<"SyncWsRpc.Push", Schema.Struct<{
33
+ batch: Schema.Array$<Schema.Struct<{
34
+ name: typeof Schema.String;
35
+ args: typeof Schema.Any;
36
+ seqNum: Schema.BrandSchema<number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">, number, never>;
37
+ parentSeqNum: Schema.BrandSchema<number & import("effect/Brand").Brand<"GlobalEventSequenceNumber">, number, never>;
38
+ clientId: typeof Schema.String;
39
+ sessionId: typeof Schema.String;
40
+ }>>;
41
+ backendId: Schema.Option<Schema.SchemaClass<string, string, never>>;
42
+ storeId: typeof Schema.String;
43
+ payload: Schema.optional<Schema.Schema<Schema.JsonValue, Schema.JsonValue, never>>;
44
+ }>, Schema.Struct<{}>, typeof InvalidPushError, never>>;
45
+ /**
46
+ * WebSocket RPC Schema for LiveStore CF Sync Provider
47
+ *
48
+ * This defines the RPC endpoints available over WebSocket transport.
49
+ * Unlike HTTP transport which uses request/response patterns for each operation,
50
+ * WebSocket transport maintains a persistent connection and uses streaming responses.
51
+ */
52
+ export declare class SyncWsRpc extends SyncWsRpc_base {
53
+ }
54
+ export {};
55
+ //# sourceMappingURL=ws-rpc-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-rpc-schema.d.ts","sourceRoot":"","sources":["../../src/common/ws-rpc-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;;;;;;;;IAezD,qEAAqE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAZ3E;;;;;;GAMG;AACH,qBAAa,SAAU,SAAQ,cAwB9B;CAAG"}
@@ -0,0 +1,32 @@
1
+ import { InvalidPullError, InvalidPushError } from '@livestore/common';
2
+ import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect';
3
+ import * as SyncMessage from "./sync-message-types.js";
4
+ /**
5
+ * WebSocket RPC Schema for LiveStore CF Sync Provider
6
+ *
7
+ * This defines the RPC endpoints available over WebSocket transport.
8
+ * Unlike HTTP transport which uses request/response patterns for each operation,
9
+ * WebSocket transport maintains a persistent connection and uses streaming responses.
10
+ */
11
+ export class SyncWsRpc extends RpcGroup.make(Rpc.make('SyncWsRpc.Pull', {
12
+ payload: Schema.Struct({
13
+ storeId: Schema.String,
14
+ payload: Schema.optional(Schema.JsonValue),
15
+ /** Whether to keep the pull stream alive and wait for more events */
16
+ live: Schema.Boolean,
17
+ ...SyncMessage.PullRequest.fields,
18
+ }),
19
+ success: SyncMessage.PullResponse,
20
+ error: InvalidPullError,
21
+ stream: true,
22
+ }), Rpc.make('SyncWsRpc.Push', {
23
+ payload: Schema.Struct({
24
+ storeId: Schema.String,
25
+ payload: Schema.optional(Schema.JsonValue),
26
+ ...SyncMessage.PushRequest.fields,
27
+ }),
28
+ success: SyncMessage.PushAck,
29
+ error: InvalidPushError,
30
+ })) {
31
+ }
32
+ //# sourceMappingURL=ws-rpc-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-rpc-schema.js","sourceRoot":"","sources":["../../src/common/ws-rpc-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAA;AAEtD;;;;;;GAMG;AACH,MAAM,OAAO,SAAU,SAAQ,QAAQ,CAAC,IAAI,CAC1C,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;QAC1C,qEAAqE;QACrE,IAAI,EAAE,MAAM,CAAC,OAAO;QACpB,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM;KAClC,CAAC;IACF,OAAO,EAAE,WAAW,CAAC,YAAY;IACjC,KAAK,EAAE,gBAAgB;IACvB,MAAM,EAAE,IAAI;CACb,CAAC,EACF,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;QAC1C,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM;KAClC,CAAC;IACF,OAAO,EAAE,WAAW,CAAC,OAAO;IAC5B,KAAK,EAAE,gBAAgB;CACxB,CAAC,CAGH;CAAG"}
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "@livestore/sync-cf",
3
- "version": "0.4.0-dev.1",
3
+ "version": "0.4.0-dev.10",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
7
- ".": "./dist/sync-impl/mod.js",
7
+ "./client": "./dist/client/mod.js",
8
8
  "./common": "./dist/common/mod.js",
9
- "./cf-worker": "./dist/cf-worker/mod.js",
10
- "./cf-worker/durable-object": "./dist/cf-worker/durable-object.js",
11
- "./cf-worker/worker": "./dist/cf-worker/worker.js"
9
+ "./cf-worker": "./dist/cf-worker/mod.js"
12
10
  },
13
11
  "dependencies": {
14
- "@cloudflare/workers-types": "4.20250807.0",
15
- "@livestore/common": "0.4.0-dev.1",
16
- "@livestore/utils": "0.4.0-dev.1"
12
+ "@cloudflare/workers-types": "4.20250923.0",
13
+ "@livestore/common": "0.4.0-dev.10",
14
+ "@livestore/utils": "0.4.0-dev.10",
15
+ "@livestore/common-cf": "0.4.0-dev.10"
17
16
  },
18
17
  "files": [
19
18
  "dist",
@@ -0,0 +1,237 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ import { DurableObject } from 'cloudflare:workers'
4
+ import { type CfTypes, setupDurableObjectWebSocketRpc } from '@livestore/common-cf'
5
+ import { CfDeclare } from '@livestore/common-cf/declare'
6
+ import {
7
+ Effect,
8
+ FetchHttpClient,
9
+ Layer,
10
+ Logger,
11
+ LogLevel,
12
+ Otlp,
13
+ RpcMessage,
14
+ Schema,
15
+ type Scope,
16
+ } from '@livestore/utils/effect'
17
+ import {
18
+ type Env,
19
+ type MakeDurableObjectClassOptions,
20
+ matchSyncRequest,
21
+ type SyncBackendRpcInterface,
22
+ WebSocketAttachmentSchema,
23
+ } from '../shared.ts'
24
+ import { DoCtx } from './layer.ts'
25
+ import { createDoRpcHandler } from './transport/do-rpc-server.ts'
26
+ import { createHttpRpcHandler } from './transport/http-rpc-server.ts'
27
+ import { makeRpcServer } from './transport/ws-rpc-server.ts'
28
+
29
+ // NOTE We need to redeclare runtime types here to avoid type conflicts with the lib.dom Response type.
30
+ // TODO get rid of those once CF fixed their type mismatch in the worker types
31
+ declare class Request extends CfDeclare.Request {}
32
+ declare class Response extends CfDeclare.Response {}
33
+ declare class WebSocketPair extends CfDeclare.WebSocketPair {}
34
+ declare class WebSocketRequestResponsePair extends CfDeclare.WebSocketRequestResponsePair {}
35
+
36
+ const DurableObjectBase = DurableObject<Env> as any as new (
37
+ state: CfTypes.DurableObjectState,
38
+ env: Env,
39
+ ) => CfTypes.DurableObject & { ctx: CfTypes.DurableObjectState; env: Env }
40
+
41
+ // Type aliases needed to avoid TS bug https://github.com/microsoft/TypeScript/issues/55021
42
+ export type DoState = CfTypes.DurableObjectState
43
+ export type DoObject<T> = CfTypes.DurableObject & T
44
+
45
+ export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) => {
46
+ new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>
47
+ }
48
+
49
+ /**
50
+ * Creates a Durable Object class for handling WebSocket-based sync.
51
+ * A sync durable object is uniquely scoped to a specific `storeId`.
52
+ *
53
+ * The sync DO supports 3 transport modes:
54
+ * - HTTP JSON-RPC
55
+ * - WebSocket
56
+ * - Durable Object RPC calls (only works in combination with `@livestore/adapter-cf`)
57
+ *
58
+ * Example:
59
+ *
60
+ * ```ts
61
+ * // In your Cloudflare Worker file
62
+ * import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
63
+ *
64
+ * export class SyncBackendDO extends makeDurableObject({
65
+ * onPush: async (message) => {
66
+ * console.log('onPush', message.batch)
67
+ * },
68
+ * onPull: async (message) => {
69
+ * console.log('onPull', message)
70
+ * },
71
+ * }) {}
72
+ * ```
73
+ *
74
+ * `wrangler.toml`
75
+ * ```toml
76
+ * [[durable_objects.bindings]]
77
+ * name = "SYNC_BACKEND_DO"
78
+ * class_name = "SyncBackendDO"
79
+
80
+ * [[migrations]]
81
+ * tag = "v1"
82
+ * new_sqlite_classes = ["SyncBackendDO"]
83
+ * ```
84
+ */
85
+ export const makeDurableObject: MakeDurableObjectClass = (options) => {
86
+ const enabledTransports = options?.enabledTransports ?? new Set(['http', 'ws', 'do-rpc'])
87
+
88
+ const Logging = Logger.consoleWithThread('SyncDo')
89
+
90
+ const Observability = options?.otel?.baseUrl
91
+ ? Otlp.layer({
92
+ baseUrl: options.otel.baseUrl,
93
+ tracerExportInterval: 50,
94
+ resource: {
95
+ serviceName: options.otel.serviceName ?? 'sync-cf-do',
96
+ },
97
+ }).pipe(Layer.provide(FetchHttpClient.layer))
98
+ : Layer.empty
99
+
100
+ return class SyncBackendDOBase extends DurableObjectBase implements SyncBackendRpcInterface {
101
+ __DURABLE_OBJECT_BRAND = 'SyncBackendDOBase' as never
102
+
103
+ constructor(ctx: CfTypes.DurableObjectState, env: Env) {
104
+ super(ctx, env)
105
+
106
+ const WebSocketRpcServerLive = makeRpcServer({ doSelf: this, doOptions: options })
107
+
108
+ // This registers the `webSocketMessage` and `webSocketClose` handlers
109
+ if (enabledTransports.has('ws')) {
110
+ setupDurableObjectWebSocketRpc({
111
+ doSelf: this,
112
+ rpcLayer: WebSocketRpcServerLive,
113
+ webSocketMode: 'hibernate',
114
+ // See `pull.ts` for more details how `pull` Effect RPC requests streams are handled
115
+ // in combination with DO hibernation
116
+ onMessage: (request, ws) => {
117
+ if (request._tag === 'Request' && request.tag === 'SyncWsRpc.Pull') {
118
+ // Is Pull request: add requestId to pullRequestIds
119
+ const attachment = ws.deserializeAttachment()
120
+ const { pullRequestIds, ...rest } = Schema.decodeSync(WebSocketAttachmentSchema)(attachment)
121
+ ws.serializeAttachment(
122
+ Schema.encodeSync(WebSocketAttachmentSchema)({
123
+ ...rest,
124
+ pullRequestIds: [...pullRequestIds, request.id],
125
+ }),
126
+ )
127
+ } else if (request._tag === 'Interrupt') {
128
+ // Is Interrupt request: remove requestId from pullRequestIds
129
+ const attachment = ws.deserializeAttachment()
130
+ const { pullRequestIds, ...rest } = Schema.decodeSync(WebSocketAttachmentSchema)(attachment)
131
+ ws.serializeAttachment(
132
+ Schema.encodeSync(WebSocketAttachmentSchema)({
133
+ ...rest,
134
+ pullRequestIds: pullRequestIds.filter((id) => id !== request.requestId),
135
+ }),
136
+ )
137
+ // TODO also emit `Exit` stream RPC message
138
+ }
139
+ },
140
+ mainLayer: Observability,
141
+ })
142
+ }
143
+ }
144
+
145
+ fetch = async (request: Request): Promise<Response> =>
146
+ Effect.gen(this, function* () {
147
+ const searchParams = matchSyncRequest(request)
148
+ if (searchParams === undefined) {
149
+ throw new Error('No search params found in request URL')
150
+ }
151
+
152
+ const { storeId, payload, transport } = searchParams
153
+
154
+ if (enabledTransports.has(transport) === false) {
155
+ throw new Error(`Transport ${transport} is not enabled (based on \`options.enabledTransports\`)`)
156
+ }
157
+
158
+ if (transport === 'http') {
159
+ return yield* this.handleHttp(request)
160
+ }
161
+
162
+ if (transport === 'ws') {
163
+ const { 0: client, 1: server } = new WebSocketPair()
164
+
165
+ // Since we're using websocket hibernation, we need to remember the storeId for subsequent `webSocketMessage` calls
166
+ server.serializeAttachment(
167
+ Schema.encodeSync(WebSocketAttachmentSchema)({ storeId, payload, pullRequestIds: [] }),
168
+ )
169
+
170
+ // See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
171
+
172
+ this.ctx.acceptWebSocket(server)
173
+
174
+ // Ping requests are sent by Effect RPC internally
175
+ this.ctx.setWebSocketAutoResponse(
176
+ new WebSocketRequestResponsePair(
177
+ JSON.stringify(RpcMessage.constPing),
178
+ JSON.stringify(RpcMessage.constPong),
179
+ ),
180
+ )
181
+
182
+ return new Response(null, {
183
+ status: 101,
184
+ webSocket: client,
185
+ })
186
+ }
187
+
188
+ console.error('Invalid path', request.url)
189
+
190
+ return new Response('Invalid path', {
191
+ status: 400,
192
+ statusText: 'Bad Request',
193
+ })
194
+ }).pipe(
195
+ Effect.tapCauseLogPretty, // Also log errors to console before catching them
196
+ Effect.catchAllCause((cause) =>
197
+ Effect.succeed(new Response('Error', { status: 500, statusText: cause.toString() })),
198
+ ),
199
+ Effect.withSpan('@livestore/sync-cf:durable-object:fetch'),
200
+ Effect.provide(DoCtx.Default({ doSelf: this, doOptions: options, from: request })),
201
+ this.runEffectAsPromise,
202
+ )
203
+
204
+ /**
205
+ * Handles DO <-> DO RPC calls
206
+ */
207
+ async rpc(payload: Uint8Array<ArrayBuffer>): Promise<Uint8Array<ArrayBuffer> | CfTypes.ReadableStream> {
208
+ if (enabledTransports.has('do-rpc') === false) {
209
+ throw new Error('Do RPC transport is not enabled (based on `options.enabledTransports`)')
210
+ }
211
+
212
+ return createDoRpcHandler({ payload, input: { doSelf: this, doOptions: options } }).pipe(
213
+ Effect.withSpan('@livestore/sync-cf:durable-object:rpc'),
214
+ this.runEffectAsPromise,
215
+ )
216
+ }
217
+
218
+ /**
219
+ * Handles HTTP RPC calls
220
+ *
221
+ * Requires the `enable_request_signal` compatibility flag to properly support `pull` streaming responses
222
+ */
223
+ private handleHttp = (request: CfTypes.Request) =>
224
+ createHttpRpcHandler({
225
+ request,
226
+ }).pipe(Effect.withSpan('@livestore/sync-cf:durable-object:handleHttp'))
227
+
228
+ private runEffectAsPromise = <T, E = never>(effect: Effect.Effect<T, E, Scope.Scope>): Promise<T> =>
229
+ effect.pipe(
230
+ Effect.tapCauseLogPretty,
231
+ Logger.withMinimumLogLevel(LogLevel.Debug),
232
+ Effect.provide(Layer.mergeAll(Observability, Logging)),
233
+ Effect.scoped,
234
+ Effect.runPromise,
235
+ )
236
+ }
237
+ }
@@ -0,0 +1,128 @@
1
+ import { UnexpectedError } from '@livestore/common'
2
+ import { EventSequenceNumber, State } from '@livestore/common/schema'
3
+ import type { CfTypes } from '@livestore/common-cf'
4
+ import { shouldNeverHappen } from '@livestore/utils'
5
+ import { Effect, Predicate } from '@livestore/utils/effect'
6
+ import { nanoid } from '@livestore/utils/nanoid'
7
+ import type { Env, MakeDurableObjectClassOptions, RpcSubscription } from '../shared.ts'
8
+ import { contextTable, eventlogTable } from './sqlite.ts'
9
+ import { makeStorage } from './sync-storage.ts'
10
+
11
+ const CacheSymbol = Symbol('Cache')
12
+
13
+ export interface DoCtxInput {
14
+ doSelf: CfTypes.DurableObject & {
15
+ ctx: CfTypes.DurableObjectState
16
+ env: Env
17
+ }
18
+ doOptions: MakeDurableObjectClassOptions | undefined
19
+ from: CfTypes.Request | { storeId: string }
20
+ }
21
+
22
+ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
23
+ effect: Effect.fn(
24
+ function* ({ doSelf, doOptions, from }: DoCtxInput) {
25
+ if ((doSelf as any)[CacheSymbol] !== undefined) {
26
+ return (doSelf as any)[CacheSymbol] as never
27
+ }
28
+
29
+ const getStoreId = (from: CfTypes.Request | { storeId: string }) => {
30
+ if (Predicate.hasProperty(from, 'url')) {
31
+ const url = new URL(from.url)
32
+ return (
33
+ url.searchParams.get('storeId') ?? shouldNeverHappen(`No storeId provided in request URL search params`)
34
+ )
35
+ }
36
+ return from.storeId
37
+ }
38
+
39
+ const storeId = getStoreId(from)
40
+ // Resolve storage engine
41
+ const makeEngine = Effect.gen(function* () {
42
+ const opt = doOptions?.storage
43
+ if (opt?._tag === 'd1') {
44
+ const db = (doSelf.env as any)[opt.binding]
45
+ if (!db) {
46
+ return yield* UnexpectedError.make({ cause: new Error(`D1 binding '${opt.binding}' not found on env`) })
47
+ }
48
+ return { _tag: 'd1' as const, db }
49
+ } else if (opt?._tag === 'do-sqlite' || opt === undefined) {
50
+ return { _tag: 'do-sqlite' as const }
51
+ } else return shouldNeverHappen(`Invalid storage engine`, opt)
52
+ })
53
+
54
+ const engine = yield* makeEngine
55
+
56
+ const storage = makeStorage(doSelf.ctx, storeId, engine)
57
+
58
+ // Initialize database tables
59
+ {
60
+ const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
61
+ if (engine._tag === 'd1') {
62
+ // D1 database is async, so we need to use a promise
63
+ yield* Effect.promise(() =>
64
+ engine.db.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`),
65
+ )
66
+ } else {
67
+ // DO SQLite table lives in Durable Object storage
68
+ doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`)
69
+ }
70
+ }
71
+ {
72
+ const colSpec = State.SQLite.makeColumnSpec(contextTable.sqliteDef.ast)
73
+ doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${contextTable.sqliteDef.name}" (${colSpec}) strict`)
74
+ }
75
+
76
+ const storageRow = doSelf.ctx.storage.sql
77
+ .exec(`SELECT * FROM "${contextTable.sqliteDef.name}" WHERE storeId = ?`, storeId)
78
+ .toArray()[0] as typeof contextTable.rowSchema.Type | undefined
79
+
80
+ const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.ROOT.global }
81
+
82
+ // TODO do concistency check with eventlog table to make sure the head is consistent
83
+
84
+ // Should be the same backendId for lifetime of the durable object
85
+ const backendId = storageRow?.backendId ?? nanoid()
86
+
87
+ const updateCurrentHead = (currentHead: EventSequenceNumber.GlobalEventSequenceNumber) => {
88
+ doSelf.ctx.storage.sql.exec(
89
+ `INSERT OR REPLACE INTO "${contextTable.sqliteDef.name}" (storeId, currentHead, backendId) VALUES (?, ?, ?)`,
90
+ storeId,
91
+ currentHead,
92
+ backendId,
93
+ )
94
+
95
+ currentHeadRef.current = currentHead
96
+
97
+ // I still don't know why we need to re-assign this ref to the `doSelf` object but somehow this seems to be needed 😵‍💫
98
+ // @ts-expect-error
99
+ doSelf[CacheSymbol].currentHeadRef = { current: currentHead }
100
+ }
101
+
102
+ const rpcSubscriptions = new Map<string, RpcSubscription>()
103
+
104
+ const storageCache = {
105
+ storeId,
106
+ backendId,
107
+ currentHeadRef,
108
+ updateCurrentHead,
109
+ storage,
110
+ doOptions,
111
+ env: doSelf.env,
112
+ ctx: doSelf.ctx,
113
+ rpcSubscriptions,
114
+ }
115
+
116
+ ;(doSelf as any)[CacheSymbol] = storageCache
117
+
118
+ // Set initial current head to root
119
+ if (storageRow === undefined) {
120
+ updateCurrentHead(EventSequenceNumber.ROOT.global)
121
+ }
122
+
123
+ return storageCache
124
+ },
125
+ UnexpectedError.mapToUnexpectedError,
126
+ Effect.withSpan('@livestore/sync-cf:durable-object:makeDoCtx'),
127
+ ),
128
+ }) {}
@@ -0,0 +1,77 @@
1
+ import { BackendIdMismatchError, InvalidPullError, SyncBackend, UnexpectedError } from '@livestore/common'
2
+ import { splitChunkBySize } from '@livestore/common/sync'
3
+ import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
4
+ import { MAX_PULL_EVENTS_PER_MESSAGE, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
5
+ import { SyncMessage } from '../../common/mod.ts'
6
+ import { DoCtx } from './layer.ts'
7
+
8
+ const encodePullResponse = Schema.encodeSync(SyncMessage.PullResponse)
9
+
10
+ // Notes on stream handling:
11
+ // We're intentionally closing the stream once we've read all existing events
12
+ //
13
+ // WebSocket:
14
+ // - Further chunks will be emitted manually in `push.ts`
15
+ // - If the client sends a `Interrupt` RPC message, it will be handled in the `durable-object.ts` constructor
16
+ // DO RPC:
17
+ // - Further chunks will be emitted manually in `push.ts`
18
+ // - If the client sends a `Interrupt` RPC message, TODO
19
+ export const makeEndingPullStream = (
20
+ req: SyncMessage.PullRequest,
21
+ payload: Schema.JsonValue | undefined,
22
+ ): Stream.Stream<SyncMessage.PullResponse, InvalidPullError, DoCtx> =>
23
+ Effect.gen(function* () {
24
+ const { doOptions, backendId, storeId, storage } = yield* DoCtx
25
+
26
+ if (doOptions?.onPull) {
27
+ yield* Effect.tryAll(() => doOptions!.onPull!(req, { storeId, payload })).pipe(
28
+ UnexpectedError.mapToUnexpectedError,
29
+ )
30
+ }
31
+
32
+ if (req.cursor._tag === 'Some' && req.cursor.value.backendId !== backendId) {
33
+ return yield* new BackendIdMismatchError({ expected: backendId, received: req.cursor.value.backendId })
34
+ }
35
+
36
+ const { stream: storedEvents, total } = yield* storage.getEvents(
37
+ Option.getOrUndefined(req.cursor)?.eventSequenceNumber,
38
+ )
39
+
40
+ return storedEvents.pipe(
41
+ Stream.mapChunksEffect(
42
+ splitChunkBySize({
43
+ maxItems: MAX_PULL_EVENTS_PER_MESSAGE,
44
+ maxBytes: MAX_WS_MESSAGE_BYTES,
45
+ encode: (batch) =>
46
+ encodePullResponse(
47
+ SyncMessage.PullResponse.make({ batch, pageInfo: SyncBackend.pageInfoNoMore, backendId }),
48
+ ),
49
+ }),
50
+ ),
51
+ Stream.mapAccum(total, (remaining, chunk) => {
52
+ const asArray = Chunk.toReadonlyArray(chunk)
53
+ const nextRemaining = Math.max(0, remaining - asArray.length)
54
+
55
+ return [
56
+ nextRemaining,
57
+ SyncMessage.PullResponse.make({
58
+ batch: asArray,
59
+ pageInfo: nextRemaining > 0 ? SyncBackend.pageInfoMoreKnown(nextRemaining) : SyncBackend.pageInfoNoMore,
60
+ backendId,
61
+ }),
62
+ ] as const
63
+ }),
64
+ Stream.tap(
65
+ Effect.fn(function* (res) {
66
+ if (doOptions?.onPullRes) {
67
+ yield* Effect.tryAll(() => doOptions.onPullRes!(res)).pipe(UnexpectedError.mapToUnexpectedError)
68
+ }
69
+ }),
70
+ ),
71
+ Stream.emitIfEmpty(SyncMessage.emptyPullResponse(backendId)),
72
+ )
73
+ }).pipe(
74
+ Stream.unwrap,
75
+ Stream.mapError((cause) => InvalidPullError.make({ cause })),
76
+ Stream.withSpan('cloudflare-provider:pull'),
77
+ )