@livestore/sync-s2 0.4.0-dev.22 → 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.
@@ -28,10 +28,10 @@
28
28
  * DO NOT couple these systems together or assume 1:1 correspondence.
29
29
  *
30
30
  * Errors
31
- * - push → InvalidPushError on non‑2xx; pull → InvalidPullError on non‑2xx; ping/connect map timeouts to offline.
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 { InvalidPullError, InvalidPushError, SyncBackend, UnknownError } from '@livestore/common'
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, InvalidPullError>
75
+ pull?: Schedule.Schedule<number, UnknownError>
75
76
  /** Custom retry schedule for pushes (default: 2 recurs, 100ms spaced) */
76
- push?: Schedule.Schedule<number, InvalidPushError>
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'] = pullEndpointHasSameOrigin
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>, InvalidPullError> => {
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 InvalidPullError({ cause: new Error(`SSE error: ${msg.data}`) })
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) => (cause._tag === 'InvalidPullError' ? cause : new InvalidPullError({ 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>, InvalidPullError> => {
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 (!lastBatchItem) return Option.none()
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>, InvalidPullError> => {
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 makeInvalidPushError = (cause: unknown): InvalidPushError => {
253
- if (cause instanceof InvalidPushError) {
254
- return cause
255
- }
256
-
266
+ const toUnknownError = (cause: unknown): UnknownError => {
257
267
  if (cause instanceof UnknownError) {
258
- return new InvalidPushError({ cause })
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 InvalidPushError({
268
- cause: new UnknownError({
269
- cause,
270
- note,
271
- payload: {
272
- limitType: cause.limitType,
273
- max: cause.max,
274
- actual: cause.actual,
275
- recordIndex: cause.recordIndex,
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 InvalidPushError({ cause: new UnknownError({ cause }) })
289
+ return new UnknownError({ cause })
282
290
  }
283
291
 
284
- const chunks = yield* Effect.sync(() => chunkEventsForS2(batch)).pipe(Effect.mapError(makeInvalidPushError))
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(makeInvalidPushError),
301
+ Effect.mapError(toUnknownError),
294
302
  Effect.retry(retry?.push ?? defaultRetry),
295
303
  )
296
304
  }