@livestore/sync-cf 0.3.0-dev.10 → 0.3.0-dev.2

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 (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cf-worker/durable-object.d.ts +34 -27
  3. package/dist/cf-worker/durable-object.d.ts.map +1 -1
  4. package/dist/cf-worker/durable-object.js +128 -149
  5. package/dist/cf-worker/durable-object.js.map +1 -1
  6. package/dist/cf-worker/index.d.ts +6 -1
  7. package/dist/cf-worker/index.d.ts.map +1 -1
  8. package/dist/cf-worker/index.js +64 -30
  9. package/dist/cf-worker/index.js.map +1 -1
  10. package/dist/common/ws-message-types.d.ts +207 -54
  11. package/dist/common/ws-message-types.d.ts.map +1 -1
  12. package/dist/common/ws-message-types.js +3 -3
  13. package/dist/common/ws-message-types.js.map +1 -1
  14. package/dist/sync-impl/ws-impl.d.ts +12 -3
  15. package/dist/sync-impl/ws-impl.d.ts.map +1 -1
  16. package/dist/sync-impl/ws-impl.js +4 -5
  17. package/dist/sync-impl/ws-impl.js.map +1 -1
  18. package/package.json +13 -15
  19. package/src/cf-worker/durable-object.ts +161 -212
  20. package/src/cf-worker/index.ts +84 -0
  21. package/src/common/ws-message-types.ts +3 -3
  22. package/src/sync-impl/ws-impl.ts +16 -8
  23. package/dist/cf-worker/make-worker.d.ts +0 -6
  24. package/dist/cf-worker/make-worker.d.ts.map +0 -1
  25. package/dist/cf-worker/make-worker.js +0 -31
  26. package/dist/cf-worker/make-worker.js.map +0 -1
  27. package/dist/cf-worker/mod.d.ts +0 -3
  28. package/dist/cf-worker/mod.d.ts.map +0 -1
  29. package/dist/cf-worker/mod.js +0 -3
  30. package/dist/cf-worker/mod.js.map +0 -1
  31. package/dist/cf-worker/types.d.ts +0 -2
  32. package/dist/cf-worker/types.d.ts.map +0 -1
  33. package/dist/cf-worker/types.js +0 -2
  34. package/dist/cf-worker/types.js.map +0 -1
  35. package/dist/cf-worker/worker.d.ts +0 -6
  36. package/dist/cf-worker/worker.d.ts.map +0 -1
  37. package/dist/cf-worker/worker.js +0 -29
  38. package/dist/cf-worker/worker.js.map +0 -1
  39. package/dist/common/mod.d.ts +0 -2
  40. package/dist/common/mod.d.ts.map +0 -1
  41. package/dist/common/mod.js +0 -2
  42. package/dist/common/mod.js.map +0 -1
  43. package/dist/sync-impl/mod.d.ts +0 -2
  44. package/dist/sync-impl/mod.d.ts.map +0 -1
  45. package/dist/sync-impl/mod.js +0 -2
  46. package/dist/sync-impl/mod.js.map +0 -1
  47. package/src/cf-worker/mod.ts +0 -2
  48. package/src/cf-worker/worker.ts +0 -39
  49. /package/src/common/{mod.ts → index.ts} +0 -0
  50. /package/src/sync-impl/{mod.ts → index.ts} +0 -0
@@ -4,11 +4,11 @@ import { shouldNeverHappen } from '@livestore/utils'
4
4
  import { Effect, Logger, LogLevel, Option, Schema } from '@livestore/utils/effect'
5
5
  import { DurableObject } from 'cloudflare:workers'
6
6
 
7
- import { WSMessage } from '../common/mod.js'
7
+ import { WSMessage } from '../common/index.js'
8
8
  import type { SyncMetadata } from '../common/ws-message-types.js'
9
9
 
10
10
  export interface Env {
11
- WEBSOCKET_SERVER: DurableObjectNamespace
11
+ WEBSOCKET_SERVER: DurableObjectNamespace<WebSocketServer>
12
12
  DB: D1Database
13
13
  ADMIN_SECRET: string
14
14
  }
@@ -21,274 +21,235 @@ const decodeIncomingMessage = Schema.decodeUnknownEither(Schema.parseJson(WSMess
21
21
 
22
22
  // NOTE actual table name is determined at runtime by `WebSocketServer.dbName`
23
23
  export const mutationLogTable = DbSchema.table('__unused', {
24
- id: DbSchema.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
25
- parentId: DbSchema.integer({ schema: EventId.GlobalEventId }),
24
+ idGlobal: DbSchema.integer({ primaryKey: true }),
25
+ parentIdGlobal: DbSchema.integer({}),
26
26
  mutation: DbSchema.text({}),
27
27
  args: DbSchema.text({ schema: Schema.parseJson(Schema.Any) }),
28
- /** ISO date format. Currently only used for debugging purposes. */
28
+ /** ISO date format */
29
29
  createdAt: DbSchema.text({}),
30
30
  })
31
31
 
32
- /**
33
- * Needs to be bumped when the storage format changes (e.g. mutationLogTable schema changes)
34
- *
35
- * Changing this version number will lead to a "soft reset".
36
- */
37
- export const PERSISTENCE_FORMAT_VERSION = 2
32
+ // Durable Object
33
+ export class WebSocketServer extends DurableObject<Env> {
34
+ dbName = `mutation_log_${this.ctx.id.toString()}`
35
+ storage = makeStorage(this.ctx, this.env, this.dbName)
38
36
 
39
- export type MakeDurableObjectClassOptions = {
40
- onPush?: (message: WSMessage.PushReq) => Effect.Effect<void> | Promise<void>
41
- onPull?: (message: WSMessage.PullReq) => Effect.Effect<void> | Promise<void>
42
- }
43
-
44
- export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) => {
45
- new (ctx: DurableObjectState, env: Env): DurableObject<Env>
46
- }
47
-
48
- export const makeDurableObject: MakeDurableObjectClass = (options) => {
49
- return class WebSocketServerBase extends DurableObject<Env> {
50
- storage: SyncStorage | undefined = undefined
51
-
52
- constructor(ctx: DurableObjectState, env: Env) {
53
- super(ctx, env)
54
- }
55
-
56
- fetch = async (request: Request) =>
57
- Effect.gen(this, function* () {
58
- if (this.storage === undefined) {
59
- const storeId = getStoreId(request)
60
- const dbName = `mutation_log_${PERSISTENCE_FORMAT_VERSION}_${toValidTableName(storeId)}`
61
- this.storage = makeStorage(this.ctx, this.env, dbName)
62
- }
63
-
64
- const { 0: client, 1: server } = new WebSocketPair()
37
+ constructor(ctx: DurableObjectState, env: Env) {
38
+ super(ctx, env)
39
+ }
65
40
 
66
- // See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
41
+ fetch = async (_request: Request) =>
42
+ Effect.gen(this, function* () {
43
+ const { 0: client, 1: server } = new WebSocketPair()
67
44
 
68
- this.ctx.acceptWebSocket(server)
45
+ // See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
69
46
 
70
- this.ctx.setWebSocketAutoResponse(
71
- new WebSocketRequestResponsePair(
72
- encodeIncomingMessage(WSMessage.Ping.make({ requestId: 'ping' })),
73
- encodeOutgoingMessage(WSMessage.Pong.make({ requestId: 'ping' })),
74
- ),
75
- )
47
+ this.ctx.acceptWebSocket(server)
76
48
 
77
- const colSpec = makeColumnSpec(mutationLogTable.sqliteDef.ast)
78
- this.env.DB.exec(`CREATE TABLE IF NOT EXISTS ${this.storage.dbName} (${colSpec}) strict`)
49
+ this.ctx.setWebSocketAutoResponse(
50
+ new WebSocketRequestResponsePair(
51
+ encodeIncomingMessage(WSMessage.Ping.make({ requestId: 'ping' })),
52
+ encodeOutgoingMessage(WSMessage.Pong.make({ requestId: 'ping' })),
53
+ ),
54
+ )
79
55
 
80
- return new Response(null, {
81
- status: 101,
82
- webSocket: client,
83
- })
84
- }).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
56
+ const colSpec = makeColumnSpec(mutationLogTable.sqliteDef.ast)
57
+ this.env.DB.exec(`CREATE TABLE IF NOT EXISTS ${this.dbName} (${colSpec}) strict`)
85
58
 
86
- webSocketMessage = (ws: WebSocketClient, message: ArrayBuffer | string) =>
87
- Effect.gen(this, function* () {
88
- const decodedMessageRes = decodeIncomingMessage(message)
59
+ return new Response(null, {
60
+ status: 101,
61
+ webSocket: client,
62
+ })
63
+ }).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
89
64
 
90
- if (decodedMessageRes._tag === 'Left') {
91
- console.error('Invalid message received', decodedMessageRes.left)
92
- return
93
- }
65
+ webSocketMessage = (ws: WebSocketClient, message: ArrayBuffer | string) =>
66
+ Effect.gen(this, function* () {
67
+ const decodedMessageRes = decodeIncomingMessage(message)
94
68
 
95
- const decodedMessage = decodedMessageRes.right
96
- const requestId = decodedMessage.requestId
69
+ if (decodedMessageRes._tag === 'Left') {
70
+ console.error('Invalid message received', decodedMessageRes.left)
71
+ return
72
+ }
97
73
 
98
- const storage = this.storage
74
+ const decodedMessage = decodedMessageRes.right
75
+ const requestId = decodedMessage.requestId
99
76
 
100
- if (storage === undefined) {
101
- throw new Error('storage not initialized')
102
- }
103
-
104
- try {
105
- switch (decodedMessage._tag) {
106
- case 'WSMessage.PullReq': {
107
- if (options?.onPull) {
108
- yield* Effect.tryAll(() => options.onPull!(decodedMessage))
109
- }
77
+ try {
78
+ switch (decodedMessage._tag) {
79
+ case 'WSMessage.PullReq': {
80
+ const cursor = decodedMessage.cursor
81
+ const CHUNK_SIZE = 100
110
82
 
111
- const cursor = decodedMessage.cursor
112
- const CHUNK_SIZE = 100
83
+ // TODO use streaming
84
+ const remainingEvents = [...(yield* Effect.promise(() => this.storage.getEvents(cursor)))]
113
85
 
114
- // TODO use streaming
115
- const remainingEvents = [...(yield* Effect.promise(() => storage.getEvents(cursor)))]
86
+ // NOTE we want to make sure the WS server responds at least once with `InitRes` even if `events` is empty
87
+ while (true) {
88
+ const events = remainingEvents.splice(0, CHUNK_SIZE)
116
89
 
117
- // NOTE we want to make sure the WS server responds at least once with `InitRes` even if `events` is empty
118
- while (true) {
119
- const events = remainingEvents.splice(0, CHUNK_SIZE)
120
-
121
- ws.send(
122
- encodeOutgoingMessage(
123
- WSMessage.PullRes.make({ events, remaining: remainingEvents.length, requestId }),
124
- ),
125
- )
90
+ ws.send(
91
+ encodeOutgoingMessage(WSMessage.PullRes.make({ events, remaining: remainingEvents.length, requestId })),
92
+ )
126
93
 
127
- if (remainingEvents.length === 0) {
128
- break
129
- }
94
+ if (remainingEvents.length === 0) {
95
+ break
130
96
  }
131
-
132
- break
133
97
  }
134
- case 'WSMessage.PushReq': {
135
- if (options?.onPush) {
136
- yield* Effect.tryAll(() => options.onPush!(decodedMessage))
137
- }
138
98
 
139
- // TODO check whether we could use the Durable Object storage for this to speed up the lookup
140
- const latestEvent = yield* Effect.promise(() => storage.getLatestEvent())
141
- const expectedParentId = latestEvent?.id ?? EventId.ROOT.global
99
+ break
100
+ }
101
+ case 'WSMessage.PushReq': {
102
+ // TODO check whether we could use the Durable Object storage for this to speed up the lookup
103
+ const latestEvent = yield* Effect.promise(() => this.storage.getLatestEvent())
104
+ const expectedParentId = latestEvent?.id ?? EventId.ROOT
105
+
106
+ let i = 0
107
+ for (const mutationEventEncoded of decodedMessage.batch) {
108
+ if (mutationEventEncoded.parentId.global !== expectedParentId.global + i) {
109
+ const err = WSMessage.Error.make({
110
+ message: `Invalid parent id. Received ${mutationEventEncoded.parentId.global} but expected ${expectedParentId.global}`,
111
+ requestId,
112
+ })
113
+
114
+ yield* Effect.fail(err).pipe(Effect.ignoreLogged)
115
+
116
+ ws.send(encodeOutgoingMessage(err))
117
+ return
118
+ }
142
119
 
143
- let i = 0
144
- for (const mutationEventEncoded of decodedMessage.batch) {
145
- if (mutationEventEncoded.parentId !== expectedParentId + i) {
146
- const err = WSMessage.Error.make({
147
- message: `Invalid parent id. Received ${mutationEventEncoded.parentId} but expected ${expectedParentId}`,
148
- requestId,
149
- })
120
+ // TODO handle clientId unique conflict
150
121
 
151
- yield* Effect.fail(err).pipe(Effect.ignoreLogged)
122
+ const createdAt = new Date().toISOString()
152
123
 
153
- ws.send(encodeOutgoingMessage(err))
154
- return
155
- }
124
+ // NOTE we're currently not blocking on this to allow broadcasting right away
125
+ const storePromise = this.storage.appendEvent(mutationEventEncoded, createdAt)
156
126
 
157
- // TODO handle clientId unique conflict
127
+ ws.send(
128
+ encodeOutgoingMessage(
129
+ WSMessage.PushAck.make({ mutationId: mutationEventEncoded.id.global, requestId }),
130
+ ),
131
+ )
158
132
 
159
- const createdAt = new Date().toISOString()
133
+ // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
160
134
 
161
- // NOTE we're currently not blocking on this to allow broadcasting right away
162
- const storePromise = storage.appendEvent(mutationEventEncoded, createdAt)
135
+ const connectedClients = this.ctx.getWebSockets()
163
136
 
164
- ws.send(
165
- encodeOutgoingMessage(WSMessage.PushAck.make({ mutationId: mutationEventEncoded.id, requestId })),
137
+ if (connectedClients.length > 0) {
138
+ const broadcastMessage = encodeOutgoingMessage(
139
+ // TODO refactor to batch api
140
+ WSMessage.PushBroadcast.make({
141
+ mutationEventEncoded,
142
+ metadata: Option.some({ createdAt }),
143
+ }),
166
144
  )
167
145
 
168
- // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
169
-
170
- const connectedClients = this.ctx.getWebSockets()
171
-
172
- if (connectedClients.length > 0) {
173
- const broadcastMessage = encodeOutgoingMessage(
174
- // TODO refactor to batch api
175
- WSMessage.PushBroadcast.make({
176
- mutationEventEncoded,
177
- metadata: Option.some({ createdAt }),
178
- }),
179
- )
180
-
181
- for (const conn of connectedClients) {
182
- console.log('Broadcasting to client', conn === ws ? 'self' : 'other')
183
- // if (conn !== ws) {
184
- conn.send(broadcastMessage)
185
- // }
186
- }
146
+ for (const conn of connectedClients) {
147
+ console.log('Broadcasting to client', conn === ws ? 'self' : 'other')
148
+ // if (conn !== ws) {
149
+ conn.send(broadcastMessage)
150
+ // }
187
151
  }
152
+ }
188
153
 
189
- yield* Effect.promise(() => storePromise)
154
+ yield* Effect.promise(() => storePromise)
190
155
 
191
- i++
192
- }
156
+ i++
157
+ }
193
158
 
194
- break
159
+ break
160
+ }
161
+ case 'WSMessage.AdminResetRoomReq': {
162
+ if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
163
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
164
+ return
195
165
  }
196
- case 'WSMessage.AdminResetRoomReq': {
197
- if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
198
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
199
- return
200
- }
201
166
 
202
- yield* Effect.promise(() => storage.resetRoom())
203
- ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
167
+ yield* Effect.promise(() => this.storage.resetRoom())
168
+ ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
204
169
 
205
- break
170
+ break
171
+ }
172
+ case 'WSMessage.AdminInfoReq': {
173
+ if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
174
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
175
+ return
206
176
  }
207
- case 'WSMessage.AdminInfoReq': {
208
- if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
209
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
210
- return
211
- }
212
177
 
213
- ws.send(
214
- encodeOutgoingMessage(
215
- WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
216
- ),
217
- )
178
+ ws.send(
179
+ encodeOutgoingMessage(
180
+ WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
181
+ ),
182
+ )
218
183
 
219
- break
220
- }
221
- default: {
222
- console.error('unsupported message', decodedMessage)
223
- return shouldNeverHappen()
224
- }
184
+ break
185
+ }
186
+ default: {
187
+ console.error('unsupported message', decodedMessage)
188
+ return shouldNeverHappen()
225
189
  }
226
- } catch (error: any) {
227
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: error.message, requestId })))
228
190
  }
229
- }).pipe(
230
- Effect.withSpan('@livestore/sync-cf:durable-object:webSocketMessage'),
231
- Effect.tapCauseLogPretty,
232
- Logger.withMinimumLogLevel(LogLevel.Debug),
233
- Effect.provide(Logger.pretty),
234
- Effect.runPromise,
235
- )
191
+ } catch (error: any) {
192
+ ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: error.message, requestId })))
193
+ }
194
+ }).pipe(
195
+ Effect.withSpan('@livestore/sync-cf:durable-object:webSocketMessage'),
196
+ Effect.tapCauseLogPretty,
197
+ Logger.withMinimumLogLevel(LogLevel.Debug),
198
+ Effect.provide(Logger.pretty),
199
+ Effect.runPromise,
200
+ )
236
201
 
237
- webSocketClose = async (ws: WebSocketClient, code: number, _reason: string, _wasClean: boolean) => {
238
- // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
239
- ws.close(code, 'Durable Object is closing WebSocket')
240
- }
202
+ webSocketClose = async (ws: WebSocketClient, code: number, _reason: string, _wasClean: boolean) => {
203
+ // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
204
+ ws.close(code, 'Durable Object is closing WebSocket')
241
205
  }
242
206
  }
243
207
 
244
- type SyncStorage = {
245
- dbName: string
246
- getLatestEvent: () => Promise<MutationEvent.AnyEncodedGlobal | undefined>
247
- getEvents: (
248
- cursor: number | undefined,
249
- ) => Promise<
250
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
251
- >
252
- appendEvent: (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => Promise<void>
253
- resetRoom: () => Promise<void>
254
- }
255
-
256
- const makeStorage = (ctx: DurableObjectState, env: Env, dbName: string): SyncStorage => {
257
- const getLatestEvent = async (): Promise<MutationEvent.AnyEncodedGlobal | undefined> => {
258
- const rawEvents = await env.DB.prepare(`SELECT * FROM ${dbName} ORDER BY id DESC LIMIT 1`).all()
208
+ const makeStorage = (ctx: DurableObjectState, env: Env, dbName: string) => {
209
+ const getLatestEvent = async (): Promise<MutationEvent.Any | undefined> => {
210
+ const rawEvents = await env.DB.prepare(`SELECT * FROM ${dbName} ORDER BY idGlobal DESC LIMIT 1`).all()
259
211
  if (rawEvents.error) {
260
212
  throw new Error(rawEvents.error)
261
213
  }
262
- const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results)
263
-
214
+ const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map((e) => ({
215
+ ...e,
216
+ // TODO remove local ids
217
+ id: { global: e.idGlobal, local: 0 },
218
+ parentId: { global: e.parentIdGlobal, local: 0 },
219
+ }))
264
220
  return events[0]
265
221
  }
266
222
 
267
223
  const getEvents = async (
268
224
  cursor: number | undefined,
269
225
  ): Promise<
270
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
226
+ ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncoded; metadata: Option.Option<SyncMetadata> }>
271
227
  > => {
272
- const whereClause = cursor === undefined ? '' : `WHERE id > ${cursor}`
273
- const sql = `SELECT * FROM ${dbName} ${whereClause} ORDER BY id ASC`
228
+ const whereClause = cursor === undefined ? '' : `WHERE idGlobal > ${cursor}`
229
+ const sql = `SELECT * FROM ${dbName} ${whereClause} ORDER BY idGlobal ASC`
274
230
  // TODO handle case where `cursor` was not found
275
231
  const rawEvents = await env.DB.prepare(sql).all()
276
232
  if (rawEvents.error) {
277
233
  throw new Error(rawEvents.error)
278
234
  }
279
235
  const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map(
280
- ({ createdAt, ...mutationEventEncoded }) => ({
281
- mutationEventEncoded,
236
+ ({ createdAt, ...e }) => ({
237
+ mutationEventEncoded: {
238
+ ...e,
239
+ // TODO remove local ids
240
+ id: { global: e.idGlobal, local: 0 },
241
+ parentId: { global: e.parentIdGlobal, local: 0 },
242
+ },
282
243
  metadata: Option.some({ createdAt }),
283
244
  }),
284
245
  )
285
246
  return events
286
247
  }
287
248
 
288
- const appendEvent = async (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => {
289
- const sql = `INSERT INTO ${dbName} (id, parentId, args, mutation, createdAt) VALUES (?, ?, ?, ?, ?)`
249
+ const appendEvent = async (event: MutationEvent.Any, createdAt: string) => {
250
+ const sql = `INSERT INTO ${dbName} (idGlobal, parentIdGlobal, args, mutation, createdAt) VALUES (?, ?, ?, ?, ?)`
290
251
  await env.DB.prepare(sql)
291
- .bind(event.id, event.parentId, JSON.stringify(event.args), event.mutation, createdAt)
252
+ .bind(event.id.global, event.parentId.global, JSON.stringify(event.args), event.mutation, createdAt)
292
253
  .run()
293
254
  }
294
255
 
@@ -296,17 +257,5 @@ const makeStorage = (ctx: DurableObjectState, env: Env, dbName: string): SyncSto
296
257
  await ctx.storage.deleteAll()
297
258
  }
298
259
 
299
- return { dbName, getLatestEvent, getEvents, appendEvent, resetRoom }
260
+ return { getLatestEvent, getEvents, appendEvent, resetRoom }
300
261
  }
301
-
302
- const getStoreId = (request: Request) => {
303
- const url = new URL(request.url)
304
- const searchParams = url.searchParams
305
- const storeId = searchParams.get('storeId')
306
- if (storeId === null) {
307
- throw new Error('storeId search param is required')
308
- }
309
- return storeId
310
- }
311
-
312
- const toValidTableName = (str: string) => str.replaceAll(/[^a-zA-Z0-9]/g, '_')
@@ -0,0 +1,84 @@
1
+ /// <reference no-default-lib="true"/>
2
+ /// <reference lib="esnext" />
3
+
4
+ // import { EncodedAny } from '@livestore/common/schema'
5
+ // import { Effect, HttpServer, Schema } from '@livestore/utils/effect'
6
+
7
+ import type { Env } from './durable-object.js'
8
+
9
+ export * from './durable-object.js'
10
+
11
+ // const handleRequest = (request: Request, env: Env) =>
12
+ // HttpServer.router.empty.pipe(
13
+ // HttpServer.router.get(
14
+ // '/websocket',
15
+ // Effect.gen(function* () {
16
+ // // This example will refer to the same Durable Object instance,
17
+ // // since the name "foo" is hardcoded.
18
+ // const id = env.WEBSOCKET_SERVER.idFromName('foo')
19
+ // const durableObject = env.WEBSOCKET_SERVER.get(id)
20
+
21
+ // HttpServer.
22
+
23
+ // // Expect to receive a WebSocket Upgrade request.
24
+ // // If there is one, accept the request and return a WebSocket Response.
25
+ // const headerRes = yield* HttpServer.request
26
+ // .schemaHeaders(
27
+ // Schema.Struct({
28
+ // Upgrade: Schema.Literal('websocket'),
29
+ // }),
30
+ // )
31
+ // .pipe(Effect.either)
32
+
33
+ // if (headerRes._tag === 'Left') {
34
+ // // return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
35
+ // return yield* HttpServer.response.text('Durable Object expected Upgrade: websocket', { status: 426 })
36
+ // }
37
+
38
+ // HttpServer.response.empty
39
+
40
+ // return yield* Effect.promise(() => durableObject.fetch(request))
41
+ // }),
42
+ // ),
43
+ // HttpServer.router.catchAll((e) => {
44
+ // console.log(e)
45
+ // return HttpServer.response.empty({ status: 400 })
46
+ // }),
47
+ // (_) => HttpServer.app.toWebHandler(_)(request),
48
+ // // request
49
+ // )
50
+
51
+ // Worker
52
+ export default {
53
+ fetch: async (request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> => {
54
+ const url = new URL(request.url)
55
+ const searchParams = url.searchParams
56
+ const roomId = searchParams.get('room')
57
+
58
+ if (roomId === null) {
59
+ return new Response('Room ID is required', { status: 400 })
60
+ }
61
+
62
+ // This example will refer to the same Durable Object instance,
63
+ // since the name "foo" is hardcoded.
64
+ const id = env.WEBSOCKET_SERVER.idFromName(roomId)
65
+ const durableObject = env.WEBSOCKET_SERVER.get(id)
66
+
67
+ if (url.pathname.endsWith('/websocket')) {
68
+ const upgradeHeader = request.headers.get('Upgrade')
69
+ if (!upgradeHeader || upgradeHeader !== 'websocket') {
70
+ return new Response('Durable Object expected Upgrade: websocket', { status: 426 })
71
+ }
72
+
73
+ return durableObject.fetch(request)
74
+ }
75
+
76
+ return new Response(null, {
77
+ status: 400,
78
+ statusText: 'Bad Request',
79
+ headers: {
80
+ 'Content-Type': 'text/plain',
81
+ },
82
+ })
83
+ },
84
+ }
@@ -20,7 +20,7 @@ export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
20
20
  requestId: Schema.String,
21
21
  events: Schema.Array(
22
22
  Schema.Struct({
23
- mutationEventEncoded: MutationEvent.AnyEncodedGlobal,
23
+ mutationEventEncoded: MutationEvent.EncodedAny,
24
24
  metadata: Schema.Option(SyncMetadata),
25
25
  }),
26
26
  ),
@@ -30,7 +30,7 @@ export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
30
30
  export type PullRes = typeof PullRes.Type
31
31
 
32
32
  export const PushBroadcast = Schema.TaggedStruct('WSMessage.PushBroadcast', {
33
- mutationEventEncoded: MutationEvent.AnyEncodedGlobal,
33
+ mutationEventEncoded: MutationEvent.EncodedAny,
34
34
  metadata: Schema.Option(SyncMetadata),
35
35
  })
36
36
 
@@ -38,7 +38,7 @@ export type PushBroadcast = typeof PushBroadcast.Type
38
38
 
39
39
  export const PushReq = Schema.TaggedStruct('WSMessage.PushReq', {
40
40
  requestId: Schema.String,
41
- batch: Schema.Array(MutationEvent.AnyEncodedGlobal),
41
+ batch: Schema.Array(MutationEvent.EncodedAny),
42
42
  })
43
43
 
44
44
  export type PushReq = typeof PushReq.Type
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
2
 
3
- import type { SyncBackend } from '@livestore/common'
3
+ import type { SyncBackend, SyncBackendOptionsBase } from '@livestore/common'
4
4
  import { InvalidPullError, InvalidPushError } from '@livestore/common'
5
5
  import { pick } from '@livestore/utils'
6
6
  import type { Scope } from '@livestore/utils/effect'
@@ -18,18 +18,26 @@ import {
18
18
  } from '@livestore/utils/effect'
19
19
  import { nanoid } from '@livestore/utils/nanoid'
20
20
 
21
- import { WSMessage } from '../common/mod.js'
21
+ import { WSMessage } from '../common/index.js'
22
22
  import type { SyncMetadata } from '../common/ws-message-types.js'
23
23
 
24
- export interface WsSyncOptions {
24
+ export interface WsSyncOptions extends SyncBackendOptionsBase {
25
+ type: 'cf'
25
26
  url: string
26
- storeId: string
27
+ roomId: string
28
+ }
29
+
30
+ interface LiveStoreGlobalCf {
31
+ syncBackend: WsSyncOptions
32
+ }
33
+
34
+ declare global {
35
+ interface LiveStoreGlobal extends LiveStoreGlobalCf {}
27
36
  }
28
37
 
29
38
  export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<SyncMetadata>, never, Scope.Scope> =>
30
39
  Effect.gen(function* () {
31
- // TODO also allow for auth scenarios
32
- const wsUrl = `${options.url}/websocket?storeId=${options.storeId}`
40
+ const wsUrl = `${options.url}/websocket?room=${options.roomId}`
33
41
 
34
42
  const { isConnected, incomingMessages, send } = yield* connect(wsUrl)
35
43
 
@@ -119,8 +127,8 @@ const connect = (wsUrl: string) =>
119
127
  `Sending message: ${message._tag}`,
120
128
  message._tag === 'WSMessage.PushReq'
121
129
  ? {
122
- id: message.batch[0]!.id,
123
- parentId: message.batch[0]!.parentId,
130
+ id: message.batch[0]!.id.global,
131
+ parentId: message.batch[0]!.parentId.global,
124
132
  batchLength: message.batch.length,
125
133
  }
126
134
  : message._tag === 'WSMessage.PullReq'
@@ -1,6 +0,0 @@
1
- import type { Env } from './durable-object.js';
2
- export type CFWorker = {
3
- fetch: (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response>;
4
- };
5
- export declare const makeWorker: () => CFWorker;
6
- //# sourceMappingURL=make-worker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"make-worker.d.ts","sourceRoot":"","sources":["../../src/cf-worker/make-worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AAE9C,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAChF,CAAA;AAED,eAAO,MAAM,UAAU,QAAO,QAkC7B,CAAA"}