@livestore/livestore 0.4.0-dev.21 → 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 (216) 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 +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  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/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. 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
- ```