@mepuka/skygent 0.2.0 → 0.3.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/README.md +269 -31
- package/index.ts +18 -3
- package/package.json +1 -1
- package/src/cli/app.ts +4 -2
- package/src/cli/compact-output.ts +52 -0
- package/src/cli/config.ts +46 -4
- package/src/cli/doc/table-renderers.ts +29 -0
- package/src/cli/doc/thread.ts +2 -4
- package/src/cli/exit-codes.ts +2 -0
- package/src/cli/feed.ts +78 -61
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +13 -11
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +83 -5
- package/src/cli/graph.ts +297 -169
- package/src/cli/input.ts +45 -0
- package/src/cli/interval.ts +4 -33
- package/src/cli/jetstream.ts +2 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/option-schemas.ts +22 -0
- package/src/cli/output-format.ts +11 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +17 -0
- package/src/cli/parse-errors.ts +30 -0
- package/src/cli/parse.ts +1 -47
- package/src/cli/pipe-input.ts +18 -0
- package/src/cli/pipe.ts +154 -0
- package/src/cli/post.ts +88 -66
- package/src/cli/query-fields.ts +13 -3
- package/src/cli/query.ts +354 -100
- package/src/cli/search.ts +93 -136
- package/src/cli/shared-options.ts +11 -63
- package/src/cli/shared.ts +1 -20
- package/src/cli/store-errors.ts +28 -21
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +41 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +24 -7
- package/src/cli/sync.ts +46 -67
- package/src/cli/thread-options.ts +25 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +29 -32
- package/src/cli/watch.ts +55 -26
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/domain/primitives.ts +20 -3
- package/src/graph/relationships.ts +129 -0
- package/src/services/bsky-client.ts +11 -5
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/shared.ts +48 -1
- package/src/services/store-cleaner.ts +5 -2
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +288 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +149 -89
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
|
@@ -59,8 +59,9 @@
|
|
|
59
59
|
* @module services/sync-engine
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
import { Clock, Context, Duration, Effect, Layer, Option, Ref, Schedule, Schema, Stream } from "effect";
|
|
62
|
+
import { Chunk, Clock, Context, Duration, Effect, Fiber, Layer, Option, Ref, Schedule, Schema, Stream } from "effect";
|
|
63
63
|
import { messageFromCause } from "./shared.js";
|
|
64
|
+
import type { BskyError } from "../domain/errors.js";
|
|
64
65
|
import { FilterRuntime } from "./filter-runtime.js";
|
|
65
66
|
import { PostParser } from "./post-parser.js";
|
|
66
67
|
import { StoreCommitter } from "./store-commit.js";
|
|
@@ -68,6 +69,7 @@ import { BskyClient } from "./bsky-client.js";
|
|
|
68
69
|
import type { FilterExpr } from "../domain/filter.js";
|
|
69
70
|
import { filterExprSignature } from "../domain/filter.js";
|
|
70
71
|
import { EventMeta, PostUpsert } from "../domain/events.js";
|
|
72
|
+
import type { EventLogEntry } from "../domain/events.js";
|
|
71
73
|
import type { Post } from "../domain/post.js";
|
|
72
74
|
import { EventSeq, Timestamp } from "../domain/primitives.js";
|
|
73
75
|
import type { RawPost } from "../domain/raw.js";
|
|
@@ -154,7 +156,7 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
154
156
|
source: DataSource,
|
|
155
157
|
target: StoreRef,
|
|
156
158
|
filter: FilterExpr,
|
|
157
|
-
options?: { readonly policy?: SyncUpsertPolicy }
|
|
159
|
+
options?: { readonly policy?: SyncUpsertPolicy; readonly limit?: number }
|
|
158
160
|
) => Effect.Effect<SyncResult, SyncError>;
|
|
159
161
|
readonly watch: (config: WatchConfig) => Stream.Stream<SyncEvent, SyncError>;
|
|
160
162
|
}
|
|
@@ -171,7 +173,12 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
171
173
|
const settings = yield* SyncSettings;
|
|
172
174
|
|
|
173
175
|
const sync = Effect.fn("SyncEngine.sync")(
|
|
174
|
-
(
|
|
176
|
+
(
|
|
177
|
+
source: DataSource,
|
|
178
|
+
target: StoreRef,
|
|
179
|
+
filter: FilterExpr,
|
|
180
|
+
options?: { readonly policy?: SyncUpsertPolicy; readonly limit?: number }
|
|
181
|
+
) =>
|
|
175
182
|
Effect.gen(function* () {
|
|
176
183
|
const predicate = yield* runtime
|
|
177
184
|
.evaluateWithMetadata(filter)
|
|
@@ -200,29 +207,10 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
200
207
|
)
|
|
201
208
|
);
|
|
202
209
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (policy === "refresh") {
|
|
208
|
-
const record = yield* committer
|
|
209
|
-
.appendUpsert(target, event)
|
|
210
|
-
.pipe(
|
|
211
|
-
Effect.mapError(
|
|
212
|
-
toSyncError("store", "Failed to append event")
|
|
213
|
-
)
|
|
214
|
-
);
|
|
215
|
-
return Option.some(record.seq);
|
|
216
|
-
}
|
|
217
|
-
const stored = yield* committer
|
|
218
|
-
.appendUpsertIfMissing(target, event)
|
|
219
|
-
.pipe(
|
|
220
|
-
Effect.mapError(
|
|
221
|
-
toSyncError("store", "Failed to append event")
|
|
222
|
-
)
|
|
223
|
-
);
|
|
224
|
-
return Option.map(stored, (entry) => entry.seq);
|
|
225
|
-
});
|
|
210
|
+
const buildUpsert = (post: Post) =>
|
|
211
|
+
makeMeta().pipe(
|
|
212
|
+
Effect.map((meta) => PostUpsert.make({ post, meta }))
|
|
213
|
+
);
|
|
226
214
|
|
|
227
215
|
const prepareRaw = (raw: RawPost): Effect.Effect<PreparedOutcome, SyncError> =>
|
|
228
216
|
parser.parsePost(raw).pipe(
|
|
@@ -244,23 +232,53 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
244
232
|
)
|
|
245
233
|
);
|
|
246
234
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
235
|
+
const commitStoreEvents = (events: ReadonlyArray<PostUpsert>) => {
|
|
236
|
+
if (events.length === 0) {
|
|
237
|
+
return Effect.succeed(
|
|
238
|
+
[] as ReadonlyArray<Option.Option<EventLogEntry>>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const commit = policy === "refresh"
|
|
242
|
+
? committer
|
|
243
|
+
.appendUpserts(target, events)
|
|
244
|
+
.pipe(Effect.map((entries) => entries.map(Option.some)))
|
|
245
|
+
: committer.appendUpsertsIfMissing(target, events);
|
|
246
|
+
return commit.pipe(
|
|
247
|
+
Effect.mapError(
|
|
248
|
+
toSyncError("store", "Failed to append events")
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const applyPreparedBatch = (
|
|
254
|
+
preparedBatch: ReadonlyArray<PreparedOutcome>
|
|
255
|
+
): Effect.Effect<ReadonlyArray<SyncOutcome>, SyncError> =>
|
|
250
256
|
Effect.gen(function* () {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
const storeItems = preparedBatch.filter(
|
|
258
|
+
(item): item is Extract<PreparedOutcome, { _tag: "Store" }> =>
|
|
259
|
+
item._tag === "Store"
|
|
260
|
+
);
|
|
261
|
+
const events = yield* Effect.forEach(storeItems, (item) =>
|
|
262
|
+
buildUpsert(item.post)
|
|
263
|
+
);
|
|
264
|
+
const storedEntries = yield* commitStoreEvents(events);
|
|
265
|
+
let storeIndex = 0;
|
|
266
|
+
return preparedBatch.map((prepared) => {
|
|
267
|
+
switch (prepared._tag) {
|
|
268
|
+
case "Skip":
|
|
269
|
+
return skippedOutcome;
|
|
270
|
+
case "Error":
|
|
271
|
+
return { _tag: "Error", error: prepared.error } as const;
|
|
272
|
+
case "Store": {
|
|
273
|
+
const entry = storedEntries[storeIndex++] ?? Option.none();
|
|
274
|
+
return Option.match(entry, {
|
|
275
|
+
onNone: () => skippedOutcome,
|
|
276
|
+
onSome: (record) =>
|
|
277
|
+
({ _tag: "Stored", eventSeq: record.seq } as const)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
262
280
|
}
|
|
263
|
-
}
|
|
281
|
+
});
|
|
264
282
|
});
|
|
265
283
|
|
|
266
284
|
const initial = SyncResultMonoid.empty;
|
|
@@ -273,41 +291,43 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
273
291
|
const cursorOption = Option.flatMap(activeCheckpoint, (value) =>
|
|
274
292
|
Option.fromNullable(value.cursor)
|
|
275
293
|
);
|
|
294
|
+
const pageLimit = settings.pageLimit;
|
|
276
295
|
|
|
277
|
-
|
|
296
|
+
let stream = (() => {
|
|
278
297
|
switch (source._tag) {
|
|
279
298
|
case "Timeline":
|
|
280
299
|
return client.getTimeline(
|
|
281
300
|
Option.match(cursorOption, {
|
|
282
|
-
onNone: () =>
|
|
283
|
-
onSome: (value) => ({ cursor: value })
|
|
301
|
+
onNone: () => ({ limit: pageLimit }),
|
|
302
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
284
303
|
})
|
|
285
304
|
);
|
|
286
305
|
case "Feed":
|
|
287
306
|
return client.getFeed(
|
|
288
307
|
source.uri,
|
|
289
308
|
Option.match(cursorOption, {
|
|
290
|
-
onNone: () =>
|
|
291
|
-
onSome: (value) => ({ cursor: value })
|
|
309
|
+
onNone: () => ({ limit: pageLimit }),
|
|
310
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
292
311
|
})
|
|
293
312
|
);
|
|
294
313
|
case "List":
|
|
295
314
|
return client.getListFeed(
|
|
296
315
|
source.uri,
|
|
297
316
|
Option.match(cursorOption, {
|
|
298
|
-
onNone: () =>
|
|
299
|
-
onSome: (value) => ({ cursor: value })
|
|
317
|
+
onNone: () => ({ limit: pageLimit }),
|
|
318
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
300
319
|
})
|
|
301
320
|
);
|
|
302
321
|
case "Notifications":
|
|
303
322
|
return client.getNotifications(
|
|
304
323
|
Option.match(cursorOption, {
|
|
305
|
-
onNone: () =>
|
|
306
|
-
onSome: (value) => ({ cursor: value })
|
|
324
|
+
onNone: () => ({ limit: pageLimit }),
|
|
325
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
307
326
|
})
|
|
308
327
|
);
|
|
309
328
|
case "Author":
|
|
310
329
|
const authorOptions = {
|
|
330
|
+
limit: pageLimit,
|
|
311
331
|
...(source.filter !== undefined
|
|
312
332
|
? { filter: source.filter }
|
|
313
333
|
: {}),
|
|
@@ -346,7 +366,10 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
346
366
|
})
|
|
347
367
|
);
|
|
348
368
|
}
|
|
349
|
-
})()
|
|
369
|
+
})() as Stream.Stream<RawPost, BskyError | SyncError>;
|
|
370
|
+
if (options?.limit !== undefined) {
|
|
371
|
+
stream = stream.pipe(Stream.take(options.limit));
|
|
372
|
+
}
|
|
350
373
|
|
|
351
374
|
type SyncState = {
|
|
352
375
|
readonly result: SyncResult;
|
|
@@ -403,6 +426,7 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
403
426
|
};
|
|
404
427
|
|
|
405
428
|
const startTime = yield* Clock.currentTimeMillis;
|
|
429
|
+
const progressIntervalMs = 5000;
|
|
406
430
|
const initialState: SyncState = {
|
|
407
431
|
result: initial,
|
|
408
432
|
lastEventSeq: Option.none<EventSeq>(),
|
|
@@ -415,6 +439,33 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
415
439
|
lastCheckpointAt: startTime
|
|
416
440
|
};
|
|
417
441
|
const stateRef = yield* Ref.make(initialState);
|
|
442
|
+
const heartbeat = Effect.gen(function* () {
|
|
443
|
+
while (true) {
|
|
444
|
+
yield* Effect.sleep(Duration.millis(progressIntervalMs));
|
|
445
|
+
const now = yield* Clock.currentTimeMillis;
|
|
446
|
+
const state = yield* Ref.get(stateRef);
|
|
447
|
+
if (now - state.lastReportAt < progressIntervalMs) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const elapsedMs = now - startTime;
|
|
451
|
+
const rate = elapsedMs > 0 ? state.processed / (elapsedMs / 1000) : 0;
|
|
452
|
+
yield* reporter.report(
|
|
453
|
+
SyncProgress.make({
|
|
454
|
+
processed: state.processed,
|
|
455
|
+
stored: state.stored,
|
|
456
|
+
skipped: state.skipped,
|
|
457
|
+
errors: state.errors,
|
|
458
|
+
elapsedMs,
|
|
459
|
+
rate
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
yield* Ref.update(stateRef, (current) => ({
|
|
463
|
+
...current,
|
|
464
|
+
lastReportAt: now
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
const heartbeatFiber = yield* Effect.fork(heartbeat);
|
|
418
469
|
|
|
419
470
|
const state = yield* stream.pipe(
|
|
420
471
|
Stream.mapError(toSyncError("source", "Source stream failed")),
|
|
@@ -422,47 +473,48 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
422
473
|
concurrency: settings.concurrency,
|
|
423
474
|
unordered: false
|
|
424
475
|
}),
|
|
476
|
+
Stream.grouped(settings.batchSize),
|
|
425
477
|
Stream.runFoldEffect(
|
|
426
478
|
initialState,
|
|
427
|
-
(state,
|
|
479
|
+
(state, preparedChunk) =>
|
|
428
480
|
Effect.gen(function* () {
|
|
429
|
-
const
|
|
430
|
-
const
|
|
481
|
+
const preparedBatch = Chunk.toReadonlyArray(preparedChunk);
|
|
482
|
+
const outcomes = yield* applyPreparedBatch(preparedBatch);
|
|
483
|
+
let storedDelta = 0;
|
|
484
|
+
let skippedDelta = 0;
|
|
485
|
+
let errorDelta = 0;
|
|
486
|
+
let lastStoredSeq = Option.none<EventSeq>();
|
|
487
|
+
const errorList: Array<SyncError> = [];
|
|
488
|
+
for (const outcome of outcomes) {
|
|
431
489
|
switch (outcome._tag) {
|
|
432
490
|
case "Stored":
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
postsSkipped: 0,
|
|
437
|
-
errors: []
|
|
438
|
-
});
|
|
491
|
+
storedDelta += 1;
|
|
492
|
+
lastStoredSeq = Option.some(outcome.eventSeq);
|
|
493
|
+
break;
|
|
439
494
|
case "Skipped":
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
postsDeleted: 0,
|
|
443
|
-
postsSkipped: 1,
|
|
444
|
-
errors: []
|
|
445
|
-
});
|
|
495
|
+
skippedDelta += 1;
|
|
496
|
+
break;
|
|
446
497
|
case "Error":
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
errors: [outcome.error]
|
|
452
|
-
});
|
|
498
|
+
skippedDelta += 1;
|
|
499
|
+
errorDelta += 1;
|
|
500
|
+
errorList.push(outcome.error);
|
|
501
|
+
break;
|
|
453
502
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
503
|
+
}
|
|
504
|
+
const delta = SyncResult.make({
|
|
505
|
+
postsAdded: storedDelta,
|
|
506
|
+
postsDeleted: 0,
|
|
507
|
+
postsSkipped: skippedDelta,
|
|
508
|
+
errors: errorList
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const processed = state.processed + preparedBatch.length;
|
|
512
|
+
const stored = state.stored + storedDelta;
|
|
513
|
+
const skipped = state.skipped + skippedDelta;
|
|
514
|
+
const errors = state.errors + errorDelta;
|
|
463
515
|
const now = yield* Clock.currentTimeMillis;
|
|
464
516
|
const shouldReport =
|
|
465
|
-
processed % 100 === 0 || now - state.lastReportAt >=
|
|
517
|
+
processed % 100 === 0 || now - state.lastReportAt >= progressIntervalMs;
|
|
466
518
|
if (shouldReport) {
|
|
467
519
|
const elapsedMs = now - startTime;
|
|
468
520
|
const rate =
|
|
@@ -479,16 +531,21 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
479
531
|
);
|
|
480
532
|
}
|
|
481
533
|
|
|
482
|
-
const nextCursor =
|
|
483
|
-
|
|
484
|
-
|
|
534
|
+
const nextCursor = preparedBatch.reduce(
|
|
535
|
+
(cursor, prepared) =>
|
|
536
|
+
prepared.pageCursor
|
|
537
|
+
? Option.some(prepared.pageCursor)
|
|
538
|
+
: cursor,
|
|
539
|
+
state.latestCursor
|
|
540
|
+
);
|
|
541
|
+
const nextLastEventSeq =
|
|
542
|
+
Option.isSome(lastStoredSeq)
|
|
543
|
+
? lastStoredSeq
|
|
544
|
+
: state.lastEventSeq;
|
|
485
545
|
|
|
486
546
|
const nextState: SyncState = {
|
|
487
547
|
result: SyncResultMonoid.combine(state.result, delta),
|
|
488
|
-
lastEventSeq:
|
|
489
|
-
outcome._tag === "Stored"
|
|
490
|
-
? Option.some(outcome.eventSeq)
|
|
491
|
-
: state.lastEventSeq,
|
|
548
|
+
lastEventSeq: nextLastEventSeq,
|
|
492
549
|
latestCursor: nextCursor,
|
|
493
550
|
processed,
|
|
494
551
|
stored,
|
|
@@ -516,6 +573,9 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
516
573
|
})
|
|
517
574
|
),
|
|
518
575
|
Effect.withRequestBatching(true),
|
|
576
|
+
Effect.ensuring(
|
|
577
|
+
Fiber.interrupt(heartbeatFiber)
|
|
578
|
+
),
|
|
519
579
|
Effect.ensuring(
|
|
520
580
|
Ref.get(stateRef).pipe(
|
|
521
581
|
Effect.flatMap((state) =>
|
|
@@ -5,12 +5,14 @@ export class SyncReporter extends Context.Tag("@skygent/SyncReporter")<
|
|
|
5
5
|
SyncReporter,
|
|
6
6
|
{
|
|
7
7
|
readonly report: (progress: SyncProgress) => Effect.Effect<void>;
|
|
8
|
+
readonly warn: (message: string, data?: Record<string, unknown>) => Effect.Effect<void>;
|
|
8
9
|
}
|
|
9
10
|
>() {
|
|
10
11
|
static readonly layer = Layer.succeed(
|
|
11
12
|
SyncReporter,
|
|
12
13
|
SyncReporter.of({
|
|
13
|
-
report: () => Effect.void
|
|
14
|
+
report: () => Effect.void,
|
|
15
|
+
warn: () => Effect.void
|
|
14
16
|
})
|
|
15
17
|
);
|
|
16
18
|
}
|
|
@@ -5,6 +5,8 @@ export type SyncSettingsValue = {
|
|
|
5
5
|
readonly checkpointEvery: number;
|
|
6
6
|
readonly checkpointIntervalMs: number;
|
|
7
7
|
readonly concurrency: number;
|
|
8
|
+
readonly batchSize: number;
|
|
9
|
+
readonly pageLimit: number;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
type SyncSettingsOverridesValue = Partial<SyncSettingsValue>;
|
|
@@ -36,11 +38,19 @@ export class SyncSettings extends Context.Tag("@skygent/SyncSettings")<
|
|
|
36
38
|
const concurrency = yield* Config.integer("SKYGENT_SYNC_CONCURRENCY").pipe(
|
|
37
39
|
Config.withDefault(5)
|
|
38
40
|
);
|
|
41
|
+
const batchSize = yield* Config.integer("SKYGENT_SYNC_BATCH_SIZE").pipe(
|
|
42
|
+
Config.withDefault(100)
|
|
43
|
+
);
|
|
44
|
+
const pageLimit = yield* Config.integer("SKYGENT_SYNC_PAGE_LIMIT").pipe(
|
|
45
|
+
Config.withDefault(100)
|
|
46
|
+
);
|
|
39
47
|
|
|
40
48
|
const merged = {
|
|
41
49
|
checkpointEvery,
|
|
42
50
|
checkpointIntervalMs,
|
|
43
51
|
concurrency,
|
|
52
|
+
batchSize,
|
|
53
|
+
pageLimit,
|
|
44
54
|
...pickDefined(overrides as Record<string, unknown>)
|
|
45
55
|
} as SyncSettingsValue;
|
|
46
56
|
|
|
@@ -65,6 +75,20 @@ export class SyncSettings extends Context.Tag("@skygent/SyncSettings")<
|
|
|
65
75
|
if (concurrencyError) {
|
|
66
76
|
return yield* concurrencyError;
|
|
67
77
|
}
|
|
78
|
+
const batchSizeError = validatePositive(
|
|
79
|
+
"SKYGENT_SYNC_BATCH_SIZE",
|
|
80
|
+
merged.batchSize
|
|
81
|
+
);
|
|
82
|
+
if (batchSizeError) {
|
|
83
|
+
return yield* batchSizeError;
|
|
84
|
+
}
|
|
85
|
+
const pageLimitError = validatePositive(
|
|
86
|
+
"SKYGENT_SYNC_PAGE_LIMIT",
|
|
87
|
+
merged.pageLimit
|
|
88
|
+
);
|
|
89
|
+
if (pageLimitError) {
|
|
90
|
+
return yield* pageLimitError;
|
|
91
|
+
}
|
|
68
92
|
|
|
69
93
|
return SyncSettings.of(merged);
|
|
70
94
|
})
|