@livestore/sync-cf 0.4.0-dev.8 → 0.4.0

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 (100) hide show
  1. package/README.md +7 -8
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cf-worker/do/durable-object.d.ts +1 -1
  4. package/dist/cf-worker/do/durable-object.d.ts.map +1 -1
  5. package/dist/cf-worker/do/durable-object.js +15 -14
  6. package/dist/cf-worker/do/durable-object.js.map +1 -1
  7. package/dist/cf-worker/do/layer.d.ts +6 -6
  8. package/dist/cf-worker/do/layer.d.ts.map +1 -1
  9. package/dist/cf-worker/do/layer.js +32 -9
  10. package/dist/cf-worker/do/layer.js.map +1 -1
  11. package/dist/cf-worker/do/pull.d.ts +8 -3
  12. package/dist/cf-worker/do/pull.d.ts.map +1 -1
  13. package/dist/cf-worker/do/pull.js +22 -10
  14. package/dist/cf-worker/do/pull.js.map +1 -1
  15. package/dist/cf-worker/do/push.d.ts +5 -4
  16. package/dist/cf-worker/do/push.d.ts.map +1 -1
  17. package/dist/cf-worker/do/push.js +80 -41
  18. package/dist/cf-worker/do/push.js.map +1 -1
  19. package/dist/cf-worker/do/sqlite.d.ts +10 -1
  20. package/dist/cf-worker/do/sqlite.d.ts.map +1 -1
  21. package/dist/cf-worker/do/sqlite.js +13 -4
  22. package/dist/cf-worker/do/sqlite.js.map +1 -1
  23. package/dist/cf-worker/do/sync-storage.d.ts +14 -9
  24. package/dist/cf-worker/do/sync-storage.d.ts.map +1 -1
  25. package/dist/cf-worker/do/sync-storage.js +92 -18
  26. package/dist/cf-worker/do/sync-storage.js.map +1 -1
  27. package/dist/cf-worker/do/transport/do-rpc-server.d.ts +2 -1
  28. package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -1
  29. package/dist/cf-worker/do/transport/do-rpc-server.js +13 -7
  30. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
  31. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +3 -1
  32. package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
  33. package/dist/cf-worker/do/transport/http-rpc-server.js +24 -15
  34. package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
  35. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +2 -1
  36. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
  37. package/dist/cf-worker/do/transport/ws-rpc-server.js +30 -8
  38. package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
  39. package/dist/cf-worker/shared.d.ts +123 -30
  40. package/dist/cf-worker/shared.d.ts.map +1 -1
  41. package/dist/cf-worker/shared.js +50 -6
  42. package/dist/cf-worker/shared.js.map +1 -1
  43. package/dist/cf-worker/worker.d.ts +64 -71
  44. package/dist/cf-worker/worker.d.ts.map +1 -1
  45. package/dist/cf-worker/worker.js +70 -48
  46. package/dist/cf-worker/worker.js.map +1 -1
  47. package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
  48. package/dist/client/transport/do-rpc-client.js +27 -10
  49. package/dist/client/transport/do-rpc-client.js.map +1 -1
  50. package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
  51. package/dist/client/transport/http-rpc-client.js +29 -9
  52. package/dist/client/transport/http-rpc-client.js.map +1 -1
  53. package/dist/client/transport/ws-rpc-client.d.ts +2 -1
  54. package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
  55. package/dist/client/transport/ws-rpc-client.js +31 -17
  56. package/dist/client/transport/ws-rpc-client.js.map +1 -1
  57. package/dist/common/constants.d.ts +7 -0
  58. package/dist/common/constants.d.ts.map +1 -0
  59. package/dist/common/constants.js +17 -0
  60. package/dist/common/constants.js.map +1 -0
  61. package/dist/common/do-rpc-schema.d.ts +6 -6
  62. package/dist/common/do-rpc-schema.d.ts.map +1 -1
  63. package/dist/common/do-rpc-schema.js +4 -4
  64. package/dist/common/do-rpc-schema.js.map +1 -1
  65. package/dist/common/http-rpc-schema.d.ts +4 -4
  66. package/dist/common/http-rpc-schema.d.ts.map +1 -1
  67. package/dist/common/http-rpc-schema.js +4 -4
  68. package/dist/common/http-rpc-schema.js.map +1 -1
  69. package/dist/common/mod.d.ts +4 -1
  70. package/dist/common/mod.d.ts.map +1 -1
  71. package/dist/common/mod.js +4 -1
  72. package/dist/common/mod.js.map +1 -1
  73. package/dist/common/sync-message-types.d.ts +2 -2
  74. package/dist/common/sync-message-types.js +3 -3
  75. package/dist/common/sync-message-types.js.map +1 -1
  76. package/dist/common/ws-rpc-schema.d.ts +3 -3
  77. package/dist/common/ws-rpc-schema.d.ts.map +1 -1
  78. package/dist/common/ws-rpc-schema.js +3 -3
  79. package/dist/common/ws-rpc-schema.js.map +1 -1
  80. package/package.json +72 -14
  81. package/src/cf-worker/do/durable-object.ts +23 -18
  82. package/src/cf-worker/do/layer.ts +35 -13
  83. package/src/cf-worker/do/pull.ts +43 -14
  84. package/src/cf-worker/do/push.ts +107 -46
  85. package/src/cf-worker/do/sqlite.ts +14 -4
  86. package/src/cf-worker/do/sync-storage.ts +151 -31
  87. package/src/cf-worker/do/transport/do-rpc-server.ts +22 -9
  88. package/src/cf-worker/do/transport/http-rpc-server.ts +33 -13
  89. package/src/cf-worker/do/transport/ws-rpc-server.ts +40 -12
  90. package/src/cf-worker/shared.ts +149 -25
  91. package/src/cf-worker/worker.ts +138 -108
  92. package/src/client/transport/do-rpc-client.ts +41 -17
  93. package/src/client/transport/http-rpc-client.ts +43 -17
  94. package/src/client/transport/ws-rpc-client.ts +42 -19
  95. package/src/common/constants.ts +18 -0
  96. package/src/common/do-rpc-schema.ts +5 -4
  97. package/src/common/http-rpc-schema.ts +5 -4
  98. package/src/common/mod.ts +4 -2
  99. package/src/common/sync-message-types.ts +3 -3
  100. package/src/common/ws-rpc-schema.ts +4 -3
@@ -1,9 +1,10 @@
1
- import { UnexpectedError } from '@livestore/common'
2
- import { EventSequenceNumber, State } from '@livestore/common/schema'
1
+ import { UnknownError } from '@livestore/common'
3
2
  import type { CfTypes } from '@livestore/common-cf'
3
+ import { EventSequenceNumber, State } from '@livestore/common/schema'
4
4
  import { shouldNeverHappen } from '@livestore/utils'
5
5
  import { Effect, Predicate } from '@livestore/utils/effect'
6
6
  import { nanoid } from '@livestore/utils/nanoid'
7
+
7
8
  import type { Env, MakeDurableObjectClassOptions, RpcSubscription } from '../shared.ts'
8
9
  import { contextTable, eventlogTable } from './sqlite.ts'
9
10
  import { makeStorage } from './sync-storage.ts'
@@ -27,7 +28,7 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
27
28
  }
28
29
 
29
30
  const getStoreId = (from: CfTypes.Request | { storeId: string }) => {
30
- if (Predicate.hasProperty(from, 'url')) {
31
+ if (Predicate.hasProperty(from, 'url') === true) {
31
32
  const url = new URL(from.url)
32
33
  return (
33
34
  url.searchParams.get('storeId') ?? shouldNeverHappen(`No storeId provided in request URL search params`)
@@ -37,15 +38,36 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
37
38
  }
38
39
 
39
40
  const storeId = getStoreId(from)
40
- const storage = makeStorage(doSelf.ctx, doSelf.env, storeId)
41
+ // Resolve storage engine
42
+ const makeEngine = Effect.gen(function* () {
43
+ const opt = doOptions?.storage
44
+ if (opt?._tag === 'd1') {
45
+ const db = (doSelf.env as any)[opt.binding]
46
+ if (db == null) {
47
+ return yield* UnknownError.make({ cause: new Error(`D1 binding '${opt.binding}' not found on env`) })
48
+ }
49
+ return { _tag: 'd1' as const, db }
50
+ } else if (opt?._tag === 'do-sqlite' || opt === undefined) {
51
+ return { _tag: 'do-sqlite' as const }
52
+ } else return shouldNeverHappen(`Invalid storage engine`, opt)
53
+ })
54
+
55
+ const engine = yield* makeEngine
56
+
57
+ const storage = makeStorage(doSelf.ctx, storeId, engine)
41
58
 
42
59
  // Initialize database tables
43
60
  {
44
61
  const colSpec = State.SQLite.makeColumnSpec(eventlogTable.sqliteDef.ast)
45
- // D1 database is async, so we need to use a promise
46
- yield* Effect.promise(() =>
47
- doSelf.env.DB.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`),
48
- )
62
+ if (engine._tag === 'd1') {
63
+ // D1 database is async, so we need to use a promise
64
+ yield* Effect.promise(() =>
65
+ engine.db.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`),
66
+ )
67
+ } else {
68
+ // DO SQLite table lives in Durable Object storage
69
+ doSelf.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS "${storage.dbName}" (${colSpec}) strict`)
70
+ }
49
71
  }
50
72
  {
51
73
  const colSpec = State.SQLite.makeColumnSpec(contextTable.sqliteDef.ast)
@@ -56,14 +78,14 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
56
78
  .exec(`SELECT * FROM "${contextTable.sqliteDef.name}" WHERE storeId = ?`, storeId)
57
79
  .toArray()[0] as typeof contextTable.rowSchema.Type | undefined
58
80
 
59
- const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.ROOT.global }
81
+ const currentHeadRef = { current: storageRow?.currentHead ?? EventSequenceNumber.Client.ROOT.global }
60
82
 
61
83
  // TODO do concistency check with eventlog table to make sure the head is consistent
62
84
 
63
- // Should be the same backendId for lifetime of the durable object
85
+ // Should be the same backendId for lifetime of the Durable Object
64
86
  const backendId = storageRow?.backendId ?? nanoid()
65
87
 
66
- const updateCurrentHead = (currentHead: EventSequenceNumber.GlobalEventSequenceNumber) => {
88
+ const updateCurrentHead = (currentHead: EventSequenceNumber.Global.Type) => {
67
89
  doSelf.ctx.storage.sql.exec(
68
90
  `INSERT OR REPLACE INTO "${contextTable.sqliteDef.name}" (storeId, currentHead, backendId) VALUES (?, ?, ?)`,
69
91
  storeId,
@@ -96,12 +118,12 @@ export class DoCtx extends Effect.Service<DoCtx>()('DoCtx', {
96
118
 
97
119
  // Set initial current head to root
98
120
  if (storageRow === undefined) {
99
- updateCurrentHead(EventSequenceNumber.ROOT.global)
121
+ updateCurrentHead(EventSequenceNumber.Client.ROOT.global)
100
122
  }
101
123
 
102
124
  return storageCache
103
125
  },
104
- UnexpectedError.mapToUnexpectedError,
126
+ UnknownError.mapToUnknownError,
105
127
  Effect.withSpan('@livestore/sync-cf:durable-object:makeDoCtx'),
106
128
  ),
107
129
  }) {}
@@ -1,9 +1,14 @@
1
- import { BackendIdMismatchError, InvalidPullError, SyncBackend, UnexpectedError } from '@livestore/common'
2
- import { Chunk, Effect, Option, type Schema, Stream } from '@livestore/utils/effect'
1
+ import { BackendIdMismatchError, SyncBackend, UnknownError } from '@livestore/common'
2
+ import { splitChunkBySize } from '@livestore/common/sync'
3
+ import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
4
+
5
+ import { MAX_PULL_EVENTS_PER_MESSAGE, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
3
6
  import { SyncMessage } from '../../common/mod.ts'
4
- import { PULL_CHUNK_SIZE } from '../shared.ts'
7
+ import type { ForwardedHeaders } from '../shared.ts'
5
8
  import { DoCtx } from './layer.ts'
6
9
 
10
+ const encodePullResponse = Schema.encodeSync(SyncMessage.PullResponse)
11
+
7
12
  // Notes on stream handling:
8
13
  // We're intentionally closing the stream once we've read all existing events
9
14
  //
@@ -13,16 +18,27 @@ import { DoCtx } from './layer.ts'
13
18
  // DO RPC:
14
19
  // - Further chunks will be emitted manually in `push.ts`
15
20
  // - If the client sends a `Interrupt` RPC message, TODO
16
- export const makeEndingPullStream = (
17
- req: SyncMessage.PullRequest,
18
- payload: Schema.JsonValue | undefined,
19
- ): Stream.Stream<SyncMessage.PullResponse, InvalidPullError, DoCtx> =>
21
+ export const makeEndingPullStream = ({
22
+ req,
23
+ payload,
24
+ headers,
25
+ }: {
26
+ req: SyncMessage.PullRequest
27
+ payload: Schema.JsonValue | undefined
28
+ headers: ForwardedHeaders | undefined
29
+ }): Stream.Stream<SyncMessage.PullResponse, UnknownError | BackendIdMismatchError, DoCtx> =>
20
30
  Effect.gen(function* () {
21
31
  const { doOptions, backendId, storeId, storage } = yield* DoCtx
22
32
 
23
- if (doOptions?.onPull) {
24
- yield* Effect.tryAll(() => doOptions!.onPull!(req, { storeId, payload })).pipe(
25
- UnexpectedError.mapToUnexpectedError,
33
+ if (doOptions?.onPull !== undefined) {
34
+ yield* Effect.tryAll(() =>
35
+ doOptions.onPull!(req, {
36
+ storeId,
37
+ ...(payload !== undefined ? { payload } : {}),
38
+ ...(headers !== undefined ? { headers } : {}),
39
+ }),
40
+ ).pipe(
41
+ UnknownError.mapToUnknownError,
26
42
  )
27
43
  }
28
44
 
@@ -35,7 +51,16 @@ export const makeEndingPullStream = (
35
51
  )
36
52
 
37
53
  return storedEvents.pipe(
38
- Stream.grouped(PULL_CHUNK_SIZE),
54
+ Stream.mapChunksEffect(
55
+ splitChunkBySize({
56
+ maxItems: MAX_PULL_EVENTS_PER_MESSAGE,
57
+ maxBytes: MAX_WS_MESSAGE_BYTES,
58
+ encode: (batch) =>
59
+ encodePullResponse(
60
+ SyncMessage.PullResponse.make({ batch, pageInfo: SyncBackend.pageInfoNoMore, backendId }),
61
+ ),
62
+ }),
63
+ ),
39
64
  Stream.mapAccum(total, (remaining, chunk) => {
40
65
  const asArray = Chunk.toReadonlyArray(chunk)
41
66
  const nextRemaining = Math.max(0, remaining - asArray.length)
@@ -51,8 +76,8 @@ export const makeEndingPullStream = (
51
76
  }),
52
77
  Stream.tap(
53
78
  Effect.fn(function* (res) {
54
- if (doOptions?.onPullRes) {
55
- yield* Effect.tryAll(() => doOptions.onPullRes!(res)).pipe(UnexpectedError.mapToUnexpectedError)
79
+ if (doOptions?.onPullRes !== undefined) {
80
+ yield* Effect.tryAll(() => doOptions.onPullRes!(res)).pipe(UnknownError.mapToUnknownError)
56
81
  }
57
82
  }),
58
83
  ),
@@ -60,6 +85,10 @@ export const makeEndingPullStream = (
60
85
  )
61
86
  }).pipe(
62
87
  Stream.unwrap,
63
- Stream.mapError((cause) => InvalidPullError.make({ cause })),
88
+ Stream.mapError((cause) =>
89
+ cause._tag === 'BackendIdMismatchError' || cause._tag === 'UnknownError'
90
+ ? cause
91
+ : new UnknownError({ cause }),
92
+ ),
64
93
  Stream.withSpan('cloudflare-provider:pull'),
65
94
  )
@@ -1,25 +1,39 @@
1
1
  import {
2
2
  BackendIdMismatchError,
3
- InvalidPushError,
4
3
  ServerAheadError,
5
4
  SyncBackend,
6
- UnexpectedError,
5
+ UnknownError,
7
6
  } from '@livestore/common'
8
7
  import { type CfTypes, emitStreamResponse } from '@livestore/common-cf'
9
- import { Effect, Option, type RpcMessage, Schema } from '@livestore/utils/effect'
8
+ import { splitChunkBySize } from '@livestore/common/sync'
9
+ import { Chunk, Effect, Option, type RpcMessage, Schema } from '@livestore/utils/effect'
10
+
11
+ import { MAX_PUSH_EVENTS_PER_REQUEST, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
10
12
  import { SyncMessage } from '../../common/mod.ts'
11
- import { type Env, type MakeDurableObjectClassOptions, type StoreId, WebSocketAttachmentSchema } from '../shared.ts'
13
+ import {
14
+ type Env,
15
+ type ForwardedHeaders,
16
+ type MakeDurableObjectClassOptions,
17
+ type StoreId,
18
+ WebSocketAttachmentSchema,
19
+ } from '../shared.ts'
12
20
  import { DoCtx } from './layer.ts'
13
21
 
22
+ const encodePullResponse = Schema.encodeSync(SyncMessage.PullResponse)
23
+ const jsonStringify = Schema.encodeSync(Schema.parseJson())
24
+ type PullBatchItem = SyncMessage.PullResponse['batch'][number]
25
+
14
26
  export const makePush =
15
27
  ({
16
28
  payload,
29
+ headers,
17
30
  options,
18
31
  storeId,
19
32
  ctx,
20
33
  env,
21
34
  }: {
22
35
  payload: Schema.JsonValue | undefined
36
+ headers: ForwardedHeaders | undefined
23
37
  options: MakeDurableObjectClassOptions | undefined
24
38
  storeId: StoreId
25
39
  ctx: CfTypes.DurableObjectState
@@ -34,9 +48,15 @@ export const makePush =
34
48
  return SyncMessage.PushAck.make({})
35
49
  }
36
50
 
37
- if (options?.onPush) {
38
- yield* Effect.tryAll(() => options.onPush!(pushRequest, { storeId, payload })).pipe(
39
- UnexpectedError.mapToUnexpectedError,
51
+ if (options?.onPush !== undefined) {
52
+ yield* Effect.tryAll(() =>
53
+ options.onPush!(pushRequest, {
54
+ storeId,
55
+ ...(payload !== undefined ? { payload } : {}),
56
+ ...(headers !== undefined ? { headers } : {}),
57
+ }),
58
+ ).pipe(
59
+ UnknownError.mapToUnknownError,
40
60
  )
41
61
  }
42
62
 
@@ -51,6 +71,13 @@ export const makePush =
51
71
  // Validate the batch
52
72
  const firstEventParent = pushRequest.batch[0]!.parentSeqNum
53
73
  if (firstEventParent !== currentHead) {
74
+ // yield* Effect.logDebug('ServerAheadError: backend head mismatch', {
75
+ // expectedHead: currentHead,
76
+ // providedHead: firstEventParent,
77
+ // batchSize: pushRequest.batch.length,
78
+ // backendId,
79
+ // })
80
+
54
81
  return yield* new ServerAheadError({ minimumExpectedNum: currentHead, providedNum: firstEventParent })
55
82
  }
56
83
 
@@ -68,40 +95,71 @@ export const makePush =
68
95
  yield* Effect.gen(function* () {
69
96
  const connectedClients = ctx.getWebSockets()
70
97
 
71
- // Dual broadcasting: WebSocket + RPC clients
72
- const pullRes = SyncMessage.PullResponse.make({
73
- batch: pushRequest.batch.map((eventEncoded) => ({
74
- eventEncoded,
75
- metadata: Option.some(SyncMessage.SyncMetadata.make({ createdAt })),
76
- })),
77
- pageInfo: SyncBackend.pageInfoNoMore,
78
- backendId,
79
- })
98
+ // Preparing chunks of responses to make sure we don't exceed the WS message size limit.
99
+ const responses = yield* Chunk.fromIterable(pushRequest.batch).pipe(
100
+ splitChunkBySize({
101
+ maxItems: MAX_PUSH_EVENTS_PER_REQUEST,
102
+ maxBytes: MAX_WS_MESSAGE_BYTES,
103
+ encode: (items) =>
104
+ encodePullResponse(
105
+ SyncMessage.PullResponse.make({
106
+ batch: items.map(
107
+ (eventEncoded): PullBatchItem => ({
108
+ eventEncoded,
109
+ metadata: Option.some(SyncMessage.SyncMetadata.make({ createdAt })),
110
+ }),
111
+ ),
112
+ pageInfo: SyncBackend.pageInfoNoMore,
113
+ backendId,
114
+ }),
115
+ ),
116
+ }),
117
+ Effect.map(
118
+ Chunk.map((eventsChunk) => {
119
+ const batchWithMetadata = Chunk.toReadonlyArray(eventsChunk).map((eventEncoded) => ({
120
+ eventEncoded,
121
+ metadata: Option.some(SyncMessage.SyncMetadata.make({ createdAt })),
122
+ }))
123
+
124
+ const response = SyncMessage.PullResponse.make({
125
+ batch: batchWithMetadata,
126
+ pageInfo: SyncBackend.pageInfoNoMore,
127
+ backendId,
128
+ })
129
+
130
+ return {
131
+ response,
132
+ encoded: Schema.encodeSync(SyncMessage.PullResponse)(response),
133
+ }
134
+ }),
135
+ ),
136
+ )
80
137
 
81
- const pullResEnc = Schema.encodeSync(SyncMessage.PullResponse)(pullRes)
138
+ // Dual broadcasting: WebSocket + RPC clients
82
139
 
83
140
  // Broadcast to WebSocket clients
84
141
  if (connectedClients.length > 0) {
85
- // Only calling once for now.
86
- if (options?.onPullRes) {
87
- yield* Effect.tryAll(() => options.onPullRes!(pullRes)).pipe(UnexpectedError.mapToUnexpectedError)
88
- }
142
+ for (const { response, encoded } of responses) {
143
+ // Only calling once for now.
144
+ if (options?.onPullRes !== undefined) {
145
+ yield* Effect.tryAll(() => options.onPullRes!(response)).pipe(UnknownError.mapToUnknownError)
146
+ }
89
147
 
90
- // NOTE we're also sending the pullRes to the pushing ws client as a confirmation
91
- for (const conn of connectedClients) {
92
- // conn.send(pullResEnc)
93
- const attachment = Schema.decodeSync(WebSocketAttachmentSchema)(conn.deserializeAttachment())
94
-
95
- // We're doing something a bit "advanced" here as we're directly emitting Effect RPC-compatible
96
- // response messsages on the Effect RPC-managed websocket connection to the WS client.
97
- // For this we need to get the RPC `requestId` from the WebSocket attachment.
98
- for (const requestId of attachment.pullRequestIds) {
99
- const res: RpcMessage.ResponseChunkEncoded = {
100
- _tag: 'Chunk',
101
- requestId,
102
- values: [pullResEnc],
148
+ // NOTE we're also sending the pullRes chunk to the pushing ws client as confirmation
149
+ for (const conn of connectedClients) {
150
+ const attachment = yield* Schema.decode(WebSocketAttachmentSchema)(conn.deserializeAttachment())
151
+
152
+ // We're doing something a bit "advanced" here as we're directly emitting Effect RPC-compatible
153
+ // response messsages on the Effect RPC-managed websocket connection to the WS client.
154
+ // For this we need to get the RPC `requestId` from the WebSocket attachment.
155
+ for (const requestId of attachment.pullRequestIds) {
156
+ const res: RpcMessage.ResponseChunkEncoded = {
157
+ _tag: 'Chunk',
158
+ requestId,
159
+ values: [encoded],
160
+ }
161
+ conn.send(jsonStringify(res))
103
162
  }
104
- conn.send(JSON.stringify(res))
105
163
  }
106
164
  }
107
165
 
@@ -110,17 +168,16 @@ export const makePush =
110
168
 
111
169
  // RPC broadcasting would require reconstructing client stubs from clientIds
112
170
  if (rpcSubscriptions.size > 0) {
113
- yield* Effect.forEach(
114
- rpcSubscriptions.values(),
115
- (subscription) =>
116
- emitStreamResponse({
171
+ for (const subscription of rpcSubscriptions.values()) {
172
+ for (const { encoded } of responses) {
173
+ yield* emitStreamResponse({
117
174
  callerContext: subscription.callerContext,
118
175
  env,
119
176
  requestId: subscription.requestId,
120
- values: [pullResEnc],
121
- }).pipe(Effect.tapCauseLogPretty, Effect.exit),
122
- { concurrency: 'unbounded' },
123
- )
177
+ values: [encoded],
178
+ }).pipe(Effect.tapCauseLogPretty, Effect.exit)
179
+ }
180
+ }
124
181
 
125
182
  yield* Effect.logDebug(`Broadcasted to ${rpcSubscriptions.size} RPC clients`)
126
183
  }
@@ -138,12 +195,16 @@ export const makePush =
138
195
  }).pipe(
139
196
  Effect.tap(
140
197
  Effect.fn(function* (message) {
141
- if (options?.onPushRes) {
142
- yield* Effect.tryAll(() => options.onPushRes!(message)).pipe(UnexpectedError.mapToUnexpectedError)
198
+ if (options?.onPushRes !== undefined) {
199
+ yield* Effect.tryAll(() => options.onPushRes!(message)).pipe(UnknownError.mapToUnknownError)
143
200
  }
144
201
  }),
145
202
  ),
146
- Effect.mapError((cause) => InvalidPushError.make({ cause })),
203
+ Effect.mapError((cause) =>
204
+ cause._tag === 'BackendIdMismatchError' || cause._tag === 'ServerAheadError' || cause._tag === 'UnknownError'
205
+ ? cause
206
+ : new UnknownError({ cause }),
207
+ ),
147
208
  Effect.withSpan('sync-cf:do:push', { attributes: { storeId, batchSize: pushRequest.batch.length } }),
148
209
  )
149
210
 
@@ -1,13 +1,19 @@
1
1
  import { EventSequenceNumber, State } from '@livestore/common/schema'
2
2
  import { Schema } from '@livestore/utils/effect'
3
+
3
4
  import { PERSISTENCE_FORMAT_VERSION } from '../shared.ts'
4
5
 
6
+ /**
7
+ * Main event log table storing all LiveStore events.
8
+ *
9
+ * ⚠️ IMPORTANT: Any changes to this schema require bumping PERSISTENCE_FORMAT_VERSION in shared.ts
10
+ */
5
11
  export const eventlogTable = State.SQLite.table({
6
12
  // NOTE actual table name is determined at runtime to use proper storeId
7
13
  name: `eventlog_${PERSISTENCE_FORMAT_VERSION}_$storeId`,
8
14
  columns: {
9
- seqNum: State.SQLite.integer({ primaryKey: true, schema: EventSequenceNumber.GlobalEventSequenceNumber }),
10
- parentSeqNum: State.SQLite.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
15
+ seqNum: State.SQLite.integer({ primaryKey: true, schema: EventSequenceNumber.Global.Schema }),
16
+ parentSeqNum: State.SQLite.integer({ schema: EventSequenceNumber.Global.Schema }),
11
17
  name: State.SQLite.text({}),
12
18
  args: State.SQLite.text({ schema: Schema.parseJson(Schema.Any), nullable: true }),
13
19
  /** ISO date format. Currently only used for debugging purposes. */
@@ -17,12 +23,16 @@ export const eventlogTable = State.SQLite.table({
17
23
  },
18
24
  })
19
25
 
20
- /** Will only ever have one row per durable object. */
26
+ /**
27
+ * Context metadata table - one row per Durable Object.
28
+ *
29
+ * ⚠️ IMPORTANT: Any changes to this schema require bumping PERSISTENCE_FORMAT_VERSION in shared.ts
30
+ */
21
31
  export const contextTable = State.SQLite.table({
22
32
  name: `context_${PERSISTENCE_FORMAT_VERSION}`,
23
33
  columns: {
24
34
  storeId: State.SQLite.text({ primaryKey: true }),
25
- currentHead: State.SQLite.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
35
+ currentHead: State.SQLite.integer({ schema: EventSequenceNumber.Global.Schema }),
26
36
  backendId: State.SQLite.text({}),
27
37
  },
28
38
  })