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

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.
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom" />
2
2
  import { InvalidPullError, InvalidPushError } from '@livestore/common';
3
- import { pick } from '@livestore/utils';
3
+ import { LS_DEV } from '@livestore/utils';
4
4
  import { Deferred, Effect, Option, PubSub, Queue, Schedule, Schema, Stream, SubscriptionRef, WebSocket, } from '@livestore/utils/effect';
5
5
  import { nanoid } from '@livestore/utils/nanoid';
6
6
  import { WSMessage } from '../common/mod.js';
@@ -14,27 +14,18 @@ export const makeWsSync = (options) => Effect.gen(function* () {
14
14
  const requestId = nanoid();
15
15
  const cursor = Option.getOrUndefined(args)?.cursor.global;
16
16
  yield* send(WSMessage.PullReq.make({ cursor, requestId }));
17
- return Stream.fromPubSub(incomingMessages).pipe(Stream.filter((_) => (_._tag === 'WSMessage.PullRes' ? _.requestId === requestId : true)), Stream.tap((_) => _._tag === 'WSMessage.Error' && _.requestId === requestId
17
+ return Stream.fromPubSub(incomingMessages).pipe(Stream.tap((_) => _._tag === 'WSMessage.Error' && _.requestId === requestId
18
18
  ? new InvalidPullError({ message: _.message })
19
- : Effect.void), Stream.filter(Schema.is(Schema.Union(WSMessage.PushBroadcast, WSMessage.PullRes))), Stream.map((msg) => msg._tag === 'WSMessage.PushBroadcast'
20
- ? { batch: [pick(msg, ['mutationEventEncoded', 'metadata'])], remaining: 0 }
21
- : {
22
- batch: msg.events.map(({ mutationEventEncoded, metadata }) => ({
23
- mutationEventEncoded,
24
- metadata,
25
- })),
26
- remaining: msg.remaining,
27
- }));
19
+ : Effect.void),
20
+ // This call is mostly here to for type narrowing
21
+ Stream.filter(Schema.is(WSMessage.PullRes)));
28
22
  }).pipe(Stream.unwrap),
29
23
  push: (batch) => Effect.gen(function* () {
30
24
  const ready = yield* Deferred.make();
31
25
  const requestId = nanoid();
32
- yield* Stream.fromPubSub(incomingMessages).pipe(Stream.filter((_) => _._tag !== 'WSMessage.PushBroadcast' && _.requestId === requestId), Stream.tap((_) => _._tag === 'WSMessage.Error'
26
+ yield* Stream.fromPubSub(incomingMessages).pipe(Stream.tap((_) => _._tag === 'WSMessage.Error' && _.requestId === requestId
33
27
  ? Deferred.fail(ready, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
34
- : Effect.void), Stream.filter(Schema.is(WSMessage.PushAck)),
35
- // TODO bring back filterting of "own events"
36
- // Stream.filter((_) => _.mutationId === mutationEventEncoded.id.global),
37
- Stream.take(1), Stream.tap(() => Deferred.succeed(ready, void 0)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.fork);
28
+ : Effect.void), Stream.filter((_) => _._tag === 'WSMessage.PushAck' && _.requestId === requestId), Stream.take(1), Stream.tap(() => Deferred.succeed(ready, void 0)), Stream.runDrain, Effect.tapCauseLogPretty, Effect.fork);
38
29
  yield* send(WSMessage.PushReq.make({ batch, requestId }));
39
30
  yield* ready;
40
31
  const createdAt = new Date().toISOString();
@@ -51,17 +42,19 @@ const connect = (wsUrl) => Effect.gen(function* () {
51
42
  const send = (message) => Effect.gen(function* () {
52
43
  // Wait first until we're online
53
44
  yield* waitUntilOnline;
54
- yield* Effect.spanEvent(`Sending message: ${message._tag}`, message._tag === 'WSMessage.PushReq'
55
- ? {
56
- id: message.batch[0].id,
57
- parentId: message.batch[0].parentId,
58
- batchLength: message.batch.length,
59
- }
60
- : message._tag === 'WSMessage.PullReq'
61
- ? { cursor: message.cursor ?? '-' }
62
- : {});
63
45
  // TODO use MsgPack instead of JSON to speed up the serialization / reduce the size of the messages
64
46
  socketRef.current.send(Schema.encodeSync(Schema.parseJson(WSMessage.Message))(message));
47
+ if (LS_DEV) {
48
+ yield* Effect.spanEvent(`Sent message: ${message._tag}`, message._tag === 'WSMessage.PushReq'
49
+ ? {
50
+ id: message.batch[0].id,
51
+ parentId: message.batch[0].parentId,
52
+ batchLength: message.batch.length,
53
+ }
54
+ : message._tag === 'WSMessage.PullReq'
55
+ ? { cursor: message.cursor ?? '-' }
56
+ : {});
57
+ }
65
58
  });
66
59
  const innerConnect = Effect.gen(function* () {
67
60
  // If the browser already tells us we're offline, then we'll at least wait until the browser
@@ -69,6 +62,7 @@ const connect = (wsUrl) => Effect.gen(function* () {
69
62
  while (typeof navigator !== 'undefined' && navigator.onLine === false) {
70
63
  yield* Effect.sleep(1000);
71
64
  }
65
+ // TODO bring this back in a cross-platform way
72
66
  // if (navigator.onLine === false) {
73
67
  // yield* Effect.async((cb) => self.addEventListener('online', () => cb(Effect.void)))
74
68
  // }
@@ -1 +1 @@
1
- {"version":3,"file":"ws-impl.js","sourceRoot":"","sources":["../../src/sync-impl/ws-impl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-impl.js","sourceRoot":"","sources":["../../src/sync-impl/ws-impl.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/sync-cf",
3
- "version": "0.3.0-dev.11",
3
+ "version": "0.3.0-dev.13",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@livestore/common": "0.3.0-dev.11",
18
- "@livestore/utils": "0.3.0-dev.11"
17
+ "@livestore/common": "0.3.0-dev.13",
18
+ "@livestore/utils": "0.3.0-dev.13"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@cloudflare/workers-types": "^4.20241022.0"
@@ -1,4 +1,4 @@
1
- import { makeColumnSpec } from '@livestore/common'
1
+ import { makeColumnSpec, UnexpectedError } from '@livestore/common'
2
2
  import { DbSchema, EventId, type MutationEvent } from '@livestore/common/schema'
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
4
  import { Effect, Logger, LogLevel, Option, Schema } from '@livestore/utils/effect'
@@ -19,14 +19,15 @@ const encodeOutgoingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.Backe
19
19
  const encodeIncomingMessage = Schema.encodeSync(Schema.parseJson(WSMessage.ClientToBackendMessage))
20
20
  const decodeIncomingMessage = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.ClientToBackendMessage))
21
21
 
22
- // NOTE actual table name is determined at runtime by `WebSocketServer.dbName`
23
- export const mutationLogTable = DbSchema.table('__unused', {
22
+ // NOTE actual table name is determined at runtime
23
+ export const mutationLogTable = DbSchema.table('mutation_log_${PERSISTENCE_FORMAT_VERSION}_${storeId}', {
24
24
  id: DbSchema.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
25
25
  parentId: DbSchema.integer({ schema: EventId.GlobalEventId }),
26
26
  mutation: DbSchema.text({}),
27
27
  args: DbSchema.text({ schema: Schema.parseJson(Schema.Any) }),
28
28
  /** ISO date format. Currently only used for debugging purposes. */
29
29
  createdAt: DbSchema.text({}),
30
+ clientId: DbSchema.text({}),
30
31
  })
31
32
 
32
33
  const WebSocketAttachmentSchema = Schema.parseJson(
@@ -40,7 +41,7 @@ const WebSocketAttachmentSchema = Schema.parseJson(
40
41
  *
41
42
  * Changing this version number will lead to a "soft reset".
42
43
  */
43
- export const PERSISTENCE_FORMAT_VERSION = 2
44
+ export const PERSISTENCE_FORMAT_VERSION = 3
44
45
 
45
46
  export type MakeDurableObjectClassOptions = {
46
47
  onPush?: (message: WSMessage.PushReq) => Effect.Effect<void> | Promise<void>
@@ -104,6 +105,7 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
104
105
 
105
106
  try {
106
107
  switch (decodedMessage._tag) {
108
+ // TODO allow pulling concurrently to not block incoming push requests
107
109
  case 'WSMessage.PullReq': {
108
110
  if (options?.onPull) {
109
111
  yield* Effect.tryAll(() => options.onPull!(decodedMessage))
@@ -113,21 +115,19 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
113
115
  const CHUNK_SIZE = 100
114
116
 
115
117
  // TODO use streaming
116
- const remainingEvents = [...(yield* Effect.promise(() => storage.getEvents(cursor)))]
117
-
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
- )
127
-
128
- if (remainingEvents.length === 0) {
129
- break
130
- }
118
+ const remainingEvents = yield* storage.getEvents(cursor)
119
+
120
+ // Send at least one response, even if there are no events
121
+ const batches =
122
+ remainingEvents.length === 0
123
+ ? [[]]
124
+ : Array.from({ length: Math.ceil(remainingEvents.length / CHUNK_SIZE) }, (_, i) =>
125
+ remainingEvents.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
126
+ )
127
+
128
+ for (const [index, batch] of batches.entries()) {
129
+ const remaining = Math.max(0, remainingEvents.length - (index + 1) * CHUNK_SIZE)
130
+ ws.send(encodeOutgoingMessage(WSMessage.PullRes.make({ batch, remaining })))
131
131
  }
132
132
 
133
133
  break
@@ -137,61 +137,63 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
137
137
  yield* Effect.tryAll(() => options.onPush!(decodedMessage))
138
138
  }
139
139
 
140
+ if (decodedMessage.batch.length === 0) {
141
+ ws.send(encodeOutgoingMessage(WSMessage.PushAck.make({ requestId })))
142
+ return
143
+ }
144
+
140
145
  // 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
146
+ const expectedParentId = yield* storage.getHead
143
147
 
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
- })
148
+ // TODO handle clientId unique conflict
151
149
 
152
- yield* Effect.fail(err).pipe(Effect.ignoreLogged)
150
+ // Validate the batch
151
+ const firstEvent = decodedMessage.batch[0]!
152
+ if (firstEvent.parentId !== expectedParentId) {
153
+ const err = WSMessage.Error.make({
154
+ message: `Invalid parent id. Received ${firstEvent.parentId} but expected ${expectedParentId}`,
155
+ requestId,
156
+ })
153
157
 
154
- ws.send(encodeOutgoingMessage(err))
155
- return
156
- }
158
+ yield* Effect.logError(err)
157
159
 
158
- // TODO handle clientId unique conflict
160
+ ws.send(encodeOutgoingMessage(err))
161
+ return
162
+ }
159
163
 
160
- const createdAt = new Date().toISOString()
164
+ ws.send(encodeOutgoingMessage(WSMessage.PushAck.make({ requestId })))
161
165
 
162
- // NOTE we're currently not blocking on this to allow broadcasting right away
163
- const storePromise = storage.appendEvent(mutationEventEncoded, createdAt)
166
+ const createdAt = new Date().toISOString()
164
167
 
165
- ws.send(
166
- encodeOutgoingMessage(WSMessage.PushAck.make({ mutationId: mutationEventEncoded.id, requestId })),
167
- )
168
+ // NOTE we're not waiting for this to complete yet to allow the broadcast to happen right away
169
+ // while letting the async storage write happen in the background
170
+ const storeFiber = yield* storage.appendEvents(decodedMessage.batch, createdAt).pipe(Effect.fork)
168
171
 
169
- // console.debug(`Broadcasting mutation event to ${this.subscribedWebSockets.size} clients`)
172
+ const connectedClients = this.ctx.getWebSockets()
170
173
 
171
- const connectedClients = this.ctx.getWebSockets()
174
+ // console.debug(`Broadcasting push batch to ${this.subscribedWebSockets.size} clients`)
172
175
 
173
- if (connectedClients.length > 0) {
174
- const broadcastMessage = encodeOutgoingMessage(
175
- // TODO refactor to batch api
176
- WSMessage.PushBroadcast.make({
176
+ if (connectedClients.length > 0) {
177
+ const pullRes = encodeOutgoingMessage(
178
+ // TODO refactor to batch api
179
+ WSMessage.PullRes.make({
180
+ batch: decodedMessage.batch.map((mutationEventEncoded) => ({
177
181
  mutationEventEncoded,
178
182
  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
- }
188
- }
189
-
190
- yield* Effect.promise(() => storePromise)
183
+ })),
184
+ remaining: 0,
185
+ }),
186
+ )
191
187
 
192
- i++
188
+ // NOTE we're also sending the pullRes to the pushing ws client as a confirmation
189
+ for (const conn of connectedClients) {
190
+ conn.send(pullRes)
191
+ }
193
192
  }
194
193
 
194
+ // Wait for the storage write to complete before finishing this request
195
+ yield* storeFiber
196
+
195
197
  break
196
198
  }
197
199
  case 'WSMessage.AdminResetRoomReq': {
@@ -200,7 +202,7 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
200
202
  return
201
203
  }
202
204
 
203
- yield* Effect.promise(() => storage.resetRoom())
205
+ yield* storage.resetStore
204
206
  ws.send(encodeOutgoingMessage(WSMessage.AdminResetRoomRes.make({ requestId })))
205
207
 
206
208
  break
@@ -244,62 +246,97 @@ export const makeDurableObject: MakeDurableObjectClass = (options) => {
244
246
 
245
247
  type SyncStorage = {
246
248
  dbName: string
247
- getLatestEvent: () => Promise<MutationEvent.AnyEncodedGlobal | undefined>
249
+ getHead: Effect.Effect<EventId.GlobalEventId, UnexpectedError>
248
250
  getEvents: (
249
251
  cursor: number | undefined,
250
- ) => Promise<
251
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
252
+ ) => Effect.Effect<
253
+ ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>,
254
+ UnexpectedError
252
255
  >
253
- appendEvent: (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => Promise<void>
254
- resetRoom: () => Promise<void>
256
+ appendEvents: (
257
+ batch: ReadonlyArray<MutationEvent.AnyEncodedGlobal>,
258
+ createdAt: string,
259
+ ) => Effect.Effect<void, UnexpectedError>
260
+ resetStore: Effect.Effect<void, UnexpectedError>
255
261
  }
256
262
 
257
263
  const makeStorage = (ctx: DurableObjectState, env: Env, storeId: string): SyncStorage => {
258
264
  const dbName = `mutation_log_${PERSISTENCE_FORMAT_VERSION}_${toValidTableName(storeId)}`
259
265
 
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()
262
- if (rawEvents.error) {
263
- throw new Error(rawEvents.error)
264
- }
265
- const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results)
266
+ const execDb = <T>(cb: (db: D1Database) => Promise<D1Result<T>>) =>
267
+ Effect.tryPromise({
268
+ try: () => cb(env.DB),
269
+ catch: (error) => new UnexpectedError({ cause: error, payload: { dbName } }),
270
+ }).pipe(Effect.map((_) => _.results))
266
271
 
267
- return events[0]
268
- }
269
-
270
- const getEvents = async (
271
- cursor: number | undefined,
272
- ): Promise<
273
- ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>
274
- > => {
275
- const whereClause = cursor === undefined ? '' : `WHERE id > ${cursor}`
276
- const sql = `SELECT * FROM ${dbName} ${whereClause} ORDER BY id ASC`
277
- // TODO handle case where `cursor` was not found
278
- const rawEvents = await env.DB.prepare(sql).all()
279
- if (rawEvents.error) {
280
- throw new Error(rawEvents.error)
281
- }
282
- const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents.results).map(
283
- ({ createdAt, ...mutationEventEncoded }) => ({
284
- mutationEventEncoded,
285
- metadata: Option.some({ createdAt }),
286
- }),
272
+ const getHead: Effect.Effect<EventId.GlobalEventId, UnexpectedError> = Effect.gen(function* () {
273
+ const result = yield* execDb<{ id: EventId.GlobalEventId }>((db) =>
274
+ db.prepare(`SELECT id FROM ${dbName} ORDER BY id DESC LIMIT 1`).all(),
287
275
  )
288
- return events
289
- }
290
276
 
291
- const appendEvent = async (event: MutationEvent.AnyEncodedGlobal, createdAt: string) => {
292
- const sql = `INSERT INTO ${dbName} (id, parentId, args, mutation, createdAt) VALUES (?, ?, ?, ?, ?)`
293
- await env.DB.prepare(sql)
294
- .bind(event.id, event.parentId, JSON.stringify(event.args), event.mutation, createdAt)
295
- .run()
296
- }
277
+ return result[0]?.id ?? EventId.ROOT.global
278
+ }).pipe(UnexpectedError.mapToUnexpectedError)
297
279
 
298
- const resetRoom = async () => {
299
- await ctx.storage.deleteAll()
300
- }
280
+ const getEvents = (
281
+ cursor: number | undefined,
282
+ ): Effect.Effect<
283
+ ReadonlyArray<{ mutationEventEncoded: MutationEvent.AnyEncodedGlobal; metadata: Option.Option<SyncMetadata> }>,
284
+ UnexpectedError
285
+ > =>
286
+ Effect.gen(function* () {
287
+ const whereClause = cursor === undefined ? '' : `WHERE id > ${cursor}`
288
+ const sql = `SELECT * FROM ${dbName} ${whereClause} ORDER BY id ASC`
289
+ // TODO handle case where `cursor` was not found
290
+ const rawEvents = yield* execDb((db) => db.prepare(sql).all())
291
+ const events = Schema.decodeUnknownSync(Schema.Array(mutationLogTable.schema))(rawEvents).map(
292
+ ({ createdAt, ...mutationEventEncoded }) => ({
293
+ mutationEventEncoded,
294
+ metadata: Option.some({ createdAt }),
295
+ }),
296
+ )
297
+ return events
298
+ })
299
+
300
+ const appendEvents: SyncStorage['appendEvents'] = (batch, createdAt) =>
301
+ Effect.gen(function* () {
302
+ // If there are no events, do nothing.
303
+ if (batch.length === 0) return
304
+
305
+ // CF D1 limits:
306
+ // Maximum bound parameters per query 100, Maximum arguments per SQL function 32
307
+ // Thus we need to split the batch into chunks of max (100/6=)16 events each.
308
+ const CHUNK_SIZE = 16
309
+
310
+ for (let i = 0; i < batch.length; i += CHUNK_SIZE) {
311
+ const chunk = batch.slice(i, i + CHUNK_SIZE)
312
+
313
+ // Create a list of placeholders ("(?, ?, ?, ?, ?), …") corresponding to each event.
314
+ const valuesPlaceholders = chunk.map(() => '(?, ?, ?, ?, ?, ?)').join(', ')
315
+ const sql = `INSERT INTO ${dbName} (id, parentId, args, mutation, createdAt, clientId) VALUES ${valuesPlaceholders}`
316
+ // Flatten the event properties into a parameters array.
317
+ const params = chunk.flatMap((event) => [
318
+ event.id,
319
+ event.parentId,
320
+ JSON.stringify(event.args),
321
+ event.mutation,
322
+ createdAt,
323
+ event.clientId,
324
+ ])
325
+
326
+ yield* execDb((db) =>
327
+ db
328
+ .prepare(sql)
329
+ .bind(...params)
330
+ .run(),
331
+ )
332
+ }
333
+ })
334
+
335
+ const resetStore = Effect.gen(function* () {
336
+ yield* Effect.promise(() => ctx.storage.deleteAll())
337
+ }).pipe(UnexpectedError.mapToUnexpectedError)
301
338
 
302
- return { dbName, getLatestEvent, getEvents, appendEvent, resetRoom }
339
+ return { dbName, getHead, getEvents, appendEvents, resetStore }
303
340
  }
304
341
 
305
342
  const getStoreId = (request: Request) => {
@@ -17,8 +17,7 @@ export const SyncMetadata = Schema.Struct({
17
17
  export type SyncMetadata = typeof SyncMetadata.Type
18
18
 
19
19
  export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
20
- requestId: Schema.String,
21
- events: Schema.Array(
20
+ batch: Schema.Array(
22
21
  Schema.Struct({
23
22
  mutationEventEncoded: MutationEvent.AnyEncodedGlobal,
24
23
  metadata: Schema.Option(SyncMetadata),
@@ -29,13 +28,6 @@ export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
29
28
 
30
29
  export type PullRes = typeof PullRes.Type
31
30
 
32
- export const PushBroadcast = Schema.TaggedStruct('WSMessage.PushBroadcast', {
33
- mutationEventEncoded: MutationEvent.AnyEncodedGlobal,
34
- metadata: Schema.Option(SyncMetadata),
35
- })
36
-
37
- export type PushBroadcast = typeof PushBroadcast.Type
38
-
39
31
  export const PushReq = Schema.TaggedStruct('WSMessage.PushReq', {
40
32
  requestId: Schema.String,
41
33
  batch: Schema.Array(MutationEvent.AnyEncodedGlobal),
@@ -45,7 +37,6 @@ export type PushReq = typeof PushReq.Type
45
37
 
46
38
  export const PushAck = Schema.TaggedStruct('WSMessage.PushAck', {
47
39
  requestId: Schema.String,
48
- mutationId: Schema.Number,
49
40
  })
50
41
 
51
42
  export type PushAck = typeof PushAck.Type
@@ -99,7 +90,6 @@ export type AdminInfoRes = typeof AdminInfoRes.Type
99
90
  export const Message = Schema.Union(
100
91
  PullReq,
101
92
  PullRes,
102
- PushBroadcast,
103
93
  PushReq,
104
94
  PushAck,
105
95
  Error,
@@ -113,15 +103,7 @@ export const Message = Schema.Union(
113
103
  export type Message = typeof Message.Type
114
104
  export type MessageEncoded = typeof Message.Encoded
115
105
 
116
- export const BackendToClientMessage = Schema.Union(
117
- PullRes,
118
- PushBroadcast,
119
- PushAck,
120
- AdminResetRoomRes,
121
- AdminInfoRes,
122
- Error,
123
- Pong,
124
- )
106
+ export const BackendToClientMessage = Schema.Union(PullRes, PushAck, AdminResetRoomRes, AdminInfoRes, Error, Pong)
125
107
  export type BackendToClientMessage = typeof BackendToClientMessage.Type
126
108
 
127
109
  export const ClientToBackendMessage = Schema.Union(PullReq, PushReq, AdminResetRoomReq, AdminInfoReq, Ping)
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { SyncBackend } from '@livestore/common'
4
4
  import { InvalidPullError, InvalidPushError } from '@livestore/common'
5
- import { pick } from '@livestore/utils'
5
+ import { LS_DEV } from '@livestore/utils'
6
6
  import type { Scope } from '@livestore/utils/effect'
7
7
  import {
8
8
  Deferred,
@@ -43,24 +43,13 @@ export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<Sy
43
43
  yield* send(WSMessage.PullReq.make({ cursor, requestId }))
44
44
 
45
45
  return Stream.fromPubSub(incomingMessages).pipe(
46
- Stream.filter((_) => (_._tag === 'WSMessage.PullRes' ? _.requestId === requestId : true)),
47
46
  Stream.tap((_) =>
48
47
  _._tag === 'WSMessage.Error' && _.requestId === requestId
49
48
  ? new InvalidPullError({ message: _.message })
50
49
  : Effect.void,
51
50
  ),
52
- Stream.filter(Schema.is(Schema.Union(WSMessage.PushBroadcast, WSMessage.PullRes))),
53
- Stream.map((msg) =>
54
- msg._tag === 'WSMessage.PushBroadcast'
55
- ? { batch: [pick(msg, ['mutationEventEncoded', 'metadata'])], remaining: 0 }
56
- : {
57
- batch: msg.events.map(({ mutationEventEncoded, metadata }) => ({
58
- mutationEventEncoded,
59
- metadata,
60
- })),
61
- remaining: msg.remaining,
62
- },
63
- ),
51
+ // This call is mostly here to for type narrowing
52
+ Stream.filter(Schema.is(WSMessage.PullRes)),
64
53
  )
65
54
  }).pipe(Stream.unwrap),
66
55
 
@@ -70,15 +59,12 @@ export const makeWsSync = (options: WsSyncOptions): Effect.Effect<SyncBackend<Sy
70
59
  const requestId = nanoid()
71
60
 
72
61
  yield* Stream.fromPubSub(incomingMessages).pipe(
73
- Stream.filter((_) => _._tag !== 'WSMessage.PushBroadcast' && _.requestId === requestId),
74
62
  Stream.tap((_) =>
75
- _._tag === 'WSMessage.Error'
63
+ _._tag === 'WSMessage.Error' && _.requestId === requestId
76
64
  ? Deferred.fail(ready, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
77
65
  : Effect.void,
78
66
  ),
79
- Stream.filter(Schema.is(WSMessage.PushAck)),
80
- // TODO bring back filterting of "own events"
81
- // Stream.filter((_) => _.mutationId === mutationEventEncoded.id.global),
67
+ Stream.filter((_) => _._tag === 'WSMessage.PushAck' && _.requestId === requestId),
82
68
  Stream.take(1),
83
69
  Stream.tap(() => Deferred.succeed(ready, void 0)),
84
70
  Stream.runDrain,
@@ -115,21 +101,23 @@ const connect = (wsUrl: string) =>
115
101
  // Wait first until we're online
116
102
  yield* waitUntilOnline
117
103
 
118
- yield* Effect.spanEvent(
119
- `Sending message: ${message._tag}`,
120
- message._tag === 'WSMessage.PushReq'
121
- ? {
122
- id: message.batch[0]!.id,
123
- parentId: message.batch[0]!.parentId,
124
- batchLength: message.batch.length,
125
- }
126
- : message._tag === 'WSMessage.PullReq'
127
- ? { cursor: message.cursor ?? '-' }
128
- : {},
129
- )
130
-
131
104
  // TODO use MsgPack instead of JSON to speed up the serialization / reduce the size of the messages
132
105
  socketRef.current!.send(Schema.encodeSync(Schema.parseJson(WSMessage.Message))(message))
106
+
107
+ if (LS_DEV) {
108
+ yield* Effect.spanEvent(
109
+ `Sent message: ${message._tag}`,
110
+ message._tag === 'WSMessage.PushReq'
111
+ ? {
112
+ id: message.batch[0]!.id,
113
+ parentId: message.batch[0]!.parentId,
114
+ batchLength: message.batch.length,
115
+ }
116
+ : message._tag === 'WSMessage.PullReq'
117
+ ? { cursor: message.cursor ?? '-' }
118
+ : {},
119
+ )
120
+ }
133
121
  })
134
122
 
135
123
  const innerConnect = Effect.gen(function* () {
@@ -138,6 +126,7 @@ const connect = (wsUrl: string) =>
138
126
  while (typeof navigator !== 'undefined' && navigator.onLine === false) {
139
127
  yield* Effect.sleep(1000)
140
128
  }
129
+ // TODO bring this back in a cross-platform way
141
130
  // if (navigator.onLine === false) {
142
131
  // yield* Effect.async((cb) => self.addEventListener('online', () => cb(Effect.void)))
143
132
  // }