@livestore/sync-s2 0.4.0 → 0.5.0-dev.1

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 (46) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/api-schema.d.ts +19 -19
  3. package/dist/api-schema.d.ts.map +1 -1
  4. package/dist/api-schema.js +4 -4
  5. package/dist/api-schema.js.map +1 -1
  6. package/dist/api-schema.test.js +18 -13
  7. package/dist/api-schema.test.js.map +1 -1
  8. package/dist/decode.d.ts +1 -1
  9. package/dist/decode.d.ts.map +1 -1
  10. package/dist/decode.js +4 -4
  11. package/dist/decode.js.map +1 -1
  12. package/dist/http-client-generated.d.ts +337 -1051
  13. package/dist/http-client-generated.d.ts.map +1 -1
  14. package/dist/http-client-generated.js +224 -221
  15. package/dist/http-client-generated.js.map +1 -1
  16. package/dist/limits.d.ts +6 -8
  17. package/dist/limits.d.ts.map +1 -1
  18. package/dist/limits.js +11 -12
  19. package/dist/limits.js.map +1 -1
  20. package/dist/limits.test.js +1 -1
  21. package/dist/limits.test.js.map +1 -1
  22. package/dist/make-s2-url.d.ts +1 -1
  23. package/dist/make-s2-url.d.ts.map +1 -1
  24. package/dist/make-s2-url.js +5 -5
  25. package/dist/make-s2-url.js.map +1 -1
  26. package/dist/mod.js +7 -7
  27. package/dist/s2-proxy-helpers.d.ts.map +1 -1
  28. package/dist/s2-proxy-helpers.js +2 -2
  29. package/dist/sync-provider.d.ts +3 -3
  30. package/dist/sync-provider.d.ts.map +1 -1
  31. package/dist/sync-provider.js +15 -11
  32. package/dist/sync-provider.js.map +1 -1
  33. package/dist/types.d.ts +3 -3
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js +1 -1
  36. package/dist/types.js.map +1 -1
  37. package/package.json +44 -63
  38. package/src/api-schema.test.ts +15 -11
  39. package/src/api-schema.ts +3 -3
  40. package/src/decode.ts +4 -4
  41. package/src/http-client-generated.ts +186 -278
  42. package/src/limits.test.ts +1 -2
  43. package/src/limits.ts +20 -22
  44. package/src/make-s2-url.ts +5 -5
  45. package/src/sync-provider.ts +16 -13
  46. package/src/types.ts +1 -1
@@ -1,6 +1,5 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
1
  import type { EventSequenceNumber, LiveStoreEvent } from '@livestore/common/schema'
2
+ import { describe, expect, it } from 'vitest'
4
3
 
5
4
  import {
6
5
  chunkEventsForS2,
package/src/limits.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { LiveStoreEvent } from '@livestore/common/schema'
2
- import { splitChunkBySize } from '@livestore/common/sync'
3
- import { Chunk, Effect, Schema } from '@livestore/utils/effect'
2
+ import { splitArrayBySize } from '@livestore/common/sync'
3
+ import { Effect, ReadonlyArray as EffectArray, Schema } from '@livestore/utils/effect'
4
4
 
5
5
  const textEncoder = new TextEncoder()
6
6
 
@@ -19,13 +19,15 @@ export const MAX_BATCH_METERED_BYTES = 1_048_576 // 1 MiB
19
19
  */
20
20
  export const MAX_RECORDS_PER_BATCH = 1_000
21
21
 
22
- const LimitType = Schema.Literal('record-metered-bytes', 'batch-metered-bytes', 'batch-count')
22
+ const LimitType = Schema.Literals(['record-metered-bytes', 'batch-metered-bytes', 'batch-count'])
23
23
 
24
- export class S2LimitExceededError extends Schema.TaggedError<S2LimitExceededError>('~@livestore/sync-s2/S2LimitExceededError')('S2LimitExceededError', {
24
+ export class S2LimitExceededError extends Schema.TaggedError<S2LimitExceededError>(
25
+ '~@livestore/sync-s2/S2LimitExceededError',
26
+ )('S2LimitExceededError', {
25
27
  limitType: LimitType,
26
- max: Schema.Number,
27
- actual: Schema.Number,
28
- recordIndex: Schema.optional(Schema.Number),
28
+ max: Schema.Finite,
29
+ actual: Schema.Finite,
30
+ recordIndex: Schema.optional(Schema.Finite),
29
31
  }) {}
30
32
 
31
33
  export interface AppendRecordBody {
@@ -89,9 +91,8 @@ const convertEventsToPrepared = (events: ReadonlyArray<LiveStoreEvent.Global.Enc
89
91
  const makeChunkMeasure = (items: ReadonlyArray<PreparedEvent>): number =>
90
92
  items.reduce((acc, item) => acc + item.meteredBytes, 0)
91
93
 
92
- const mapPreparedChunks = (chunks: Chunk.Chunk<Chunk.Chunk<PreparedEvent>>): ReadonlyArray<S2Chunk> =>
93
- Chunk.toReadonlyArray(chunks).map((chunk) => {
94
- const chunkItems = Chunk.toReadonlyArray(chunk)
94
+ const mapPreparedChunks = (chunks: ReadonlyArray<ReadonlyArray<PreparedEvent>>): ReadonlyArray<S2Chunk> =>
95
+ chunks.map((chunkItems) => {
95
96
  const events = chunkItems.map((item) => item.event)
96
97
  const records = chunkItems.map((item) => item.record)
97
98
  return {
@@ -102,22 +103,19 @@ const mapPreparedChunks = (chunks: Chunk.Chunk<Chunk.Chunk<PreparedEvent>>): Rea
102
103
  })
103
104
 
104
105
  export const chunkEventsForS2 = (events: ReadonlyArray<LiveStoreEvent.Global.Encoded>): ReadonlyArray<S2Chunk> => {
105
- if (events.length === 0) {
106
+ const prepared = convertEventsToPrepared(events)
107
+
108
+ if (EffectArray.isReadonlyArrayNonEmpty(prepared) === false) {
106
109
  return []
107
110
  }
108
111
 
109
- const prepared = convertEventsToPrepared(events)
110
-
111
112
  try {
112
- const chunks = Chunk.fromIterable(prepared).pipe(
113
- splitChunkBySize({
114
- maxItems: MAX_RECORDS_PER_BATCH,
115
- maxBytes: MAX_BATCH_METERED_BYTES,
116
- encode: (items) => ({ records: items.map((item) => item.record) }),
117
- measure: makeChunkMeasure,
118
- }),
119
- Effect.runSync,
120
- )
113
+ const chunks = splitArrayBySize<PreparedEvent>({
114
+ maxItems: MAX_RECORDS_PER_BATCH,
115
+ maxBytes: MAX_BATCH_METERED_BYTES,
116
+ encode: (items) => ({ records: items.map((item) => item.record) }),
117
+ measure: makeChunkMeasure,
118
+ })(prepared).pipe(Effect.runSync)
121
119
 
122
120
  return mapPreparedChunks(chunks)
123
121
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { shouldNeverHappen } from '@livestore/utils'
2
- import { Schema } from '@livestore/utils/effect'
2
+ import { Result, Schema } from '@livestore/utils/effect'
3
3
 
4
4
  import * as ApiSchema from './api-schema.ts'
5
5
 
@@ -8,11 +8,11 @@ export const makeS2StreamName = (storeId: string) => storeId.replace(/[^a-zA-Z0-
8
8
  /**
9
9
  * Decode `args` from URLSearchParams using Effect Schema, mirroring Electric's approach.
10
10
  */
11
- export const decodePullArgsFromSearchParams = (searchParams: URLSearchParams): typeof ApiSchema.PullArgs.Type => {
11
+ export const decodePullArgsFromSearchParams = (searchParams: URLSearchParams): ApiSchema.PullArgs => {
12
12
  const UrlParamsSchema = Schema.Struct({ args: ApiSchema.ArgsSchema })
13
- const argsResult = Schema.decodeUnknownEither(UrlParamsSchema)(Object.fromEntries(searchParams.entries()))
13
+ const argsResult = Schema.decodeUnknownResult(UrlParamsSchema)(Object.fromEntries(searchParams.entries()))
14
14
 
15
- if (argsResult._tag === 'Left') {
15
+ if (Result.isFailure(argsResult) === true) {
16
16
  return shouldNeverHappen(
17
17
  'Invalid search params provided to decodePullArgsFromSearchParams',
18
18
  searchParams,
@@ -20,5 +20,5 @@ export const decodePullArgsFromSearchParams = (searchParams: URLSearchParams): t
20
20
  )
21
21
  }
22
22
 
23
- return argsResult.right.args
23
+ return argsResult.success.args
24
24
  }
@@ -37,16 +37,17 @@ import { shouldNeverHappen } from '@livestore/utils'
37
37
  import {
38
38
  type Duration,
39
39
  Effect,
40
+ Filter,
40
41
  HttpClient,
41
42
  HttpClientRequest,
42
43
  HttpClientResponse,
43
44
  Option,
44
45
  Schedule,
45
46
  Schema,
46
- Sse,
47
47
  Stream,
48
48
  SubscriptionRef,
49
49
  } from '@livestore/utils/effect'
50
+ import { Sse } from 'effect/encoding'
50
51
 
51
52
  import * as ApiSchema from './api-schema.ts'
52
53
  import { decodeReadBatch } from './decode.ts'
@@ -66,9 +67,9 @@ export interface SyncS2Options {
66
67
  /** Enable periodic ping; default true */
67
68
  enabled?: boolean
68
69
  /** Timeout for individual ping request; default 10s */
69
- requestTimeout?: Duration.DurationInput
70
+ requestTimeout?: Duration.Input
70
71
  /** Interval between ping requests; default 10s */
71
- requestInterval?: Duration.DurationInput
72
+ requestInterval?: Duration.Input
72
73
  }
73
74
  retry?: {
74
75
  /** Custom retry schedule for non-live pulls (default: 2 recurs, 100ms spaced) */
@@ -78,7 +79,7 @@ export interface SyncS2Options {
78
79
  }
79
80
  }
80
81
 
81
- export const defaultRetry = Schedule.compose(Schedule.recurs(2), Schedule.spaced(100))
82
+ export const defaultRetry = Schedule.recurs(2).pipe(Schedule.addDelay(() => Effect.succeed(100)))
82
83
 
83
84
  const getBrowserOrigin = () => {
84
85
  if (typeof globalThis !== 'object' || globalThis === null || !('location' in globalThis)) {
@@ -114,8 +115,10 @@ export const makeSyncBackend =
114
115
  yield* SubscriptionRef.set(isConnected, true)
115
116
  }).pipe(
116
117
  UnknownError.mapToUnknownError,
117
- Effect.timeout(pingTimeout),
118
- Effect.catchTag('TimeoutException', () => SubscriptionRef.set(isConnected, false)),
118
+ Effect.timeoutOrElse({
119
+ duration: pingTimeout,
120
+ orElse: () => SubscriptionRef.set(isConnected, false),
121
+ }),
119
122
  )
120
123
 
121
124
  const pingInterval = pingOptions?.requestInterval ?? 10_000
@@ -149,8 +152,8 @@ export const makeSyncBackend =
149
152
  .pipe(
150
153
  HttpClientResponse.stream,
151
154
  // decode text and split into lines
152
- Stream.decodeText('utf8'),
153
- Stream.pipeThroughChannel(Sse.makeChannel()),
155
+ Stream.decodeText({ encoding: 'utf8' }),
156
+ Stream.pipeThroughChannel(Sse.decode()),
154
157
  // Filter out pings, map errors to stream failures
155
158
  Stream.mapEffect(
156
159
  Effect.fnUntraced(function* (msg) {
@@ -160,7 +163,9 @@ export const makeSyncBackend =
160
163
  return yield* new UnknownError({ cause: new Error(`SSE error: ${msg.data}`) })
161
164
  }
162
165
  if (evt === 'batch') {
163
- const readBatch = yield* Schema.decode(Schema.parseJson(HttpClientGenerated.ReadBatch))(msg.data)
166
+ const readBatch = yield* Schema.decodeEffect(Schema.fromJsonString(HttpClientGenerated.ReadBatch))(
167
+ msg.data,
168
+ )
164
169
  const batch = decodeReadBatch(readBatch)
165
170
 
166
171
  const lastS2SeqNum = batch.at(-1)?.metadata.pipe(
@@ -188,10 +193,8 @@ export const makeSyncBackend =
188
193
  return shouldNeverHappen(`Unexpected SSE event: ${evt}`, msg)
189
194
  }),
190
195
  ),
191
- Stream.filterMap((_) => _), // filter out Option.none()
192
- Stream.mapError((cause) =>
193
- cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }),
194
- ),
196
+ Stream.filterMap(Filter.fromPredicateOption((_) => _)), // filter out Option.none()
197
+ Stream.mapError((cause) => (cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }))),
195
198
  Stream.retry(retry?.pull ?? defaultRetry),
196
199
  )
197
200
  }
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ import { Brand, Schema } from '@livestore/utils/effect'
12
12
  */
13
13
  export type S2SeqNum = Brand.Branded<number, 'S2SeqNum'>
14
14
  export const s2SeqNum = Brand.nominal<S2SeqNum>()
15
- export const S2SeqNum = Schema.fromBrand(s2SeqNum)(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)))
15
+ export const S2SeqNum = Schema.fromBrand('S2SeqNum', s2SeqNum)(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
16
16
 
17
17
  /**
18
18
  * Metadata for tracking S2-specific cursor information.