@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/api-schema.d.ts +19 -19
- package/dist/api-schema.d.ts.map +1 -1
- package/dist/api-schema.js +4 -4
- package/dist/api-schema.js.map +1 -1
- package/dist/api-schema.test.js +18 -13
- package/dist/api-schema.test.js.map +1 -1
- package/dist/decode.d.ts +1 -1
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +4 -4
- package/dist/decode.js.map +1 -1
- package/dist/http-client-generated.d.ts +337 -1051
- package/dist/http-client-generated.d.ts.map +1 -1
- package/dist/http-client-generated.js +224 -221
- package/dist/http-client-generated.js.map +1 -1
- package/dist/limits.d.ts +6 -8
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -12
- package/dist/limits.js.map +1 -1
- package/dist/limits.test.js +1 -1
- package/dist/limits.test.js.map +1 -1
- package/dist/make-s2-url.d.ts +1 -1
- package/dist/make-s2-url.d.ts.map +1 -1
- package/dist/make-s2-url.js +5 -5
- package/dist/make-s2-url.js.map +1 -1
- package/dist/mod.js +7 -7
- package/dist/s2-proxy-helpers.d.ts.map +1 -1
- package/dist/s2-proxy-helpers.js +2 -2
- package/dist/sync-provider.d.ts +3 -3
- package/dist/sync-provider.d.ts.map +1 -1
- package/dist/sync-provider.js +15 -11
- package/dist/sync-provider.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +44 -63
- package/src/api-schema.test.ts +15 -11
- package/src/api-schema.ts +3 -3
- package/src/decode.ts +4 -4
- package/src/http-client-generated.ts +186 -278
- package/src/limits.test.ts +1 -2
- package/src/limits.ts +20 -22
- package/src/make-s2-url.ts +5 -5
- package/src/sync-provider.ts +16 -13
- package/src/types.ts +1 -1
package/src/limits.test.ts
CHANGED
package/src/limits.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LiveStoreEvent } from '@livestore/common/schema'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
22
|
+
const LimitType = Schema.Literals(['record-metered-bytes', 'batch-metered-bytes', 'batch-count'])
|
|
23
23
|
|
|
24
|
-
export class S2LimitExceededError extends Schema.TaggedError<S2LimitExceededError>(
|
|
24
|
+
export class S2LimitExceededError extends Schema.TaggedError<S2LimitExceededError>(
|
|
25
|
+
'~@livestore/sync-s2/S2LimitExceededError',
|
|
26
|
+
)('S2LimitExceededError', {
|
|
25
27
|
limitType: LimitType,
|
|
26
|
-
max: Schema.
|
|
27
|
-
actual: Schema.
|
|
28
|
-
recordIndex: Schema.optional(Schema.
|
|
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:
|
|
93
|
-
|
|
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
|
-
|
|
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 =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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) {
|
package/src/make-s2-url.ts
CHANGED
|
@@ -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):
|
|
11
|
+
export const decodePullArgsFromSearchParams = (searchParams: URLSearchParams): ApiSchema.PullArgs => {
|
|
12
12
|
const UrlParamsSchema = Schema.Struct({ args: ApiSchema.ArgsSchema })
|
|
13
|
-
const argsResult = Schema.
|
|
13
|
+
const argsResult = Schema.decodeUnknownResult(UrlParamsSchema)(Object.fromEntries(searchParams.entries()))
|
|
14
14
|
|
|
15
|
-
if (argsResult
|
|
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.
|
|
23
|
+
return argsResult.success.args
|
|
24
24
|
}
|
package/src/sync-provider.ts
CHANGED
|
@@ -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.
|
|
70
|
+
requestTimeout?: Duration.Input
|
|
70
71
|
/** Interval between ping requests; default 10s */
|
|
71
|
-
requestInterval?: Duration.
|
|
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.
|
|
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.
|
|
118
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|