@livestore/sync-s2 0.4.0-dev.21 → 0.4.0-dev.23
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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/api-schema.d.ts.map +1 -1
- package/dist/api-schema.js.map +1 -1
- package/dist/api-schema.test.js +1 -1
- package/dist/api-schema.test.js.map +1 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js.map +1 -1
- package/dist/http-client-generated.d.ts +22 -22
- package/dist/http-client-generated.d.ts.map +1 -1
- package/dist/http-client-generated.js +64 -22
- package/dist/http-client-generated.js.map +1 -1
- package/dist/limits.js +2 -2
- package/dist/limits.js.map +1 -1
- package/dist/limits.test.js.map +1 -1
- package/dist/make-s2-url.d.ts.map +1 -1
- package/dist/make-s2-url.js.map +1 -1
- package/dist/s2-proxy-helpers.d.ts +10 -2
- package/dist/s2-proxy-helpers.d.ts.map +1 -1
- package/dist/s2-proxy-helpers.js +16 -10
- package/dist/s2-proxy-helpers.js.map +1 -1
- package/dist/sync-provider.d.ts +4 -4
- package/dist/sync-provider.d.ts.map +1 -1
- package/dist/sync-provider.js +31 -31
- package/dist/sync-provider.js.map +1 -1
- package/package.json +76 -13
- package/src/api-schema.test.ts +3 -1
- package/src/api-schema.ts +1 -0
- package/src/decode.ts +1 -0
- package/src/http-client-generated.ts +74 -53
- package/src/limits.test.ts +3 -1
- package/src/limits.ts +2 -2
- package/src/make-s2-url.ts +1 -0
- package/src/s2-proxy-helpers.ts +27 -10
- package/src/sync-provider.ts +45 -37
package/src/sync-provider.ts
CHANGED
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
* DO NOT couple these systems together or assume 1:1 correspondence.
|
|
29
29
|
*
|
|
30
30
|
* Errors
|
|
31
|
-
* - push →
|
|
31
|
+
* - push → UnknownError on non‑2xx; pull → UnknownError on non‑2xx; ping/connect map timeouts to offline.
|
|
32
32
|
* - The proxy should surface helpful status codes and error bodies.
|
|
33
33
|
*/
|
|
34
|
-
import {
|
|
34
|
+
import { SyncBackend, UnknownError } from '@livestore/common'
|
|
35
35
|
import type { EventSequenceNumber } from '@livestore/common/schema'
|
|
36
36
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
37
37
|
import {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
Stream,
|
|
48
48
|
SubscriptionRef,
|
|
49
49
|
} from '@livestore/utils/effect'
|
|
50
|
+
|
|
50
51
|
import * as ApiSchema from './api-schema.ts'
|
|
51
52
|
import { decodeReadBatch } from './decode.ts'
|
|
52
53
|
import * as HttpClientGenerated from './http-client-generated.ts'
|
|
@@ -71,14 +72,26 @@ export interface SyncS2Options {
|
|
|
71
72
|
}
|
|
72
73
|
retry?: {
|
|
73
74
|
/** Custom retry schedule for non-live pulls (default: 2 recurs, 100ms spaced) */
|
|
74
|
-
pull?: Schedule.Schedule<number,
|
|
75
|
+
pull?: Schedule.Schedule<number, UnknownError>
|
|
75
76
|
/** Custom retry schedule for pushes (default: 2 recurs, 100ms spaced) */
|
|
76
|
-
push?: Schedule.Schedule<number,
|
|
77
|
+
push?: Schedule.Schedule<number, UnknownError>
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export const defaultRetry = Schedule.compose(Schedule.recurs(2), Schedule.spaced(100))
|
|
81
82
|
|
|
83
|
+
const getBrowserOrigin = () => {
|
|
84
|
+
if (typeof globalThis !== 'object' || globalThis === null || !('location' in globalThis)) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { location } = globalThis as typeof globalThis & {
|
|
89
|
+
location?: { origin?: unknown } | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return typeof location?.origin === 'string' ? location.origin : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
export const makeSyncBackend =
|
|
83
96
|
({ endpoint, ping: pingOptions, retry }: SyncS2Options): SyncBackend.SyncBackendConstructor<SyncMetadata> =>
|
|
84
97
|
({ storeId, payload }) =>
|
|
@@ -90,9 +103,9 @@ export const makeSyncBackend =
|
|
|
90
103
|
|
|
91
104
|
const httpClient = yield* HttpClient.HttpClient
|
|
92
105
|
|
|
106
|
+
const browserOrigin = getBrowserOrigin()
|
|
93
107
|
const pullEndpointHasSameOrigin =
|
|
94
|
-
pullEndpoint.startsWith('/') ||
|
|
95
|
-
(globalThis.location !== undefined && globalThis.location.origin === new URL(pullEndpoint).origin)
|
|
108
|
+
pullEndpoint.startsWith('/') || (browserOrigin !== undefined && browserOrigin === new URL(pullEndpoint).origin)
|
|
96
109
|
|
|
97
110
|
const pingTimeout = pingOptions?.requestTimeout ?? 10_000
|
|
98
111
|
|
|
@@ -111,9 +124,8 @@ export const makeSyncBackend =
|
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
// No need to connect if the pull endpoint has the same origin as the current page
|
|
114
|
-
const connect: SyncBackend.SyncBackend<SyncMetadata>['connect'] =
|
|
115
|
-
? Effect.void
|
|
116
|
-
: ping.pipe(UnknownError.mapToUnknownError)
|
|
127
|
+
const connect: SyncBackend.SyncBackend<SyncMetadata>['connect'] =
|
|
128
|
+
pullEndpointHasSameOrigin === true ? Effect.void : ping.pipe(UnknownError.mapToUnknownError)
|
|
117
129
|
|
|
118
130
|
const runPullSse = (
|
|
119
131
|
cursor: Option.Option<{
|
|
@@ -121,7 +133,7 @@ export const makeSyncBackend =
|
|
|
121
133
|
metadata: Option.Option<SyncMetadata>
|
|
122
134
|
}>,
|
|
123
135
|
live: boolean,
|
|
124
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
136
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
125
137
|
// Extract S2 seqNum from metadata for SSE cursor
|
|
126
138
|
const s2SeqNum = cursor.pipe(
|
|
127
139
|
Option.flatMap((_) => _.metadata),
|
|
@@ -145,7 +157,7 @@ export const makeSyncBackend =
|
|
|
145
157
|
const evt = msg.event.toLowerCase()
|
|
146
158
|
if (evt === 'ping') return Option.none()
|
|
147
159
|
if (evt === 'error') {
|
|
148
|
-
return yield* new
|
|
160
|
+
return yield* new UnknownError({ cause: new Error(`SSE error: ${msg.data}`) })
|
|
149
161
|
}
|
|
150
162
|
if (evt === 'batch') {
|
|
151
163
|
const readBatch = yield* Schema.decode(Schema.parseJson(HttpClientGenerated.ReadBatch))(msg.data)
|
|
@@ -177,7 +189,9 @@ export const makeSyncBackend =
|
|
|
177
189
|
}),
|
|
178
190
|
),
|
|
179
191
|
Stream.filterMap((_) => _), // filter out Option.none()
|
|
180
|
-
Stream.mapError((cause) =>
|
|
192
|
+
Stream.mapError((cause) =>
|
|
193
|
+
cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }),
|
|
194
|
+
),
|
|
181
195
|
Stream.retry(retry?.pull ?? defaultRetry),
|
|
182
196
|
)
|
|
183
197
|
}
|
|
@@ -187,7 +201,7 @@ export const makeSyncBackend =
|
|
|
187
201
|
eventSequenceNumber: EventSequenceNumber.Global.Type
|
|
188
202
|
metadata: Option.Option<SyncMetadata>
|
|
189
203
|
}>,
|
|
190
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
204
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
191
205
|
const computeNextCursor = (
|
|
192
206
|
lastItem: Option.Option<SyncBackend.PullResItem<SyncMetadata>>,
|
|
193
207
|
current: Option.Option<{
|
|
@@ -198,7 +212,7 @@ export const makeSyncBackend =
|
|
|
198
212
|
lastItem.pipe(
|
|
199
213
|
Option.flatMap((item) => {
|
|
200
214
|
const lastBatchItem = item.batch.at(-1)
|
|
201
|
-
if (
|
|
215
|
+
if (lastBatchItem == null) return Option.none()
|
|
202
216
|
return Option.some({
|
|
203
217
|
eventSequenceNumber: lastBatchItem.eventEncoded.seqNum,
|
|
204
218
|
metadata: lastBatchItem.metadata,
|
|
@@ -213,7 +227,7 @@ export const makeSyncBackend =
|
|
|
213
227
|
metadata: Option.Option<SyncMetadata>
|
|
214
228
|
}>,
|
|
215
229
|
isFirst: boolean,
|
|
216
|
-
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>,
|
|
230
|
+
): Stream.Stream<SyncBackend.PullResItem<SyncMetadata>, UnknownError> => {
|
|
217
231
|
const sseStream = (live: boolean) =>
|
|
218
232
|
runPullSse(cursor, live).pipe(
|
|
219
233
|
Stream.emitIfEmpty({
|
|
@@ -222,7 +236,7 @@ export const makeSyncBackend =
|
|
|
222
236
|
} as SyncBackend.PullResItem<SyncMetadata>),
|
|
223
237
|
)
|
|
224
238
|
|
|
225
|
-
const stream = isFirst ? sseStream(false) : sseStream(true)
|
|
239
|
+
const stream = isFirst === true ? sseStream(false) : sseStream(true)
|
|
226
240
|
|
|
227
241
|
return stream.pipe(
|
|
228
242
|
// Reconnect from last item if stream
|
|
@@ -236,7 +250,7 @@ export const makeSyncBackend =
|
|
|
236
250
|
return SyncBackend.of({
|
|
237
251
|
connect,
|
|
238
252
|
pull: (cursor, options) => {
|
|
239
|
-
if (options?.live) {
|
|
253
|
+
if (options?.live === true) {
|
|
240
254
|
return ssePull(cursor)
|
|
241
255
|
} else {
|
|
242
256
|
return runPullSse(cursor, false).pipe(
|
|
@@ -249,13 +263,9 @@ export const makeSyncBackend =
|
|
|
249
263
|
},
|
|
250
264
|
push: (batch) =>
|
|
251
265
|
Effect.gen(function* () {
|
|
252
|
-
const
|
|
253
|
-
if (cause instanceof InvalidPushError) {
|
|
254
|
-
return cause
|
|
255
|
-
}
|
|
256
|
-
|
|
266
|
+
const toUnknownError = (cause: unknown): UnknownError => {
|
|
257
267
|
if (cause instanceof UnknownError) {
|
|
258
|
-
return
|
|
268
|
+
return cause
|
|
259
269
|
}
|
|
260
270
|
|
|
261
271
|
if (cause instanceof S2LimitExceededError) {
|
|
@@ -264,24 +274,22 @@ export const makeSyncBackend =
|
|
|
264
274
|
? `S2 record exceeded ${cause.max} metered bytes (actual: ${cause.actual})`
|
|
265
275
|
: `S2 batch exceeded ${cause.max} (type: ${cause.limitType}, actual: ${cause.actual})`
|
|
266
276
|
|
|
267
|
-
return new
|
|
268
|
-
cause
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
},
|
|
277
|
-
}),
|
|
277
|
+
return new UnknownError({
|
|
278
|
+
cause,
|
|
279
|
+
note,
|
|
280
|
+
payload: {
|
|
281
|
+
limitType: cause.limitType,
|
|
282
|
+
max: cause.max,
|
|
283
|
+
actual: cause.actual,
|
|
284
|
+
recordIndex: cause.recordIndex,
|
|
285
|
+
},
|
|
278
286
|
})
|
|
279
287
|
}
|
|
280
288
|
|
|
281
|
-
return new
|
|
289
|
+
return new UnknownError({ cause })
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
const chunks = yield* Effect.sync(() => chunkEventsForS2(batch)).pipe(Effect.mapError(
|
|
292
|
+
const chunks = yield* Effect.sync(() => chunkEventsForS2(batch)).pipe(Effect.mapError(toUnknownError))
|
|
285
293
|
|
|
286
294
|
for (const chunk of chunks) {
|
|
287
295
|
yield* HttpClientRequest.schemaBodyJson(ApiSchema.PushPayload)(HttpClientRequest.post(pushEndpoint), {
|
|
@@ -290,7 +298,7 @@ export const makeSyncBackend =
|
|
|
290
298
|
}).pipe(
|
|
291
299
|
Effect.andThen(httpClient.pipe(HttpClient.filterStatusOk).execute),
|
|
292
300
|
Effect.andThen(HttpClientResponse.schemaBodyJson(ApiSchema.PushResponse)),
|
|
293
|
-
Effect.mapError(
|
|
301
|
+
Effect.mapError(toUnknownError),
|
|
294
302
|
Effect.retry(retry?.push ?? defaultRetry),
|
|
295
303
|
)
|
|
296
304
|
}
|