@livestore/livestore 0.4.0-dev.22 → 0.4.0-dev.23

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 (207) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +14 -7
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +0 -15
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +3 -3
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +2 -2
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  25. package/dist/live-queries/client-document-get-query.js +1 -1
  26. package/dist/live-queries/client-document-get-query.js.map +1 -1
  27. package/dist/live-queries/computed.d.ts.map +1 -1
  28. package/dist/live-queries/computed.js +2 -2
  29. package/dist/live-queries/computed.js.map +1 -1
  30. package/dist/live-queries/db-query.js +14 -14
  31. package/dist/live-queries/db-query.js.map +1 -1
  32. package/dist/live-queries/db-query.test.js +2 -2
  33. package/dist/live-queries/db-query.test.js.map +1 -1
  34. package/dist/live-queries/signal.test.js +2 -2
  35. package/dist/live-queries/signal.test.js.map +1 -1
  36. package/dist/mod.d.ts +1 -1
  37. package/dist/mod.d.ts.map +1 -1
  38. package/dist/mod.js.map +1 -1
  39. package/dist/reactive.d.ts +9 -9
  40. package/dist/reactive.d.ts.map +1 -1
  41. package/dist/reactive.js +9 -26
  42. package/dist/reactive.js.map +1 -1
  43. package/dist/reactive.test.js +2 -2
  44. package/dist/reactive.test.js.map +1 -1
  45. package/dist/store/StoreRegistry.d.ts +30 -5
  46. package/dist/store/StoreRegistry.d.ts.map +1 -1
  47. package/dist/store/StoreRegistry.js +54 -31
  48. package/dist/store/StoreRegistry.js.map +1 -1
  49. package/dist/store/StoreRegistry.test.js +251 -250
  50. package/dist/store/StoreRegistry.test.js.map +1 -1
  51. package/dist/store/create-store.d.ts +6 -2
  52. package/dist/store/create-store.d.ts.map +1 -1
  53. package/dist/store/create-store.js +13 -7
  54. package/dist/store/create-store.js.map +1 -1
  55. package/dist/store/devtools.d.ts +1 -1
  56. package/dist/store/devtools.d.ts.map +1 -1
  57. package/dist/store/devtools.js +3 -3
  58. package/dist/store/devtools.js.map +1 -1
  59. package/dist/store/store-eventstream.test.js +2 -2
  60. package/dist/store/store-eventstream.test.js.map +1 -1
  61. package/dist/store/store-types.d.ts +70 -5
  62. package/dist/store/store-types.d.ts.map +1 -1
  63. package/dist/store/store-types.js.map +1 -1
  64. package/dist/store/store-types.test.js +1 -1
  65. package/dist/store/store-types.test.js.map +1 -1
  66. package/dist/store/store.d.ts +81 -2
  67. package/dist/store/store.d.ts.map +1 -1
  68. package/dist/store/store.js +128 -45
  69. package/dist/store/store.js.map +1 -1
  70. package/dist/utils/dev.js.map +1 -1
  71. package/dist/utils/stack-info.js +2 -2
  72. package/dist/utils/stack-info.js.map +1 -1
  73. package/dist/utils/tests/fixture.d.ts +1 -1
  74. package/dist/utils/tests/fixture.d.ts.map +1 -1
  75. package/dist/utils/tests/fixture.js.map +1 -1
  76. package/dist/utils/tests/otel.d.ts.map +1 -1
  77. package/dist/utils/tests/otel.js +5 -5
  78. package/dist/utils/tests/otel.js.map +1 -1
  79. package/package.json +58 -17
  80. package/src/QueryCache.ts +1 -1
  81. package/src/SqliteDbWrapper.test.ts +4 -2
  82. package/src/SqliteDbWrapper.ts +12 -11
  83. package/src/ambient.d.ts +0 -7
  84. package/src/effect/LiveStore.test.ts +61 -0
  85. package/src/effect/LiveStore.ts +17 -26
  86. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  87. package/src/live-queries/base-class.ts +7 -6
  88. package/src/live-queries/client-document-get-query.ts +4 -2
  89. package/src/live-queries/computed.ts +3 -2
  90. package/src/live-queries/db-query.test.ts +3 -2
  91. package/src/live-queries/db-query.ts +15 -15
  92. package/src/live-queries/signal.test.ts +3 -2
  93. package/src/mod.ts +1 -0
  94. package/src/reactive.test.ts +3 -2
  95. package/src/reactive.ts +22 -23
  96. package/src/store/StoreRegistry.test.ts +317 -293
  97. package/src/store/StoreRegistry.ts +63 -38
  98. package/src/store/create-store.ts +26 -11
  99. package/src/store/devtools.ts +5 -6
  100. package/src/store/store-eventstream.test.ts +4 -2
  101. package/src/store/store-types.test.ts +3 -1
  102. package/src/store/store-types.ts +47 -8
  103. package/src/store/store.ts +172 -55
  104. package/src/utils/dev.ts +2 -2
  105. package/src/utils/stack-info.ts +2 -2
  106. package/src/utils/tests/fixture.ts +2 -1
  107. package/src/utils/tests/otel.ts +8 -7
  108. package/docs/api/index.md +0 -3
  109. package/docs/building-with-livestore/complex-ui-state/index.md +0 -3
  110. package/docs/building-with-livestore/crud/index.md +0 -3
  111. package/docs/building-with-livestore/data-modeling/index.md +0 -30
  112. package/docs/building-with-livestore/debugging/index.md +0 -17
  113. package/docs/building-with-livestore/devtools/index.md +0 -79
  114. package/docs/building-with-livestore/events/index.md +0 -355
  115. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  116. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -885
  117. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  118. package/docs/building-with-livestore/opentelemetry/index.md +0 -227
  119. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  120. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  121. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  122. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  123. package/docs/building-with-livestore/state/sql-queries/index.md +0 -94
  124. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  125. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  126. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  127. package/docs/building-with-livestore/store/index.md +0 -625
  128. package/docs/building-with-livestore/syncing/index.md +0 -136
  129. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  130. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  131. package/docs/examples/cloudflare-adapter/index.md +0 -44
  132. package/docs/examples/expo-adapter/index.md +0 -44
  133. package/docs/examples/index.md +0 -55
  134. package/docs/examples/node-adapter/index.md +0 -44
  135. package/docs/examples/web-adapter/index.md +0 -52
  136. package/docs/framework-integrations/custom-elements/index.md +0 -142
  137. package/docs/framework-integrations/react-integration/index.md +0 -937
  138. package/docs/framework-integrations/solid-integration/index.md +0 -293
  139. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  140. package/docs/framework-integrations/vue-integration/index.md +0 -294
  141. package/docs/getting-started/expo/index.md +0 -882
  142. package/docs/getting-started/node/index.md +0 -115
  143. package/docs/getting-started/react-web/index.md +0 -626
  144. package/docs/getting-started/solid/index.md +0 -3
  145. package/docs/getting-started/vue/index.md +0 -471
  146. package/docs/index.md +0 -208
  147. package/docs/llms.txt +0 -146
  148. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  149. package/docs/misc/FAQ/index.md +0 -37
  150. package/docs/misc/community/index.md +0 -88
  151. package/docs/misc/credits/index.md +0 -14
  152. package/docs/misc/design-partners/index.md +0 -13
  153. package/docs/misc/package-management/index.md +0 -21
  154. package/docs/misc/performance/index.md +0 -25
  155. package/docs/misc/resources/index.md +0 -46
  156. package/docs/misc/state-of-the-project/index.md +0 -37
  157. package/docs/misc/troubleshooting/index.md +0 -82
  158. package/docs/overview/concepts/index.md +0 -78
  159. package/docs/overview/how-livestore-works/index.md +0 -56
  160. package/docs/overview/introduction/index.md +0 -413
  161. package/docs/overview/technology-comparison/index.md +0 -40
  162. package/docs/overview/when-livestore/index.md +0 -81
  163. package/docs/overview/why-livestore/index.md +0 -111
  164. package/docs/patterns/ai/index.md +0 -15
  165. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  166. package/docs/patterns/app-evolution/index.md +0 -72
  167. package/docs/patterns/auth/index.md +0 -377
  168. package/docs/patterns/effect/index.md +0 -1505
  169. package/docs/patterns/encryption/index.md +0 -6
  170. package/docs/patterns/external-data/index.md +0 -5
  171. package/docs/patterns/file-management/index.md +0 -11
  172. package/docs/patterns/file-structure/index.md +0 -14
  173. package/docs/patterns/list-ordering/index.md +0 -369
  174. package/docs/patterns/offline/index.md +0 -32
  175. package/docs/patterns/orm/index.md +0 -18
  176. package/docs/patterns/presence/index.md +0 -11
  177. package/docs/patterns/rich-text-editing/index.md +0 -11
  178. package/docs/patterns/server-side-clients/index.md +0 -97
  179. package/docs/patterns/side-effects/index.md +0 -11
  180. package/docs/patterns/state-machines/index.md +0 -11
  181. package/docs/patterns/storybook/index.md +0 -209
  182. package/docs/patterns/undo-redo/index.md +0 -9
  183. package/docs/patterns/version-control/index.md +0 -8
  184. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  185. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  186. package/docs/platform-adapters/expo-adapter/index.md +0 -262
  187. package/docs/platform-adapters/node-adapter/index.md +0 -160
  188. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  189. package/docs/platform-adapters/web-adapter/index.md +0 -287
  190. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  191. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  192. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  193. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  194. package/docs/sync-providers/cloudflare/index.md +0 -773
  195. package/docs/sync-providers/custom/index.md +0 -65
  196. package/docs/sync-providers/electricsql/index.md +0 -159
  197. package/docs/sync-providers/s2/index.md +0 -230
  198. package/docs/tutorial/0-welcome/index.md +0 -48
  199. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  200. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  201. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -530
  202. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  203. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  204. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  205. package/docs/tutorial/7-next-steps/index.md +0 -22
  206. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  207. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -1,773 +0,0 @@
1
- # Cloudflare Workers
2
-
3
- The `@livestore/sync-cf` package provides a comprehensive LiveStore sync provider for Cloudflare Workers. It uses Durable Objects for connectivity and, by default, persists events in the Durable Object's own SQLite. You can optionally use Cloudflare D1 instead. Multiple transports are supported to fit different deployment scenarios.
4
-
5
- ## Architecture
6
-
7
- <div class="d2-full-width">
8
-
9
- ```d2
10
- ...@../../../../src/content/base.d2
11
-
12
- direction: right
13
-
14
- Client: {
15
- label: "LiveStore Client"
16
- shape: rectangle
17
- }
18
-
19
- CF: {
20
- label: "Cloudflare"
21
- style.stroke-dash: 3
22
-
23
- Worker: {
24
- label: "Worker"
25
- shape: rectangle
26
- }
27
-
28
- DO: {
29
- label: "Durable Object\n(per storeId)"
30
- shape: rectangle
31
- }
32
-
33
- Storage: {
34
- label: "DO SQLite (default)\nor D1 (optional)"
35
- shape: cylinder
36
- }
37
-
38
- Worker -> DO: "Route by\nstoreId"
39
- DO -> Storage: "Read/Write"
40
- }
41
-
42
- Client -> CF.Worker: "WebSocket / HTTP\npush & pull"
43
- ```
44
-
45
- </div>
46
-
47
- Key responsibilities:
48
- - **Worker**: Routes sync requests to Durable Objects by `storeId`, handles auth validation
49
- - **Durable Object**: Manages sync state, handles push/pull operations, maintains WebSocket connections
50
- - **Storage**: Persists events in DO SQLite (default) or D1 (optional)
51
-
52
- ## Installation
53
-
54
- ```bash
55
- pnpm add @livestore/sync-cf
56
- ```
57
-
58
- ## Transport modes
59
-
60
- The sync provider supports three transport protocols, each optimized for different use cases:
61
-
62
- ### WebSocket transport (Recommended)
63
-
64
- Real-time bidirectional communication with automatic reconnection and live pull support.
65
-
66
- ## `reference/syncing/cloudflare/client-ws.ts`
67
-
68
- ```ts filename="reference/syncing/cloudflare/client-ws.ts"
69
-
70
- export const syncBackend = makeWsSync({
71
- url: 'wss://sync.example.com',
72
- })
73
- ```
74
-
75
- ### HTTP transport
76
-
77
- HTTP-based sync with polling for live updates. Requires the `enable_request_signal` compatibility flag.
78
-
79
- ## `reference/syncing/cloudflare/client-http.ts`
80
-
81
- ```ts filename="reference/syncing/cloudflare/client-http.ts"
82
-
83
- export const syncBackend = makeHttpSync({
84
- url: 'https://sync.example.com',
85
- livePull: {
86
- pollInterval: 3000, // Poll every 3 seconds
87
- },
88
- })
89
- ```
90
-
91
- ### Durable Object RPC transport
92
-
93
- Direct RPC communication between Durable Objects (internal use by `@livestore/adapter-cloudflare`).
94
-
95
- ## `reference/syncing/cloudflare/client-do-rpc.ts`
96
-
97
- ```ts filename="reference/syncing/cloudflare/client-do-rpc.ts"
98
-
99
- declare const state: CfTypes.DurableObjectState
100
- declare const syncBackendDurableObject: CfTypes.DurableObjectStub<SyncBackendRpcInterface>
101
-
102
- export const syncBackend = makeDoRpcSync({
103
- syncBackendStub: syncBackendDurableObject,
104
- durableObjectContext: {
105
- bindingName: 'CLIENT_DO',
106
- durableObjectId: state.id.toString(),
107
- },
108
- })
109
- ```
110
-
111
- ## Client API reference
112
-
113
- ### `makeWsSync(options)`
114
-
115
- Creates a WebSocket-based sync backend client.
116
-
117
- **Options:**
118
- - `url` - WebSocket URL (supports `ws`/`wss` or `http`/`https` protocols)
119
- - `webSocketFactory?` - Custom WebSocket implementation
120
- - `ping?` - Ping configuration:
121
- - `enabled?: boolean` - Enable/disable ping (default: `true`)
122
- - `requestTimeout?: Duration` - Ping timeout (default: 10 seconds)
123
- - `requestInterval?: Duration` - Ping interval (default: 10 seconds)
124
-
125
- **Features:**
126
- - Real-time live pull
127
- - Automatic reconnection
128
- - Connection status tracking
129
- - Ping/pong keep-alive
130
-
131
- ## `reference/syncing/cloudflare/client-ws-options.ts`
132
-
133
- ```ts filename="reference/syncing/cloudflare/client-ws-options.ts"
134
-
135
- export const syncBackend = makeWsSync({
136
- url: 'wss://sync.example.com',
137
- ping: {
138
- enabled: true,
139
- requestTimeout: 5000,
140
- requestInterval: 15000,
141
- },
142
- })
143
- ```
144
-
145
- ### `makeHttpSync(options)`
146
-
147
- Creates an HTTP-based sync backend client with polling for live updates.
148
-
149
- **Options:**
150
- - `url` - HTTP endpoint URL
151
- - `headers?` - Additional HTTP headers
152
- - `livePull?` - Live pull configuration:
153
- - `pollInterval?: Duration` - Polling interval (default: 5 seconds)
154
- - `ping?` - Ping configuration (same as WebSocket)
155
-
156
- **Features:**
157
- - HTTP request/response based
158
- - Polling-based live pull
159
- - Custom headers support
160
- - Connection status via ping
161
-
162
- ## `reference/syncing/cloudflare/client-http-options.ts`
163
-
164
- ```ts filename="reference/syncing/cloudflare/client-http-options.ts"
165
-
166
- export const syncBackend = makeHttpSync({
167
- url: 'https://sync.example.com',
168
- headers: {
169
- Authorization: 'Bearer token',
170
- 'X-Custom-Header': 'value',
171
- },
172
- livePull: {
173
- pollInterval: 2000, // Poll every 2 seconds
174
- },
175
- })
176
- ```
177
-
178
- ### `makeDoRpcSync(options)`
179
-
180
- Creates a Durable Object RPC-based sync backend (for internal use).
181
-
182
- **Options:**
183
- - `syncBackendStub` - Durable Object stub implementing `SyncBackendRpcInterface`
184
- - `durableObjectContext` - Context for RPC callbacks:
185
- - `bindingName` - Wrangler binding name for the client DO
186
- - `durableObjectId` - Client Durable Object ID
187
-
188
- **Features:**
189
- - Direct RPC communication
190
- - Real-time live pull via callbacks
191
- - Hibernation support
192
-
193
- ### `handleSyncUpdateRpc(payload)`
194
-
195
- Handles RPC callback for live pull updates in Durable Objects.
196
-
197
- ## `reference/platform-adapters/cloudflare/client-do.ts`
198
-
199
- ```ts filename="reference/platform-adapters/cloudflare/client-do.ts"
200
- /// <reference types="@cloudflare/workers-types" />
201
-
202
- type AlarmInfo = {
203
- isRetry: boolean
204
- retryCount: number
205
- }
206
-
207
- export class LiveStoreClientDO extends DurableObject<Env> implements ClientDoWithRpcCallback {
208
- __DURABLE_OBJECT_BRAND: never = undefined as never
209
-
210
- private storeId: string | undefined
211
- private cachedStore: Store<typeof schema> | undefined
212
- private storeSubscription: Unsubscribe | undefined
213
- private readonly todosQuery = tables.todos.select()
214
-
215
- async fetch(request: Request): Promise<Response> {
216
- // @ts-expect-error TODO remove casts once CF types are fixed in https://github.com/cloudflare/workerd/issues/4811
217
- this.storeId = storeIdFromRequest(request)
218
-
219
- const store = await this.getStore()
220
- await this.subscribeToStore()
221
-
222
- const todos = store.query(this.todosQuery)
223
- return new Response(JSON.stringify(todos, null, 2), {
224
- headers: { 'Content-Type': 'application/json' },
225
- })
226
- }
227
-
228
- private async getStore() {
229
- if (this.cachedStore !== undefined) {
230
- return this.cachedStore
231
- }
232
-
233
- const storeId = this.storeId ?? nanoid()
234
-
235
- const store = await createStoreDoPromise({
236
- schema,
237
- storeId,
238
- clientId: 'client-do',
239
- sessionId: nanoid(),
240
- durableObject: {
241
- // @ts-expect-error TODO remove once CF types are fixed in https://github.com/cloudflare/workerd/issues/4811
242
- ctx: this.ctx,
243
- env: this.env,
244
- bindingName: 'CLIENT_DO',
245
- },
246
- syncBackendStub: this.env.SYNC_BACKEND_DO.get(this.env.SYNC_BACKEND_DO.idFromName(storeId)),
247
- livePull: true,
248
- })
249
-
250
- this.cachedStore = store
251
- return store
252
- }
253
-
254
- private async subscribeToStore() {
255
- const store = await this.getStore()
256
-
257
- if (this.storeSubscription === undefined) {
258
- this.storeSubscription = store.subscribe(this.todosQuery, (todos: ReadonlyArray<typeof tables.todos.Type>) => {
259
- console.log(`todos for store (${this.storeId})`, todos)
260
- })
261
- }
262
-
263
- await this.ctx.storage.setAlarm(Date.now() + 1000)
264
- }
265
-
266
- alarm(_alarmInfo?: AlarmInfo): void | Promise<void> {
267
- return this.subscribeToStore()
268
- }
269
-
270
- async syncUpdateRpc(payload: unknown) {
271
- await handleSyncUpdateRpc(payload)
272
- }
273
- }
274
- ```
275
-
276
- ### `reference/platform-adapters/cloudflare/env.ts`
277
-
278
- ```ts filename="reference/platform-adapters/cloudflare/env.ts"
279
-
280
- export type Env = {
281
- CLIENT_DO: CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
282
- SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>
283
- DB: CfTypes.D1Database
284
- }
285
- ```
286
-
287
- ### `reference/platform-adapters/cloudflare/schema.ts`
288
-
289
- ```ts filename="reference/platform-adapters/cloudflare/schema.ts"
290
-
291
- export const tables = {
292
- todos: State.SQLite.table({
293
- name: 'todos',
294
- columns: {
295
- id: State.SQLite.text({ primaryKey: true }),
296
- text: State.SQLite.text({ default: '' }),
297
- completed: State.SQLite.boolean({ default: false }),
298
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
299
- },
300
- }),
301
- }
302
-
303
- export const events = {
304
- todoCreated: Events.synced({
305
- name: 'v1.TodoCreated',
306
- schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
307
- }),
308
- todoCompleted: Events.synced({
309
- name: 'v1.TodoCompleted',
310
- schema: Schema.Struct({ id: Schema.String }),
311
- }),
312
- todoUncompleted: Events.synced({
313
- name: 'v1.TodoUncompleted',
314
- schema: Schema.Struct({ id: Schema.String }),
315
- }),
316
- todoDeleted: Events.synced({
317
- name: 'v1.TodoDeleted',
318
- schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
319
- }),
320
- todoClearedCompleted: Events.synced({
321
- name: 'v1.TodoClearedCompleted',
322
- schema: Schema.Struct({ deletedAt: Schema.Date }),
323
- }),
324
- }
325
-
326
- const materializers = State.SQLite.materializers(events, {
327
- 'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text, completed: false }),
328
- 'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
329
- 'v1.TodoUncompleted': ({ id }) => tables.todos.update({ completed: false }).where({ id }),
330
- 'v1.TodoDeleted': ({ id, deletedAt }) => tables.todos.update({ deletedAt }).where({ id }),
331
- 'v1.TodoClearedCompleted': ({ deletedAt }) => tables.todos.update({ deletedAt }).where({ completed: true }),
332
- })
333
-
334
- const state = State.SQLite.makeState({ tables, materializers })
335
-
336
- export const schema = makeSchema({ events, state })
337
- ```
338
-
339
- ### `reference/platform-adapters/cloudflare/shared.ts`
340
-
341
- ```ts filename="reference/platform-adapters/cloudflare/shared.ts"
342
-
343
- export const storeIdFromRequest = (request: CfTypes.Request) => {
344
- const url = new URL(request.url)
345
- const storeId = url.searchParams.get('storeId')
346
-
347
- if (storeId === null) {
348
- throw new Error('storeId is required in URL search params')
349
- }
350
-
351
- return storeId
352
- }
353
- ```
354
-
355
- ## Server API reference
356
-
357
- ### `makeDurableObject(options)`
358
-
359
- Creates a sync backend Durable Object class.
360
-
361
- **Options:**
362
- - `onPush?` - Callback for push events: `(message, context) => void | Promise<void>`
363
- - `onPushRes?` - Callback for push responses: `(message) => void | Promise<void>`
364
- - `onPull?` - Callback for pull requests: `(message, context) => void | Promise<void>`
365
- - `onPullRes?` - Callback for pull responses: `(message) => void | Promise<void>`
366
- - `storage?` - Storage engine: `{ _tag: 'do-sqlite' } | { _tag: 'd1', binding: string }` (default: `do-sqlite`)
367
- - `enabledTransports?` - Set of enabled transports: `Set<'http' | 'ws' | 'do-rpc'>`
368
- - `otel?` - OpenTelemetry configuration:
369
- - `baseUrl?` - OTEL endpoint URL
370
- - `serviceName?` - Service name for traces
371
-
372
- ## `reference/syncing/cloudflare/do-sync-backend.ts`
373
-
374
- ```ts filename="reference/syncing/cloudflare/do-sync-backend.ts"
375
-
376
- const hasUserId = (p: unknown): p is { userId: string } =>
377
- typeof p === 'object' && p !== undefined && p !== null && 'userId' in p
378
-
379
- export class SyncBackendDO extends makeDurableObject({
380
- onPush: async (message, { storeId, payload }) => {
381
- console.log(`Push to store ${storeId}:`, message.batch)
382
-
383
- // Custom business logic
384
- if (hasUserId(payload)) {
385
- await Promise.resolve()
386
- }
387
- },
388
- onPull: async (_message, { storeId }) => {
389
- console.log(`Pull from store ${storeId}`)
390
- },
391
- enabledTransports: new Set(['ws', 'http']), // Disable DO RPC
392
- otel: {
393
- baseUrl: 'https://otel.example.com',
394
- serviceName: 'livestore-sync',
395
- },
396
- }) {}
397
- ```
398
-
399
- ### `makeWorker(options)`
400
-
401
- Creates a complete Cloudflare Worker for the sync backend.
402
-
403
- **Options:**
404
- - `syncBackendBinding` - Durable Object binding name defined in `wrangler.toml`
405
- - `validatePayload?` - Payload validation function: `(payload, context) => void | Promise<void>`
406
- - `enableCORS?` - Enable CORS headers (default: `false`)
407
-
408
- `makeWorker` is a quick way to get started in simple demos. In most production workers you typically want to share routing logic with other endpoints, so prefer wiring your own `fetch` handler and call `handleSyncRequest` when you detect a sync request. A minimal example:
409
-
410
- ## `reference/syncing/cloudflare/worker-minimal.ts`
411
-
412
- ```ts filename="reference/syncing/cloudflare/worker-minimal.ts"
413
-
414
- export default {
415
- fetch: async (request: CfTypes.Request, env: Env, ctx: CfTypes.ExecutionContext) => {
416
- const searchParams = matchSyncRequest(request)
417
-
418
- if (searchParams !== undefined) {
419
- return handleSyncRequest({
420
- request,
421
- searchParams,
422
- env,
423
- ctx,
424
- syncBackendBinding: 'SYNC_BACKEND_DO',
425
- })
426
- }
427
-
428
- // Custom routes, assets, etc.
429
- return new Response('Not found', { status: 404 }) as unknown as CfTypes.Response
430
- },
431
- } satisfies CFWorker<Env>
432
- ```
433
-
434
- ### `reference/syncing/cloudflare/env.ts`
435
-
436
- ```ts filename="reference/syncing/cloudflare/env.ts"
437
-
438
- export interface Env {
439
- SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>
440
- }
441
- ```
442
-
443
- ## `reference/syncing/cloudflare/worker-makeWorker.ts`
444
-
445
- ```ts filename="reference/syncing/cloudflare/worker-makeWorker.ts"
446
-
447
- export default makeWorker({
448
- syncBackendBinding: 'SYNC_BACKEND_DO',
449
- validatePayload: (payload, { storeId }) => {
450
- // Simple token-based guard at connection time
451
- const hasAuthToken = typeof payload === 'object' && payload !== null && 'authToken' in payload
452
- if (!hasAuthToken) {
453
- throw new Error('Missing auth token')
454
- }
455
- if ((payload as any).authToken !== 'insecure-token-change-me') {
456
- throw new Error('Invalid auth token')
457
- }
458
- console.log(`Validated connection for store: ${storeId}`)
459
- },
460
- enableCORS: true,
461
- })
462
- ```
463
-
464
- ### `handleSyncRequest(args)`
465
-
466
- Handles sync backend HTTP requests in custom workers.
467
-
468
- **Options:**
469
- - `request` - The incoming request
470
- - `searchParams` - Parsed sync request parameters
471
- - `env` - Worker environment
472
- - `ctx` - Worker execution context
473
- - `syncBackendBinding` - Durable Object binding name defined in `wrangler.toml`
474
- - `headers?` - Response headers
475
- - `validatePayload?` - Payload validation function
476
-
477
- ## `reference/syncing/cloudflare/worker-handleSyncRequest.ts`
478
-
479
- ```ts filename="reference/syncing/cloudflare/worker-handleSyncRequest.ts"
480
-
481
- export default {
482
- fetch: async (request: CfTypes.Request, env: Env, ctx: CfTypes.ExecutionContext) => {
483
- const searchParams = matchSyncRequest(request)
484
-
485
- if (searchParams !== undefined) {
486
- return handleSyncRequest({
487
- request,
488
- searchParams,
489
- env,
490
- ctx,
491
- syncBackendBinding: 'SYNC_BACKEND_DO',
492
- headers: { 'X-Custom': 'header' },
493
- validatePayload: (payload, { storeId }) => {
494
- // Custom validation logic
495
- if (!(typeof payload === 'object' && payload !== null && 'authToken' in payload)) {
496
- throw new Error('Missing auth token')
497
- }
498
- console.log('Validating store', storeId)
499
- },
500
- })
501
- }
502
-
503
- return new Response('Not found', { status: 404 }) as unknown as CfTypes.Response
504
- },
505
- } satisfies CFWorker<Env>
506
- ```
507
-
508
- ### `reference/syncing/cloudflare/env.ts`
509
-
510
- ```ts filename="reference/syncing/cloudflare/env.ts"
511
-
512
- export interface Env {
513
- SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>
514
- }
515
- ```
516
-
517
- ### `matchSyncRequest(request)`
518
-
519
- Parses and validates sync request search parameters.
520
-
521
- Returns the decoded search params or `undefined` if the request is not a LiveStore sync request.
522
-
523
- ## `reference/syncing/cloudflare/match-sync.ts`
524
-
525
- ```ts filename="reference/syncing/cloudflare/match-sync.ts"
526
-
527
- declare const request: CfTypes.Request
528
-
529
- const searchParams = matchSyncRequest(request)
530
- if (searchParams !== undefined) {
531
- const { storeId, payload, transport } = searchParams
532
- console.log(`Sync request for store ${storeId} via ${transport}`)
533
- console.log(payload)
534
- }
535
- ```
536
-
537
- ## Configuration
538
-
539
- ### Wrangler configuration
540
-
541
- Configure your `wrangler.toml` for sync backend deployment (default: DO SQLite storage):
542
-
543
- ```toml
544
- name = "livestore-sync"
545
- main = "./src/worker.ts"
546
- compatibility_date = "2025-05-07"
547
- compatibility_flags = [
548
- "enable_request_signal", # Required for HTTP streaming
549
- ]
550
-
551
- [[durable_objects.bindings]]
552
- name = "SYNC_BACKEND_DO"
553
- class_name = "SyncBackendDO"
554
-
555
- [[migrations]]
556
- tag = "v1"
557
- new_sqlite_classes = ["SyncBackendDO"]
558
- ```
559
-
560
- To use D1 instead of DO SQLite, add a D1 binding and reference it from `makeDurableObject({ storage: { _tag: 'd1', binding: '...' } })`:
561
-
562
- ```toml
563
- [[d1_databases]]
564
- binding = "DB"
565
- database_name = "livestore-sync"
566
- database_id = "your-database-id"
567
- ```
568
-
569
- ### Environment variables
570
-
571
- Required environment bindings:
572
-
573
- ## `reference/syncing/cloudflare/env.ts`
574
-
575
- ```ts filename="reference/syncing/cloudflare/env.ts"
576
-
577
- export interface Env {
578
- SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>
579
- }
580
- ```
581
-
582
- ## Transport protocol details
583
-
584
- LiveStore identifies sync requests purely by search parameters; the request path does not matter. Use `matchSyncRequest(request)` to detect sync traffic.
585
-
586
- Required search parameters:
587
-
588
- | Param | Type | Required | Description |
589
- | --- | --- | --- | --- |
590
- | `storeId` | `string` | Yes | Target LiveStore identifier. |
591
- | `transport` | `'ws' \| 'http'` | Yes | Transport protocol selector. |
592
- | `payload` | JSON (URI-encoded) | No | Arbitrary JSON used for auth/tenant routing; validated in `validatePayload`. |
593
-
594
- Examples (any path):
595
-
596
- - WebSocket: `https://sync.example.com?storeId=abc&transport=ws` (must include `Upgrade: websocket`)
597
- - HTTP: `https://sync.example.com?storeId=abc&transport=http`
598
-
599
- Notes:
600
- - For `transport=ws`, if the request is not a WebSocket upgrade, the backend returns `426 Upgrade Required`.
601
- - `transport='do-rpc'` is internal for Durable Object RPC and not exposed via URL parameters.
602
-
603
- ## Data storage
604
-
605
- By default, events are stored in the Durable Object’s SQLite with tables following the pattern:
606
- ```
607
- eventlog_{PERSISTENCE_FORMAT_VERSION}_{storeId}
608
- ```
609
-
610
- You can opt into D1 with the same table shape. The persistence format version is automatically managed and incremented when the storage schema changes.
611
-
612
- ### Storage engines
613
- - DO SQLite (default)
614
- - Pros: easiest deploy (no D1), data co-located with the DO, lowest latency
615
- - Cons: not directly inspectable outside the DO; operational tooling must go through the DO
616
- - D1 (optional)
617
- - Pros: inspectable using D1 tools/clients; enables cross-store analytics outside DOs
618
- - Cons: extra hop, JSON response size considerations; requires D1 provisioning
619
-
620
- ## Deployment
621
-
622
- Deploy to Cloudflare Workers:
623
-
624
- ```bash
625
- # Deploy the worker
626
- npx wrangler deploy
627
-
628
- # Create D1 database
629
- npx wrangler d1 create livestore-sync
630
-
631
- # Run migrations if needed
632
- npx wrangler d1 migrations apply livestore-sync
633
- ```
634
-
635
- ## Local development
636
-
637
- Run locally with Wrangler:
638
-
639
- ```bash
640
- # Start local development server
641
- npx wrangler dev
642
-
643
- # Access local D1 database
644
- # Located at: .wrangler/state/d1/miniflare-D1DatabaseObject/XXX.sqlite
645
- ```
646
-
647
- ## Examples
648
-
649
- ### Basic WebSocket client
650
-
651
- ## `reference/syncing/cloudflare/basic-ws-client.ts`
652
-
653
- ```ts filename="reference/syncing/cloudflare/basic-ws-client.ts"
654
-
655
- makeWorker({
656
- schema,
657
- sync: {
658
- backend: makeWsSync({
659
- url: 'wss://sync.example.com',
660
- }),
661
- },
662
- })
663
- ```
664
-
665
- ### `reference/syncing/cloudflare/schema.ts`
666
-
667
- ```ts filename="reference/syncing/cloudflare/schema.ts"
668
-
669
- export const tables = {
670
- todos: State.SQLite.table({
671
- name: 'todos',
672
- columns: {
673
- id: State.SQLite.text({ primaryKey: true }),
674
- text: State.SQLite.text({ default: '' }),
675
- completed: State.SQLite.boolean({ default: false }),
676
- deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
677
- },
678
- }),
679
- }
680
-
681
- export const events = {
682
- todoCreated: Events.synced({
683
- name: 'v1.TodoCreated',
684
- schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
685
- }),
686
- todoCompleted: Events.synced({
687
- name: 'v1.TodoCompleted',
688
- schema: Schema.Struct({ id: Schema.String }),
689
- }),
690
- todoUncompleted: Events.synced({
691
- name: 'v1.TodoUncompleted',
692
- schema: Schema.Struct({ id: Schema.String }),
693
- }),
694
- todoDeleted: Events.synced({
695
- name: 'v1.TodoDeleted',
696
- schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
697
- }),
698
- todoClearedCompleted: Events.synced({
699
- name: 'v1.TodoClearedCompleted',
700
- schema: Schema.Struct({ deletedAt: Schema.Date }),
701
- }),
702
- }
703
-
704
- const materializers = State.SQLite.materializers(events, {
705
- 'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text, completed: false }),
706
- 'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
707
- 'v1.TodoUncompleted': ({ id }) => tables.todos.update({ completed: false }).where({ id }),
708
- 'v1.TodoDeleted': ({ id, deletedAt }) => tables.todos.update({ deletedAt }).where({ id }),
709
- 'v1.TodoClearedCompleted': ({ deletedAt }) => tables.todos.update({ deletedAt }).where({ completed: true }),
710
- })
711
-
712
- const state = State.SQLite.makeState({ tables, materializers })
713
-
714
- export const schema = makeSchema({ events, state })
715
- ```
716
-
717
- ### Custom worker with authentication
718
-
719
- ## `reference/syncing/cloudflare/worker-auth.ts`
720
-
721
- ```ts filename="reference/syncing/cloudflare/worker-auth.ts"
722
-
723
- export class SyncBackendDO extends makeDurableObject({
724
- onPush: async (message, { storeId }) => {
725
- // Log all sync events
726
- console.log(`Store ${storeId} received ${message.batch.length} events`)
727
- },
728
- }) {}
729
-
730
- const hasStoreAccess = (_userId: string, _storeId: string): boolean => true
731
-
732
- export default makeWorker({
733
- syncBackendBinding: 'SYNC_BACKEND_DO',
734
- validatePayload: (payload, { storeId }) => {
735
- if (!(typeof payload === 'object' && payload !== null && 'userId' in payload)) {
736
- throw new Error('User ID required')
737
- }
738
-
739
- // Validate user has access to store
740
- if (!hasStoreAccess((payload as any).userId as string, storeId)) {
741
- throw new Error('Unauthorized access to store')
742
- }
743
- },
744
- enableCORS: true,
745
- })
746
- ```
747
-
748
- ### Multi-Transport Setup
749
-
750
- ## `reference/syncing/cloudflare/multi-transport.ts`
751
-
752
- ```ts filename="reference/syncing/cloudflare/multi-transport.ts"
753
-
754
- type Transport = 'http' | 'ws' | 'do-rpc'
755
-
756
- const getTransportFromContext = (ctx: unknown): Transport => {
757
- if (typeof ctx === 'object' && ctx !== null && 'transport' in (ctx as any)) {
758
- const t = (ctx as any).transport
759
- if (t === 'http' || t === 'ws' || t === 'do-rpc') return t
760
- }
761
- return 'http'
762
- }
763
-
764
- export class SyncBackendDO extends makeDurableObject({
765
- // Enable all transport modes
766
- enabledTransports: new Set<Transport>(['http', 'ws', 'do-rpc']),
767
-
768
- onPush: async (message, context) => {
769
- const transport = getTransportFromContext(context)
770
- console.log(`Push via ${transport}:`, message.batch.length)
771
- },
772
- }) {}
773
- ```