@livestore/sync-cf 0.3.0-dev.11 → 0.3.0-dev.3

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 +129 -150
  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 -215
  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,277 +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
- const WebSocketAttachmentSchema = Schema.parseJson(
33
- Schema.Struct({
34
- storeId: Schema.String,
35
- }),
36
- )
37
-
38
- /**
39
- * Needs to be bumped when the storage format changes (e.g. mutationLogTable schema changes)
40
- *
41
- * Changing this version number will lead to a "soft reset".
42
- */
43
- export const PERSISTENCE_FORMAT_VERSION = 2
44
-
45
- export type MakeDurableObjectClassOptions = {
46
- onPush?: (message: WSMessage.PushReq) => Effect.Effect<void> | Promise<void>
47
- onPull?: (message: WSMessage.PullReq) => Effect.Effect<void> | Promise<void>
48
- }
49
-
50
- export type MakeDurableObjectClass = (options?: MakeDurableObjectClassOptions) => {
51
- new (ctx: DurableObjectState, env: Env): DurableObject<Env>
52
- }
53
-
54
- export const makeDurableObject: MakeDurableObjectClass = (options) => {
55
- return class WebSocketServerBase extends DurableObject<Env> {
56
- constructor(ctx: DurableObjectState, env: Env) {
57
- super(ctx, env)
58
- }
59
-
60
- fetch = async (request: Request) =>
61
- Effect.gen(this, function* () {
62
- const storeId = getStoreId(request)
63
- const storage = makeStorage(this.ctx, this.env, storeId)
64
-
65
- const { 0: client, 1: server } = new WebSocketPair()
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)
66
36
 
67
- // Since we're using websocket hibernation, we need to remember the storeId for subsequent `webSocketMessage` calls
68
- server.serializeAttachment(Schema.encodeSync(WebSocketAttachmentSchema)({ storeId }))
37
+ constructor(ctx: DurableObjectState, env: Env) {
38
+ super(ctx, env)
39
+ }
69
40
 
70
- // 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()
71
44
 
72
- this.ctx.acceptWebSocket(server)
45
+ // See https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server
73
46
 
74
- this.ctx.setWebSocketAutoResponse(
75
- new WebSocketRequestResponsePair(
76
- encodeIncomingMessage(WSMessage.Ping.make({ requestId: 'ping' })),
77
- encodeOutgoingMessage(WSMessage.Pong.make({ requestId: 'ping' })),
78
- ),
79
- )
47
+ this.ctx.acceptWebSocket(server)
80
48
 
81
- const colSpec = makeColumnSpec(mutationLogTable.sqliteDef.ast)
82
- this.env.DB.exec(`CREATE TABLE IF NOT EXISTS ${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
+ )
83
55
 
84
- return new Response(null, {
85
- status: 101,
86
- webSocket: client,
87
- })
88
- }).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`)
89
58
 
90
- webSocketMessage = (ws: WebSocketClient, message: ArrayBuffer | string) =>
91
- Effect.gen(this, function* () {
92
- const decodedMessageRes = decodeIncomingMessage(message)
59
+ return new Response(null, {
60
+ status: 101,
61
+ webSocket: client,
62
+ })
63
+ }).pipe(Effect.tapCauseLogPretty, Effect.runPromise)
93
64
 
94
- if (decodedMessageRes._tag === 'Left') {
95
- console.error('Invalid message received', decodedMessageRes.left)
96
- return
97
- }
65
+ webSocketMessage = (ws: WebSocketClient, message: ArrayBuffer | string) =>
66
+ Effect.gen(this, function* () {
67
+ const decodedMessageRes = decodeIncomingMessage(message)
98
68
 
99
- const { storeId } = yield* Schema.decode(WebSocketAttachmentSchema)(ws.deserializeAttachment())
100
- const storage = makeStorage(this.ctx, this.env, storeId)
69
+ if (decodedMessageRes._tag === 'Left') {
70
+ console.error('Invalid message received', decodedMessageRes.left)
71
+ return
72
+ }
101
73
 
102
- const decodedMessage = decodedMessageRes.right
103
- const requestId = decodedMessage.requestId
74
+ const decodedMessage = decodedMessageRes.right
75
+ const requestId = decodedMessage.requestId
104
76
 
105
- try {
106
- switch (decodedMessage._tag) {
107
- case 'WSMessage.PullReq': {
108
- if (options?.onPull) {
109
- yield* Effect.tryAll(() => options.onPull!(decodedMessage))
110
- }
77
+ try {
78
+ switch (decodedMessage._tag) {
79
+ case 'WSMessage.PullReq': {
80
+ const cursor = decodedMessage.cursor
81
+ const CHUNK_SIZE = 100
111
82
 
112
- const cursor = decodedMessage.cursor
113
- const CHUNK_SIZE = 100
83
+ // TODO use streaming
84
+ const remainingEvents = [...(yield* Effect.promise(() => this.storage.getEvents(cursor)))]
114
85
 
115
- // TODO use streaming
116
- 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)
117
89
 
118
- // NOTE we want to make sure the WS server responds at least once with `InitRes` even if `events` is empty
119
- while (true) {
120
- const events = remainingEvents.splice(0, CHUNK_SIZE)
121
-
122
- ws.send(
123
- encodeOutgoingMessage(
124
- WSMessage.PullRes.make({ events, remaining: remainingEvents.length, requestId }),
125
- ),
126
- )
90
+ ws.send(
91
+ encodeOutgoingMessage(WSMessage.PullRes.make({ events, remaining: remainingEvents.length, requestId })),
92
+ )
127
93
 
128
- if (remainingEvents.length === 0) {
129
- break
130
- }
94
+ if (remainingEvents.length === 0) {
95
+ break
131
96
  }
132
-
133
- break
134
97
  }
135
- case 'WSMessage.PushReq': {
136
- if (options?.onPush) {
137
- yield* Effect.tryAll(() => options.onPush!(decodedMessage))
98
+
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
138
118
  }
139
119
 
140
- // TODO check whether we could use the Durable Object storage for this to speed up the lookup
141
- const latestEvent = yield* Effect.promise(() => storage.getLatestEvent())
142
- const expectedParentId = latestEvent?.id ?? EventId.ROOT.global
120
+ // TODO handle clientId unique conflict
143
121
 
144
- let i = 0
145
- for (const mutationEventEncoded of decodedMessage.batch) {
146
- if (mutationEventEncoded.parentId !== expectedParentId + i) {
147
- const err = WSMessage.Error.make({
148
- message: `Invalid parent id. Received ${mutationEventEncoded.parentId} but expected ${expectedParentId}`,
149
- requestId,
150
- })
122
+ const createdAt = new Date().toISOString()
151
123
 
152
- yield* Effect.fail(err).pipe(Effect.ignoreLogged)
124
+ // NOTE we're currently not blocking on this to allow broadcasting right away
125
+ const storePromise = this.storage.appendEvent(mutationEventEncoded, createdAt)
153
126
 
154
- ws.send(encodeOutgoingMessage(err))
155
- return
156
- }
157
-
158
- // TODO handle clientId unique conflict
127
+ ws.send(
128
+ encodeOutgoingMessage(
129
+ WSMessage.PushAck.make({ mutationId: mutationEventEncoded.id.global, requestId }),
130
+ ),
131
+ )
159
132
 
160
- const createdAt = new Date().toISOString()
133
+ // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
161
134
 
162
- // NOTE we're currently not blocking on this to allow broadcasting right away
163
- const storePromise = storage.appendEvent(mutationEventEncoded, createdAt)
135
+ const connectedClients = this.ctx.getWebSockets()
164
136
 
165
- ws.send(
166
- 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
+ }),
167
144
  )
168
145
 
169
- // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
170
-
171
- const connectedClients = this.ctx.getWebSockets()
172
-
173
- if (connectedClients.length > 0) {
174
- const broadcastMessage = encodeOutgoingMessage(
175
- // TODO refactor to batch api
176
- WSMessage.PushBroadcast.make({
177
- mutationEventEncoded,
178
- metadata: Option.some({ createdAt }),
179
- }),
180
- )
181
-
182
- for (const conn of connectedClients) {
183
- console.log('Broadcasting to client', conn === ws ? 'self' : 'other')
184
- // if (conn !== ws) {
185
- conn.send(broadcastMessage)
186
- // }
187
- }
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
+ // }
188
151
  }
152
+ }
189
153
 
190
- yield* Effect.promise(() => storePromise)
154
+ yield* Effect.promise(() => storePromise)
191
155
 
192
- i++
193
- }
156
+ i++
157
+ }
194
158
 
195
- 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
196
165
  }
197
- case 'WSMessage.AdminResetRoomReq': {
198
- if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
199
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
200
- return
201
- }
202
166
 
203
- yield* Effect.promise(() => storage.resetRoom())
204
- ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
167
+ yield* Effect.promise(() => this.storage.resetRoom())
168
+ ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
205
169
 
206
- 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
207
176
  }
208
- case 'WSMessage.AdminInfoReq': {
209
- if (decodedMessage.adminSecret !== this.env.ADMIN_SECRET) {
210
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: 'Invalid admin secret', requestId })))
211
- return
212
- }
213
177
 
214
- ws.send(
215
- encodeOutgoingMessage(
216
- WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
217
- ),
218
- )
178
+ ws.send(
179
+ encodeOutgoingMessage(
180
+ WSMessage.AdminInfoRes.make({ requestId, info: { durableObjectId: this.ctx.id.toString() } }),
181
+ ),
182
+ )
219
183
 
220
- break
221
- }
222
- default: {
223
- console.error('unsupported message', decodedMessage)
224
- return shouldNeverHappen()
225
- }
184
+ break
185
+ }
186
+ default: {
187
+ console.error('unsupported message', decodedMessage)
188
+ return shouldNeverHappen()
226
189
  }
227
- } catch (error: any) {
228
- ws.send(encodeOutgoingMessage(WSMessage.Error.make({ message: error.message, requestId })))
229
190
  }
230
- }).pipe(
231
- Effect.withSpan('@livestore/sync-cf:durable-object:webSocketMessage'),
232
- Effect.tapCauseLogPretty,
233
- Logger.withMinimumLogLevel(LogLevel.Debug),
234
- Effect.provide(Logger.pretty),
235
- Effect.runPromise,
236
- )
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
+ )
237
201
 
238
- webSocketClose = async (ws: WebSocketClient, code: number, _reason: string, _wasClean: boolean) => {
239
- // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
240
- ws.close(code, 'Durable Object is closing WebSocket')
241
- }
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')
242
205
  }
243
206
  }
244
207
 
245
- type SyncStorage = {
246
- dbName: string
247
- getLatestEvent: () => Promise<MutationEvent.AnyEncodedGlobal | undefined>
248
- getEvents: (
249
- cursor: number | undefined,
250
- ) => Promise<
251
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
252
- >
253
- appendEvent: (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => Promise<void>
254
- resetRoom: () => Promise<void>
255
- }
256
-
257
- const makeStorage = (ctx: DurableObjectState, env: Env, storeId: string): SyncStorage => {
258
- const dbName = `mutation_log_${PERSISTENCE_FORMAT_VERSION}_${toValidTableName(storeId)}`
259
-
260
- const getLatestEvent = async (): Promise<MutationEvent.AnyEncodedGlobal | undefined> => {
261
- 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()
262
211
  if (rawEvents.error) {
263
212
  throw new Error(rawEvents.error)
264
213
  }
265
- const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results)
266
-
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
+ }))
267
220
  return events[0]
268
221
  }
269
222
 
270
223
  const getEvents = async (
271
224
  cursor: number | undefined,
272
225
  ): Promise<
273
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
226
+ ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncoded; metadata: Option.Option<SyncMetadata> }>
274
227
  > => {
275
- const whereClause = cursor === undefined ? '' : `WHERE id > ${cursor}`
276
- 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`
277
230
  // TODO handle case where `cursor` was not found
278
231
  const rawEvents = await env.DB.prepare(sql).all()
279
232
  if (rawEvents.error) {
280
233
  throw new Error(rawEvents.error)
281
234
  }
282
235
  const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map(
283
- ({ createdAt, ...mutationEventEncoded }) => ({
284
- 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
+ },
285
243
  metadata: Option.some({ createdAt }),
286
244
  }),
287
245
  )
288
246
  return events
289
247
  }
290
248
 
291
- const appendEvent = async (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => {
292
- 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 (?, ?, ?, ?, ?)`
293
251
  await env.DB.prepare(sql)
294
- .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)
295
253
  .run()
296
254
  }
297
255
 
@@ -299,17 +257,5 @@ const makeStorage = (ctx: DurableObjectState, env: Env, storeId: string): SyncSt
299
257
  await ctx.storage.deleteAll()
300
258
  }
301
259
 
302
- return { dbName, getLatestEvent, getEvents, appendEvent, resetRoom }
303
- }
304
-
305
- const getStoreId = (request: Request) => {
306
- const url = new URL(request.url)
307
- const searchParams = url.searchParams
308
- const storeId = searchParams.get('storeId')
309
- if (storeId === null) {
310
- throw new Error('storeId search param is required')
311
- }
312
- return storeId
260
+ return { getLatestEvent, getEvents, appendEvent, resetRoom }
313
261
  }
314
-
315
- 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"}