@livestore/sync-s2 0.4.0 → 0.5.0-dev.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.
@@ -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,9 +19,11 @@ 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
28
  max: Schema.Number,
27
29
  actual: Schema.Number,
@@ -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
 
@@ -10,9 +10,9 @@ export const makeS2StreamName = (storeId: string) => storeId.replace(/[^a-zA-Z0-
10
10
  */
11
11
  export const decodePullArgsFromSearchParams = (searchParams: URLSearchParams): typeof ApiSchema.PullArgs.Type => {
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/unstable/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)) {
@@ -115,7 +116,7 @@ export const makeSyncBackend =
115
116
  }).pipe(
116
117
  UnknownError.mapToUnknownError,
117
118
  Effect.timeout(pingTimeout),
118
- Effect.catchTag('TimeoutException', () => SubscriptionRef.set(isConnected, false)),
119
+ Effect.catchTag('TimeoutError', () => SubscriptionRef.set(isConnected, false)),
119
120
  )
120
121
 
121
122
  const pingInterval = pingOptions?.requestInterval ?? 10_000
@@ -149,8 +150,8 @@ export const makeSyncBackend =
149
150
  .pipe(
150
151
  HttpClientResponse.stream,
151
152
  // decode text and split into lines
152
- Stream.decodeText('utf8'),
153
- Stream.pipeThroughChannel(Sse.makeChannel()),
153
+ Stream.decodeText({ encoding: 'utf8' }),
154
+ Stream.pipeThroughChannel(Sse.decode()),
154
155
  // Filter out pings, map errors to stream failures
155
156
  Stream.mapEffect(
156
157
  Effect.fnUntraced(function* (msg) {
@@ -160,7 +161,9 @@ export const makeSyncBackend =
160
161
  return yield* new UnknownError({ cause: new Error(`SSE error: ${msg.data}`) })
161
162
  }
162
163
  if (evt === 'batch') {
163
- const readBatch = yield* Schema.decode(Schema.parseJson(HttpClientGenerated.ReadBatch))(msg.data)
164
+ const readBatch = yield* Schema.decodeEffect(Schema.fromJsonString(HttpClientGenerated.ReadBatch))(
165
+ msg.data,
166
+ )
164
167
  const batch = decodeReadBatch(readBatch)
165
168
 
166
169
  const lastS2SeqNum = batch.at(-1)?.metadata.pipe(
@@ -188,10 +191,8 @@ export const makeSyncBackend =
188
191
  return shouldNeverHappen(`Unexpected SSE event: ${evt}`, msg)
189
192
  }),
190
193
  ),
191
- Stream.filterMap((_) => _), // filter out Option.none()
192
- Stream.mapError((cause) =>
193
- cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }),
194
- ),
194
+ Stream.filterMap(Filter.fromPredicateOption((_) => _)), // filter out Option.none()
195
+ Stream.mapError((cause) => (cause._tag === 'UnknownError' ? cause : new UnknownError({ cause }))),
195
196
  Stream.retry(retry?.pull ?? defaultRetry),
196
197
  )
197
198
  }
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.