@livestore/sync-cf 0.4.0-dev.9 → 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 (104) 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 +13 -8
  6. package/dist/cf-worker/do/durable-object.js.map +1 -1
  7. package/dist/cf-worker/do/layer.d.ts +5 -5
  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 +7 -2
  12. package/dist/cf-worker/do/pull.d.ts.map +1 -1
  13. package/dist/cf-worker/do/pull.js +16 -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 +25 -17
  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.map +1 -1
  28. package/dist/cf-worker/do/transport/do-rpc-server.js +13 -7
  29. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -1
  30. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +4 -2
  31. package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -1
  32. package/dist/cf-worker/do/transport/http-rpc-server.js +24 -15
  33. package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -1
  34. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +2 -1
  35. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -1
  36. package/dist/cf-worker/do/transport/ws-rpc-server.js +30 -8
  37. package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -1
  38. package/dist/cf-worker/shared.d.ts +118 -31
  39. package/dist/cf-worker/shared.d.ts.map +1 -1
  40. package/dist/cf-worker/shared.js +40 -7
  41. package/dist/cf-worker/shared.js.map +1 -1
  42. package/dist/cf-worker/worker.d.ts +46 -38
  43. package/dist/cf-worker/worker.d.ts.map +1 -1
  44. package/dist/cf-worker/worker.js +51 -34
  45. package/dist/cf-worker/worker.js.map +1 -1
  46. package/dist/client/transport/do-rpc-client.d.ts.map +1 -1
  47. package/dist/client/transport/do-rpc-client.js +27 -10
  48. package/dist/client/transport/do-rpc-client.js.map +1 -1
  49. package/dist/client/transport/http-rpc-client.d.ts.map +1 -1
  50. package/dist/client/transport/http-rpc-client.js +29 -9
  51. package/dist/client/transport/http-rpc-client.js.map +1 -1
  52. package/dist/client/transport/ws-rpc-client.d.ts +2 -1
  53. package/dist/client/transport/ws-rpc-client.d.ts.map +1 -1
  54. package/dist/client/transport/ws-rpc-client.js +31 -17
  55. package/dist/client/transport/ws-rpc-client.js.map +1 -1
  56. package/dist/common/constants.d.ts +7 -0
  57. package/dist/common/constants.d.ts.map +1 -0
  58. package/dist/common/constants.js +17 -0
  59. package/dist/common/constants.js.map +1 -0
  60. package/dist/common/do-rpc-schema.d.ts +6 -6
  61. package/dist/common/do-rpc-schema.d.ts.map +1 -1
  62. package/dist/common/do-rpc-schema.js +4 -4
  63. package/dist/common/do-rpc-schema.js.map +1 -1
  64. package/dist/common/http-rpc-schema.d.ts +4 -4
  65. package/dist/common/http-rpc-schema.d.ts.map +1 -1
  66. package/dist/common/http-rpc-schema.js +4 -4
  67. package/dist/common/http-rpc-schema.js.map +1 -1
  68. package/dist/common/mod.d.ts +4 -1
  69. package/dist/common/mod.d.ts.map +1 -1
  70. package/dist/common/mod.js +4 -1
  71. package/dist/common/mod.js.map +1 -1
  72. package/dist/common/sync-message-types.d.ts +7 -7
  73. package/dist/common/sync-message-types.js +3 -3
  74. package/dist/common/sync-message-types.js.map +1 -1
  75. package/dist/common/ws-rpc-schema.d.ts +3 -3
  76. package/dist/common/ws-rpc-schema.d.ts.map +1 -1
  77. package/dist/common/ws-rpc-schema.js +3 -3
  78. package/dist/common/ws-rpc-schema.js.map +1 -1
  79. package/package.json +72 -14
  80. package/src/cf-worker/do/durable-object.ts +19 -10
  81. package/src/cf-worker/do/layer.ts +35 -13
  82. package/src/cf-worker/do/pull.ts +31 -14
  83. package/src/cf-worker/do/push.ts +49 -34
  84. package/src/cf-worker/do/sqlite.ts +14 -4
  85. package/src/cf-worker/do/sync-storage.ts +151 -31
  86. package/src/cf-worker/do/transport/do-rpc-server.ts +18 -7
  87. package/src/cf-worker/do/transport/http-rpc-server.ts +33 -13
  88. package/src/cf-worker/do/transport/ws-rpc-server.ts +40 -12
  89. package/src/cf-worker/shared.ts +136 -25
  90. package/src/cf-worker/worker.ts +107 -54
  91. package/src/client/transport/do-rpc-client.ts +41 -17
  92. package/src/client/transport/http-rpc-client.ts +43 -17
  93. package/src/client/transport/ws-rpc-client.ts +42 -19
  94. package/src/common/constants.ts +18 -0
  95. package/src/common/do-rpc-schema.ts +5 -4
  96. package/src/common/http-rpc-schema.ts +5 -4
  97. package/src/common/mod.ts +4 -2
  98. package/src/common/sync-message-types.ts +3 -3
  99. package/src/common/ws-rpc-schema.ts +4 -3
  100. package/dist/cf-worker/do/ws-chunking.d.ts +0 -22
  101. package/dist/cf-worker/do/ws-chunking.d.ts.map +0 -1
  102. package/dist/cf-worker/do/ws-chunking.js +0 -49
  103. package/dist/cf-worker/do/ws-chunking.js.map +0 -1
  104. package/src/cf-worker/do/ws-chunking.ts +0 -76
@@ -1,5 +1,6 @@
1
- import { InvalidPullError, InvalidPushError, SyncBackend, UnexpectedError } from '@livestore/common'
1
+ import { SyncBackend, UnknownError } from '@livestore/common'
2
2
  import type { EventSequenceNumber } from '@livestore/common/schema'
3
+ import { splitChunkBySize } from '@livestore/common/sync'
3
4
  import { omit } from '@livestore/utils'
4
5
  import {
5
6
  Chunk,
@@ -18,6 +19,8 @@ import {
18
19
  SubscriptionRef,
19
20
  UrlParams,
20
21
  } from '@livestore/utils/effect'
22
+
23
+ import { MAX_HTTP_REQUEST_BYTES, MAX_PUSH_EVENTS_PER_REQUEST } from '../../common/constants.ts'
21
24
  import { SyncHttpRpc } from '../../common/http-rpc-schema.ts'
22
25
  import { SearchParamsSchema } from '../../common/mod.ts'
23
26
  import type { SyncMetadata } from '../../common/sync-message-types.ts'
@@ -74,7 +77,7 @@ export const makeHttpSync =
74
77
  storeId,
75
78
  payload,
76
79
  transport: 'http',
77
- }).pipe(UnexpectedError.mapToUnexpectedError)
80
+ }).pipe(UnknownError.mapToUnknownError)
78
81
 
79
82
  const urlParams = UrlParams.fromInput(urlParamsData)
80
83
 
@@ -101,7 +104,7 @@ export const makeHttpSync =
101
104
 
102
105
  yield* SubscriptionRef.set(isConnected, true)
103
106
  }).pipe(
104
- UnexpectedError.mapToUnexpectedError,
107
+ UnknownError.mapToUnknownError,
105
108
  Effect.timeout(pingTimeout),
106
109
  Effect.catchTag('TimeoutException', () => SubscriptionRef.set(isConnected, false)),
107
110
  )
@@ -114,14 +117,14 @@ export const makeHttpSync =
114
117
  }
115
118
 
116
119
  // Helps already establish a TCP connection to the server
117
- const connect = ping.pipe(UnexpectedError.mapToUnexpectedError)
120
+ const connect = ping.pipe(UnknownError.mapToUnknownError)
118
121
 
119
122
  const backendIdHelper = yield* SyncBackend.makeBackendIdHelper
120
123
 
121
124
  const mapCursor = (cursor: Option.Option<{ eventSequenceNumber: number }>) =>
122
125
  cursor.pipe(
123
126
  Option.map((a) => ({
124
- eventSequenceNumber: a.eventSequenceNumber as EventSequenceNumber.GlobalEventSequenceNumber,
127
+ eventSequenceNumber: a.eventSequenceNumber as EventSequenceNumber.Global.Type,
125
128
  backendId: backendIdHelper.get().pipe(Option.getOrThrow),
126
129
  })),
127
130
  )
@@ -132,7 +135,7 @@ export const makeHttpSync =
132
135
  payload,
133
136
  cursor: mapCursor(cursor),
134
137
  }).pipe(
135
- options?.live
138
+ options?.live === true
136
139
  ? // Phase 2: Simulate `live` pull by polling for new events
137
140
  Stream.concatWithLastElement((lastElement) => {
138
141
  const initialPhase2Cursor = lastElement.pipe(
@@ -164,26 +167,49 @@ export const makeHttpSync =
164
167
  : identity,
165
168
  Stream.tap((res) => backendIdHelper.lazySet(res.backendId)),
166
169
  Stream.map((res) => omit(res, ['backendId'])),
167
- Stream.mapError((cause) => (cause._tag === 'InvalidPullError' ? cause : InvalidPullError.make({ cause }))),
170
+ Stream.mapError((cause) =>
171
+ cause._tag === 'UnknownError' || cause._tag === 'BackendIdMismatchError'
172
+ ? cause
173
+ : new UnknownError({ cause }),
174
+ ),
168
175
  Stream.withSpan('http-sync-client:pull'),
169
176
  )
170
177
 
171
178
  const pushSemaphore = yield* Effect.makeSemaphore(1)
172
179
 
173
- const push: SyncBackend.SyncBackend<SyncMetadata>['push'] = (batch) =>
174
- Effect.gen(function* () {
180
+ const push: SyncBackend.SyncBackend<SyncMetadata>['push'] = Effect.fn('http-sync-client:push')(
181
+ function* (batch) {
175
182
  if (batch.length === 0) {
176
183
  return
177
184
  }
178
185
 
179
- yield* rpcClient.SyncHttpRpc.Push({ storeId, payload, batch, backendId: backendIdHelper.get() })
180
- }).pipe(
181
- pushSemaphore.withPermits(1),
182
- Effect.mapError((cause) =>
183
- cause._tag === 'InvalidPushError' ? cause : new InvalidPushError({ cause: new UnexpectedError({ cause }) }),
184
- ),
185
- Effect.withSpan('http-sync-client:push'),
186
- )
186
+ const backendId = backendIdHelper.get()
187
+ const batchChunks = yield* Chunk.fromIterable(batch).pipe(
188
+ splitChunkBySize({
189
+ maxItems: MAX_PUSH_EVENTS_PER_REQUEST,
190
+ maxBytes: MAX_HTTP_REQUEST_BYTES,
191
+ encode: (items) => ({
192
+ batch: items,
193
+ storeId,
194
+ payload,
195
+ backendId,
196
+ }),
197
+ }),
198
+ Effect.mapError((cause) => new UnknownError({ cause })),
199
+ )
200
+
201
+ for (const chunk of Chunk.toReadonlyArray(batchChunks)) {
202
+ const chunkArray = Chunk.toReadonlyArray(chunk)
203
+ yield* rpcClient.SyncHttpRpc.Push({ storeId, payload, batch: chunkArray, backendId })
204
+ }
205
+ },
206
+ pushSemaphore.withPermits(1),
207
+ Effect.mapError((cause) =>
208
+ cause._tag === 'UnknownError' || cause._tag === 'ServerAheadError' || cause._tag === 'BackendIdMismatchError'
209
+ ? cause
210
+ : new UnknownError({ cause }),
211
+ ),
212
+ )
187
213
 
188
214
  return SyncBackend.of({
189
215
  connect,
@@ -1,6 +1,9 @@
1
- import { InvalidPullError, InvalidPushError, IsOfflineError, SyncBackend, UnexpectedError } from '@livestore/common'
1
+ import { IsOfflineError, SyncBackend, UnknownError } from '@livestore/common'
2
+ import type { LiveStoreEvent } from '@livestore/common/schema'
3
+ import { splitChunkBySize } from '@livestore/common/sync'
2
4
  import { omit } from '@livestore/utils'
3
5
  import {
6
+ Chunk,
4
7
  type Duration,
5
8
  Effect,
6
9
  Layer,
@@ -14,8 +17,10 @@ import {
14
17
  Stream,
15
18
  SubscriptionRef,
16
19
  UrlParams,
17
- type WebSocket,
18
20
  } from '@livestore/utils/effect'
21
+ import type { WebSocket } from '@livestore/utils/effect/browser'
22
+
23
+ import { MAX_PUSH_EVENTS_PER_REQUEST, MAX_WS_MESSAGE_BYTES } from '../../common/constants.ts'
19
24
  import { SearchParamsSchema } from '../../common/mod.ts'
20
25
  import type { SyncMetadata } from '../../common/sync-message-types.ts'
21
26
  import { SyncWsRpc } from '../../common/ws-rpc-schema.ts'
@@ -69,7 +74,7 @@ export const makeWsSync =
69
74
  storeId,
70
75
  payload,
71
76
  transport: 'ws',
72
- }).pipe(UnexpectedError.mapToUnexpectedError)
77
+ }).pipe(UnknownError.mapToUnknownError)
73
78
 
74
79
  const urlParams = UrlParams.fromInput(urlParamsData)
75
80
  const wsUrl = `${options.url}?${UrlParams.toString(urlParams)}`
@@ -91,7 +96,10 @@ export const makeWsSync =
91
96
 
92
97
  const ProtocolLive = RpcClient.layerProtocolSocketWithIsConnected({
93
98
  isConnected,
94
- retryTransientErrors: Schedule.fixed(1000),
99
+ retryTransientErrors: Schedule.exponential('1 seconds').pipe(
100
+ Schedule.union(Schedule.fixed('30 seconds')),
101
+ Schedule.jittered,
102
+ ),
95
103
  pingSchedule: Schedule.once.pipe(Schedule.andThen(Schedule.fixed(pingInterval))),
96
104
  url: wsUrl,
97
105
  }).pipe(
@@ -115,7 +123,7 @@ export const makeWsSync =
115
123
  }).pipe(
116
124
  Effect.timeout(pingTimeout),
117
125
  Effect.catchTag('TimeoutException', () => SubscriptionRef.set(isConnected, false)),
118
- UnexpectedError.mapToUnexpectedError,
126
+ UnknownError.mapToUnknownError,
119
127
  Effect.withSpan('ping'),
120
128
  )
121
129
 
@@ -134,39 +142,54 @@ export const makeWsSync =
134
142
  backendId: backendIdHelper.get().pipe(Option.getOrThrow),
135
143
  })),
136
144
  ),
137
- live: options?.live ?? false,
145
+ live: options?.live === true,
138
146
  }).pipe(
139
147
  Stream.tap((res) => backendIdHelper.lazySet(res.backendId)),
140
148
  Stream.map((res) => omit(res, ['backendId'])),
141
149
  Stream.mapError((cause) =>
142
- cause._tag === 'RpcClientError' && Socket.isSocketError(cause.cause)
150
+ cause._tag === 'RpcClientError' && Socket.isSocketError(cause.cause) === true
143
151
  ? new IsOfflineError({ cause: cause.cause })
144
- : cause._tag === 'InvalidPullError'
152
+ : cause._tag === 'UnknownError' || cause._tag === 'BackendIdMismatchError'
145
153
  ? cause
146
- : InvalidPullError.make({ cause }),
154
+ : new UnknownError({ cause }),
147
155
  ),
148
156
  Stream.withSpan('pull'),
149
157
  ),
150
158
 
151
- push: (batch) =>
152
- Effect.gen(function* () {
153
- if (batch.length === 0) {
154
- return
155
- }
159
+ push: Effect.fn('push')(function* (batch) {
160
+ if (batch.length === 0) return
161
+
162
+ const encodePayload = (batch: ReadonlyArray<LiveStoreEvent.Global.Encoded>) => ({
163
+ storeId,
164
+ payload,
165
+ batch,
166
+ backendId: backendIdHelper.get(),
167
+ })
168
+
169
+ const chunksChunk = yield* Chunk.fromIterable(batch).pipe(
170
+ splitChunkBySize({
171
+ maxItems: MAX_PUSH_EVENTS_PER_REQUEST,
172
+ maxBytes: MAX_WS_MESSAGE_BYTES,
173
+ encode: encodePayload,
174
+ }),
175
+ Effect.mapError((cause) => new UnknownError({ cause })),
176
+ )
156
177
 
157
- return yield* rpcClient.SyncWsRpc.Push({
178
+ for (const sub of chunksChunk) {
179
+ yield* rpcClient.SyncWsRpc.Push({
158
180
  storeId,
159
181
  payload,
160
- batch,
182
+ batch: Chunk.toReadonlyArray(sub),
161
183
  backendId: backendIdHelper.get(),
162
184
  }).pipe(
163
185
  Effect.mapError((cause) =>
164
- cause._tag === 'InvalidPushError'
186
+ cause._tag === 'UnknownError' || cause._tag === 'ServerAheadError' || cause._tag === 'BackendIdMismatchError'
165
187
  ? cause
166
- : new InvalidPushError({ cause: new UnexpectedError({ cause }) }),
188
+ : new UnknownError({ cause }),
167
189
  ),
168
190
  )
169
- }).pipe(Effect.withSpan('push')),
191
+ }
192
+ }),
170
193
  ping,
171
194
  metadata: {
172
195
  name: '@livestore/cf-sync',
@@ -0,0 +1,18 @@
1
+ // Shared transport limits for Cloudflare sync provider
2
+ // Keep payloads comfortably below ~1MB frame caps across Cloudflare transports.
3
+ // References:
4
+ // - Durable Objects WebSockets + hibernation best practices:
5
+ // https://developers.cloudflare.com/durable-objects/best-practices/websockets/
6
+ // - Workers platform limits (general context):
7
+ // https://developers.cloudflare.com/workers/platform/limits/
8
+ // Empirically, frames just below 1MB can fail on hibernated DO WebSockets; we use 900_000 bytes to keep a safety margin.
9
+ export const MAX_TRANSPORT_PAYLOAD_BYTES = 900_000
10
+
11
+ export const MAX_WS_MESSAGE_BYTES = MAX_TRANSPORT_PAYLOAD_BYTES
12
+ export const MAX_DO_RPC_REQUEST_BYTES = MAX_TRANSPORT_PAYLOAD_BYTES
13
+ export const MAX_HTTP_REQUEST_BYTES = MAX_TRANSPORT_PAYLOAD_BYTES
14
+
15
+ // Upper bound for items per message/request. Mirrors server broadcast chunking.
16
+ // Not Cloudflare-enforced; chosen to balance payload size and latency.
17
+ export const MAX_PULL_EVENTS_PER_MESSAGE = 100
18
+ export const MAX_PUSH_EVENTS_PER_REQUEST = 100
@@ -1,10 +1,11 @@
1
- import { InvalidPullError, InvalidPushError } from '@livestore/common'
1
+ import { BackendIdMismatchError, ServerAheadError, UnknownError } from '@livestore/common'
2
2
  import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect'
3
+
3
4
  import * as SyncMessage from './sync-message-types.ts'
4
5
 
5
6
  const commonPayloadFields = {
6
7
  /**
7
- * While the storeId is already implied by the durable object, we still need the explicit storeId
8
+ * While the storeId is already implied by the Durable Object, we still need the explicit storeId
8
9
  * since a DO doesn't know its own id.name value. 🫠
9
10
  * https://community.cloudflare.com/t/how-can-i-get-the-name-of-a-durable-object-from-itself/505961/8
10
11
  */
@@ -34,7 +35,7 @@ export class SyncDoRpc extends RpcGroup.make(
34
35
  rpcRequestId: Schema.String,
35
36
  ...SyncMessage.PullResponse.fields,
36
37
  }),
37
- error: InvalidPullError,
38
+ error: Schema.Union(UnknownError, BackendIdMismatchError),
38
39
  stream: true,
39
40
  }),
40
41
  Rpc.make('SyncDoRpc.Push', {
@@ -43,7 +44,7 @@ export class SyncDoRpc extends RpcGroup.make(
43
44
  ...commonPayloadFields,
44
45
  },
45
46
  success: SyncMessage.PushAck,
46
- error: InvalidPushError,
47
+ error: Schema.Union(UnknownError, ServerAheadError, BackendIdMismatchError),
47
48
  }),
48
49
  Rpc.make('SyncDoRpc.Ping', {
49
50
  payload: {
@@ -1,5 +1,6 @@
1
- import { InvalidPullError, InvalidPushError, UnexpectedError } from '@livestore/common'
1
+ import { BackendIdMismatchError, ServerAheadError, UnknownError } from '@livestore/common'
2
2
  import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect'
3
+
3
4
  import * as SyncMessage from './sync-message-types.ts'
4
5
 
5
6
  /**
@@ -17,7 +18,7 @@ export class SyncHttpRpc extends RpcGroup.make(
17
18
  ...SyncMessage.PullRequest.fields,
18
19
  }),
19
20
  success: SyncMessage.PullResponse,
20
- error: InvalidPullError,
21
+ error: Schema.Union(UnknownError, BackendIdMismatchError),
21
22
  stream: true,
22
23
  }),
23
24
  Rpc.make('SyncHttpRpc.Push', {
@@ -27,7 +28,7 @@ export class SyncHttpRpc extends RpcGroup.make(
27
28
  ...SyncMessage.PushRequest.fields,
28
29
  }),
29
30
  success: SyncMessage.PushAck,
30
- error: InvalidPushError,
31
+ error: Schema.Union(UnknownError, ServerAheadError, BackendIdMismatchError),
31
32
  }),
32
33
  Rpc.make('SyncHttpRpc.Ping', {
33
34
  payload: Schema.Struct({
@@ -35,6 +36,6 @@ export class SyncHttpRpc extends RpcGroup.make(
35
36
  payload: Schema.optional(Schema.JsonValue),
36
37
  }),
37
38
  success: SyncMessage.Pong,
38
- error: UnexpectedError,
39
+ error: UnknownError,
39
40
  }),
40
41
  ) {}
package/src/common/mod.ts CHANGED
@@ -1,15 +1,17 @@
1
+ import { OversizeChunkItemError, splitChunkBySize } from '@livestore/common/sync'
1
2
  import { Schema } from '@livestore/utils/effect'
2
3
 
3
4
  export type { CfTypes } from '@livestore/common-cf'
4
-
5
+ export * from './constants.ts'
5
6
  export { SyncHttpRpc } from './http-rpc-schema.ts'
6
7
  export * as SyncMessage from './sync-message-types.ts'
8
+ export { OversizeChunkItemError, splitChunkBySize }
7
9
 
8
10
  export const SearchParamsSchema = Schema.Struct({
9
11
  storeId: Schema.String,
10
12
  payload: Schema.compose(Schema.StringFromUriComponent, Schema.parseJson(Schema.JsonValue)).pipe(Schema.UndefinedOr),
11
13
  // NOTE `do-rpc` is handled differently
12
- transport: Schema.Union(Schema.Literal('http'), Schema.Literal('ws')),
14
+ transport: Schema.Literal('http', 'ws'),
13
15
  })
14
16
 
15
17
  export type SearchParams = typeof SearchParamsSchema.Type
@@ -20,7 +20,7 @@ export const PullRequest = Schema.Struct({
20
20
  cursor: Schema.Option(
21
21
  Schema.Struct({
22
22
  backendId: BackendId,
23
- eventSequenceNumber: EventSequenceNumber.GlobalEventSequenceNumber,
23
+ eventSequenceNumber: EventSequenceNumber.Global.Schema,
24
24
  }),
25
25
  ),
26
26
  }).annotations({ title: '@livestore/sync-cf:PullRequest' })
@@ -30,7 +30,7 @@ export type PullRequest = typeof PullRequest.Type
30
30
  export const PullResponse = Schema.Struct({
31
31
  batch: Schema.Array(
32
32
  Schema.Struct({
33
- eventEncoded: LiveStoreEvent.AnyEncodedGlobal,
33
+ eventEncoded: LiveStoreEvent.Global.Encoded,
34
34
  metadata: Schema.Option(SyncMetadata),
35
35
  }),
36
36
  ),
@@ -48,7 +48,7 @@ export const emptyPullResponse = (backendId: string) =>
48
48
  export type PullResponse = typeof PullResponse.Type
49
49
 
50
50
  export const PushRequest = Schema.Struct({
51
- batch: Schema.Array(LiveStoreEvent.AnyEncodedGlobal),
51
+ batch: Schema.Array(LiveStoreEvent.Global.Encoded),
52
52
  backendId: Schema.Option(BackendId),
53
53
  }).annotations({ title: '@livestore/sync-cf:PushRequest' })
54
54
 
@@ -1,5 +1,6 @@
1
- import { InvalidPullError, InvalidPushError } from '@livestore/common'
1
+ import { BackendIdMismatchError, ServerAheadError, UnknownError } from '@livestore/common'
2
2
  import { Rpc, RpcGroup, Schema } from '@livestore/utils/effect'
3
+
3
4
  import * as SyncMessage from './sync-message-types.ts'
4
5
 
5
6
  /**
@@ -19,7 +20,7 @@ export class SyncWsRpc extends RpcGroup.make(
19
20
  ...SyncMessage.PullRequest.fields,
20
21
  }),
21
22
  success: SyncMessage.PullResponse,
22
- error: InvalidPullError,
23
+ error: Schema.Union(UnknownError, BackendIdMismatchError),
23
24
  stream: true,
24
25
  }),
25
26
  Rpc.make('SyncWsRpc.Push', {
@@ -29,7 +30,7 @@ export class SyncWsRpc extends RpcGroup.make(
29
30
  ...SyncMessage.PushRequest.fields,
30
31
  }),
31
32
  success: SyncMessage.PushAck,
32
- error: InvalidPushError,
33
+ error: Schema.Union(UnknownError, ServerAheadError, BackendIdMismatchError),
33
34
  }),
34
35
  // Ping <> Pong is handled by DO WS auto-response
35
36
  // TODO add admin RPCs
@@ -1,22 +0,0 @@
1
- import { Chunk } from '@livestore/utils/effect';
2
- /**
3
- * Configuration describing how to break a chunk into smaller payload-safe chunks.
4
- */
5
- export interface ChunkingOptions<A> {
6
- /** Maximum number of items that may appear in any emitted chunk. */
7
- readonly maxItems: number;
8
- /** Maximum encoded byte size allowed for any emitted chunk. */
9
- readonly maxBytes: number;
10
- /**
11
- * Callback that produces a JSON-serialisable structure whose byte size should
12
- * fit within {@link maxBytes}. This lets callers control framing overhead.
13
- */
14
- readonly encode: (items: ReadonlyArray<A>) => unknown;
15
- }
16
- /**
17
- * Derives a function that splits an input chunk into sub-chunks confined by
18
- * both item count and encoded byte size limits. Designed for transports with
19
- * strict frame caps (e.g. Cloudflare hibernated WebSockets).
20
- */
21
- export declare const splitChunkBySize: <A>(options: ChunkingOptions<A>) => (chunk: Chunk.Chunk<A>) => Chunk.Chunk<Chunk.Chunk<A>>;
22
- //# sourceMappingURL=ws-chunking.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ws-chunking.d.ts","sourceRoot":"","sources":["../../../src/cf-worker/do/ws-chunking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAI/C;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,OAAO,CAAA;CACtD;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC1B,CAAC,EAAE,SAAS,eAAe,CAAC,CAAC,CAAC,MAC9B,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAiDlD,CAAA"}
@@ -1,49 +0,0 @@
1
- import { Chunk } from '@livestore/utils/effect';
2
- const textEncoder = new TextEncoder();
3
- /**
4
- * Derives a function that splits an input chunk into sub-chunks confined by
5
- * both item count and encoded byte size limits. Designed for transports with
6
- * strict frame caps (e.g. Cloudflare hibernated WebSockets).
7
- */
8
- export const splitChunkBySize = (options) => (chunk) => {
9
- const maxItems = Math.max(1, options.maxItems);
10
- const maxBytes = Math.max(1, options.maxBytes);
11
- const encode = options.encode;
12
- const measure = (items) => {
13
- const encoded = encode(items);
14
- return textEncoder.encode(JSON.stringify(encoded)).byteLength;
15
- };
16
- const items = Chunk.toReadonlyArray(chunk);
17
- if (items.length === 0) {
18
- return Chunk.fromIterable([]);
19
- }
20
- const result = [];
21
- let current = [];
22
- const flushCurrent = () => {
23
- if (current.length > 0) {
24
- result.push(Chunk.fromIterable(current));
25
- current = [];
26
- }
27
- };
28
- for (const item of items) {
29
- current.push(item);
30
- const exceedsLimit = current.length > maxItems || measure(current) > maxBytes;
31
- if (exceedsLimit) {
32
- // remove the item we just added and emit the previous chunk if it exists
33
- const last = current.pop();
34
- flushCurrent();
35
- if (last !== undefined) {
36
- current = [last];
37
- const singleItemTooLarge = measure(current) > maxBytes;
38
- if (singleItemTooLarge || current.length > maxItems) {
39
- // Emit the oversized item on its own; downstream can decide how to handle it.
40
- result.push(Chunk.of(last));
41
- current = [];
42
- }
43
- }
44
- }
45
- }
46
- flushCurrent();
47
- return Chunk.fromIterable(result);
48
- };
49
- //# sourceMappingURL=ws-chunking.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ws-chunking.js","sourceRoot":"","sources":["../../../src/cf-worker/do/ws-chunking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAE/C,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAA;AAiBrC;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,CAAI,OAA2B,EAAE,EAAE,CACnC,CAAC,KAAqB,EAA+B,EAAE;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAE7B,MAAM,OAAO,GAAG,CAAC,KAAuB,EAAE,EAAE;QAC1C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC7B,OAAO,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAA;IAC/D,CAAC,CAAA;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,YAAY,CAAiB,EAAE,CAAC,CAAA;IAC/C,CAAC;IAED,MAAM,MAAM,GAA0B,EAAE,CAAA;IACxC,IAAI,OAAO,GAAa,EAAE,CAAA;IAE1B,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;YACxC,OAAO,GAAG,EAAE,CAAA;QACd,CAAC;IACH,CAAC,CAAA;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAClB,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAA;QAE7E,IAAI,YAAY,EAAE,CAAC;YACjB,yEAAyE;YACzE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAG,CAAA;YAC3B,YAAY,EAAE,CAAA;YAEd,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,OAAO,GAAG,CAAC,IAAI,CAAC,CAAA;gBAChB,MAAM,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,QAAQ,CAAA;gBACtD,IAAI,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;oBACpD,8EAA8E;oBAC9E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;oBAC3B,OAAO,GAAG,EAAE,CAAA;gBACd,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,YAAY,EAAE,CAAA;IAEd,OAAO,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;AACnC,CAAC,CAAA"}
@@ -1,76 +0,0 @@
1
- import { Chunk } from '@livestore/utils/effect'
2
-
3
- const textEncoder = new TextEncoder()
4
-
5
- /**
6
- * Configuration describing how to break a chunk into smaller payload-safe chunks.
7
- */
8
- export interface ChunkingOptions<A> {
9
- /** Maximum number of items that may appear in any emitted chunk. */
10
- readonly maxItems: number
11
- /** Maximum encoded byte size allowed for any emitted chunk. */
12
- readonly maxBytes: number
13
- /**
14
- * Callback that produces a JSON-serialisable structure whose byte size should
15
- * fit within {@link maxBytes}. This lets callers control framing overhead.
16
- */
17
- readonly encode: (items: ReadonlyArray<A>) => unknown
18
- }
19
-
20
- /**
21
- * Derives a function that splits an input chunk into sub-chunks confined by
22
- * both item count and encoded byte size limits. Designed for transports with
23
- * strict frame caps (e.g. Cloudflare hibernated WebSockets).
24
- */
25
- export const splitChunkBySize =
26
- <A>(options: ChunkingOptions<A>) =>
27
- (chunk: Chunk.Chunk<A>): Chunk.Chunk<Chunk.Chunk<A>> => {
28
- const maxItems = Math.max(1, options.maxItems)
29
- const maxBytes = Math.max(1, options.maxBytes)
30
- const encode = options.encode
31
-
32
- const measure = (items: ReadonlyArray<A>) => {
33
- const encoded = encode(items)
34
- return textEncoder.encode(JSON.stringify(encoded)).byteLength
35
- }
36
-
37
- const items = Chunk.toReadonlyArray(chunk)
38
- if (items.length === 0) {
39
- return Chunk.fromIterable<Chunk.Chunk<A>>([])
40
- }
41
-
42
- const result: Array<Chunk.Chunk<A>> = []
43
- let current: Array<A> = []
44
-
45
- const flushCurrent = () => {
46
- if (current.length > 0) {
47
- result.push(Chunk.fromIterable(current))
48
- current = []
49
- }
50
- }
51
-
52
- for (const item of items) {
53
- current.push(item)
54
- const exceedsLimit = current.length > maxItems || measure(current) > maxBytes
55
-
56
- if (exceedsLimit) {
57
- // remove the item we just added and emit the previous chunk if it exists
58
- const last = current.pop()!
59
- flushCurrent()
60
-
61
- if (last !== undefined) {
62
- current = [last]
63
- const singleItemTooLarge = measure(current) > maxBytes
64
- if (singleItemTooLarge || current.length > maxItems) {
65
- // Emit the oversized item on its own; downstream can decide how to handle it.
66
- result.push(Chunk.of(last))
67
- current = []
68
- }
69
- }
70
- }
71
- }
72
-
73
- flushCurrent()
74
-
75
- return Chunk.fromIterable(result)
76
- }