@mepuka/skygent 0.2.0 → 0.3.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.
- 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/config.ts +20 -3
- 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 +35 -55
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +9 -3
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +84 -4
- package/src/cli/graph.ts +193 -156
- package/src/cli/input.ts +45 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +18 -0
- package/src/cli/parse-errors.ts +18 -0
- package/src/cli/pipe.ts +157 -0
- package/src/cli/post.ts +43 -66
- package/src/cli/query.ts +349 -74
- package/src/cli/search.ts +92 -118
- package/src/cli/shared.ts +0 -19
- package/src/cli/store-errors.ts +24 -13
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +35 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +28 -3
- package/src/cli/sync.ts +16 -18
- package/src/cli/thread-options.ts +33 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +12 -18
- package/src/cli/watch.ts +61 -19
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/graph/relationships.ts +129 -0
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +286 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +136 -85
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { FileSystem, Path } from "@effect/platform";
|
|
40
40
|
import { directorySize } from "./shared.js";
|
|
41
|
-
import { Context, Effect, Layer, Option } from "effect";
|
|
41
|
+
import { Context, Effect, Layer, Option, Order } from "effect";
|
|
42
42
|
import { AppConfigService } from "./app-config.js";
|
|
43
43
|
import { StoreManager } from "./store-manager.js";
|
|
44
44
|
import { StoreIndex } from "./store-index.js";
|
|
@@ -47,11 +47,12 @@ import { LineageStore } from "./lineage-store.js";
|
|
|
47
47
|
import { DerivationValidator } from "./derivation-validator.js";
|
|
48
48
|
import { StoreEventLog } from "./store-event-log.js";
|
|
49
49
|
import { SyncCheckpointStore } from "./sync-checkpoint-store.js";
|
|
50
|
-
import { DataSource } from "../domain/sync.js";
|
|
50
|
+
import { DataSource, type SyncCheckpoint } from "../domain/sync.js";
|
|
51
51
|
import { StoreName, type StorePath } from "../domain/primitives.js";
|
|
52
52
|
import { StoreRef } from "../domain/store.js";
|
|
53
53
|
import type { StoreLineage } from "../domain/derivation.js";
|
|
54
54
|
import { StoreIoError, type StoreIndexError } from "../domain/errors.js";
|
|
55
|
+
import { updatedAtOrder } from "../domain/order.js";
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Detailed statistics for a single store.
|
|
@@ -257,9 +258,10 @@ const resolveSyncStatus = (
|
|
|
257
258
|
if (candidates.length === 0) {
|
|
258
259
|
return "unknown" as const;
|
|
259
260
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
const checkpointOrder = updatedAtOrder<SyncCheckpoint>();
|
|
262
|
+
const latest = candidates.reduce((acc, candidate) =>
|
|
263
|
+
Order.max(checkpointOrder)(acc, candidate)
|
|
264
|
+
);
|
|
263
265
|
if (!latest || !latest.lastEventSeq) {
|
|
264
266
|
return "stale" as const;
|
|
265
267
|
}
|
|
@@ -59,7 +59,7 @@
|
|
|
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
64
|
import { FilterRuntime } from "./filter-runtime.js";
|
|
65
65
|
import { PostParser } from "./post-parser.js";
|
|
@@ -68,6 +68,7 @@ import { BskyClient } from "./bsky-client.js";
|
|
|
68
68
|
import type { FilterExpr } from "../domain/filter.js";
|
|
69
69
|
import { filterExprSignature } from "../domain/filter.js";
|
|
70
70
|
import { EventMeta, PostUpsert } from "../domain/events.js";
|
|
71
|
+
import type { EventLogEntry } from "../domain/events.js";
|
|
71
72
|
import type { Post } from "../domain/post.js";
|
|
72
73
|
import { EventSeq, Timestamp } from "../domain/primitives.js";
|
|
73
74
|
import type { RawPost } from "../domain/raw.js";
|
|
@@ -200,29 +201,10 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
200
201
|
)
|
|
201
202
|
);
|
|
202
203
|
|
|
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
|
-
});
|
|
204
|
+
const buildUpsert = (post: Post) =>
|
|
205
|
+
makeMeta().pipe(
|
|
206
|
+
Effect.map((meta) => PostUpsert.make({ post, meta }))
|
|
207
|
+
);
|
|
226
208
|
|
|
227
209
|
const prepareRaw = (raw: RawPost): Effect.Effect<PreparedOutcome, SyncError> =>
|
|
228
210
|
parser.parsePost(raw).pipe(
|
|
@@ -244,23 +226,53 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
244
226
|
)
|
|
245
227
|
);
|
|
246
228
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
229
|
+
const commitStoreEvents = (events: ReadonlyArray<PostUpsert>) => {
|
|
230
|
+
if (events.length === 0) {
|
|
231
|
+
return Effect.succeed(
|
|
232
|
+
[] as ReadonlyArray<Option.Option<EventLogEntry>>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const commit = policy === "refresh"
|
|
236
|
+
? committer
|
|
237
|
+
.appendUpserts(target, events)
|
|
238
|
+
.pipe(Effect.map((entries) => entries.map(Option.some)))
|
|
239
|
+
: committer.appendUpsertsIfMissing(target, events);
|
|
240
|
+
return commit.pipe(
|
|
241
|
+
Effect.mapError(
|
|
242
|
+
toSyncError("store", "Failed to append events")
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const applyPreparedBatch = (
|
|
248
|
+
preparedBatch: ReadonlyArray<PreparedOutcome>
|
|
249
|
+
): Effect.Effect<ReadonlyArray<SyncOutcome>, SyncError> =>
|
|
250
250
|
Effect.gen(function* () {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
251
|
+
const storeItems = preparedBatch.filter(
|
|
252
|
+
(item): item is Extract<PreparedOutcome, { _tag: "Store" }> =>
|
|
253
|
+
item._tag === "Store"
|
|
254
|
+
);
|
|
255
|
+
const events = yield* Effect.forEach(storeItems, (item) =>
|
|
256
|
+
buildUpsert(item.post)
|
|
257
|
+
);
|
|
258
|
+
const storedEntries = yield* commitStoreEvents(events);
|
|
259
|
+
let storeIndex = 0;
|
|
260
|
+
return preparedBatch.map((prepared) => {
|
|
261
|
+
switch (prepared._tag) {
|
|
262
|
+
case "Skip":
|
|
263
|
+
return skippedOutcome;
|
|
264
|
+
case "Error":
|
|
265
|
+
return { _tag: "Error", error: prepared.error } as const;
|
|
266
|
+
case "Store": {
|
|
267
|
+
const entry = storedEntries[storeIndex++] ?? Option.none();
|
|
268
|
+
return Option.match(entry, {
|
|
269
|
+
onNone: () => skippedOutcome,
|
|
270
|
+
onSome: (record) =>
|
|
271
|
+
({ _tag: "Stored", eventSeq: record.seq } as const)
|
|
272
|
+
});
|
|
273
|
+
}
|
|
262
274
|
}
|
|
263
|
-
}
|
|
275
|
+
});
|
|
264
276
|
});
|
|
265
277
|
|
|
266
278
|
const initial = SyncResultMonoid.empty;
|
|
@@ -273,41 +285,43 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
273
285
|
const cursorOption = Option.flatMap(activeCheckpoint, (value) =>
|
|
274
286
|
Option.fromNullable(value.cursor)
|
|
275
287
|
);
|
|
288
|
+
const pageLimit = settings.pageLimit;
|
|
276
289
|
|
|
277
290
|
const stream = (() => {
|
|
278
291
|
switch (source._tag) {
|
|
279
292
|
case "Timeline":
|
|
280
293
|
return client.getTimeline(
|
|
281
294
|
Option.match(cursorOption, {
|
|
282
|
-
onNone: () =>
|
|
283
|
-
onSome: (value) => ({ cursor: value })
|
|
295
|
+
onNone: () => ({ limit: pageLimit }),
|
|
296
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
284
297
|
})
|
|
285
298
|
);
|
|
286
299
|
case "Feed":
|
|
287
300
|
return client.getFeed(
|
|
288
301
|
source.uri,
|
|
289
302
|
Option.match(cursorOption, {
|
|
290
|
-
onNone: () =>
|
|
291
|
-
onSome: (value) => ({ cursor: value })
|
|
303
|
+
onNone: () => ({ limit: pageLimit }),
|
|
304
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
292
305
|
})
|
|
293
306
|
);
|
|
294
307
|
case "List":
|
|
295
308
|
return client.getListFeed(
|
|
296
309
|
source.uri,
|
|
297
310
|
Option.match(cursorOption, {
|
|
298
|
-
onNone: () =>
|
|
299
|
-
onSome: (value) => ({ cursor: value })
|
|
311
|
+
onNone: () => ({ limit: pageLimit }),
|
|
312
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
300
313
|
})
|
|
301
314
|
);
|
|
302
315
|
case "Notifications":
|
|
303
316
|
return client.getNotifications(
|
|
304
317
|
Option.match(cursorOption, {
|
|
305
|
-
onNone: () =>
|
|
306
|
-
onSome: (value) => ({ cursor: value })
|
|
318
|
+
onNone: () => ({ limit: pageLimit }),
|
|
319
|
+
onSome: (value) => ({ cursor: value, limit: pageLimit })
|
|
307
320
|
})
|
|
308
321
|
);
|
|
309
322
|
case "Author":
|
|
310
323
|
const authorOptions = {
|
|
324
|
+
limit: pageLimit,
|
|
311
325
|
...(source.filter !== undefined
|
|
312
326
|
? { filter: source.filter }
|
|
313
327
|
: {}),
|
|
@@ -403,6 +417,7 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
403
417
|
};
|
|
404
418
|
|
|
405
419
|
const startTime = yield* Clock.currentTimeMillis;
|
|
420
|
+
const progressIntervalMs = 5000;
|
|
406
421
|
const initialState: SyncState = {
|
|
407
422
|
result: initial,
|
|
408
423
|
lastEventSeq: Option.none<EventSeq>(),
|
|
@@ -415,6 +430,33 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
415
430
|
lastCheckpointAt: startTime
|
|
416
431
|
};
|
|
417
432
|
const stateRef = yield* Ref.make(initialState);
|
|
433
|
+
const heartbeat = Effect.gen(function* () {
|
|
434
|
+
while (true) {
|
|
435
|
+
yield* Effect.sleep(Duration.millis(progressIntervalMs));
|
|
436
|
+
const now = yield* Clock.currentTimeMillis;
|
|
437
|
+
const state = yield* Ref.get(stateRef);
|
|
438
|
+
if (now - state.lastReportAt < progressIntervalMs) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const elapsedMs = now - startTime;
|
|
442
|
+
const rate = elapsedMs > 0 ? state.processed / (elapsedMs / 1000) : 0;
|
|
443
|
+
yield* reporter.report(
|
|
444
|
+
SyncProgress.make({
|
|
445
|
+
processed: state.processed,
|
|
446
|
+
stored: state.stored,
|
|
447
|
+
skipped: state.skipped,
|
|
448
|
+
errors: state.errors,
|
|
449
|
+
elapsedMs,
|
|
450
|
+
rate
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
yield* Ref.update(stateRef, (current) => ({
|
|
454
|
+
...current,
|
|
455
|
+
lastReportAt: now
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const heartbeatFiber = yield* Effect.fork(heartbeat);
|
|
418
460
|
|
|
419
461
|
const state = yield* stream.pipe(
|
|
420
462
|
Stream.mapError(toSyncError("source", "Source stream failed")),
|
|
@@ -422,47 +464,48 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
422
464
|
concurrency: settings.concurrency,
|
|
423
465
|
unordered: false
|
|
424
466
|
}),
|
|
467
|
+
Stream.grouped(settings.batchSize),
|
|
425
468
|
Stream.runFoldEffect(
|
|
426
469
|
initialState,
|
|
427
|
-
(state,
|
|
470
|
+
(state, preparedChunk) =>
|
|
428
471
|
Effect.gen(function* () {
|
|
429
|
-
const
|
|
430
|
-
const
|
|
472
|
+
const preparedBatch = Chunk.toReadonlyArray(preparedChunk);
|
|
473
|
+
const outcomes = yield* applyPreparedBatch(preparedBatch);
|
|
474
|
+
let storedDelta = 0;
|
|
475
|
+
let skippedDelta = 0;
|
|
476
|
+
let errorDelta = 0;
|
|
477
|
+
let lastStoredSeq = Option.none<EventSeq>();
|
|
478
|
+
const errorList: Array<SyncError> = [];
|
|
479
|
+
for (const outcome of outcomes) {
|
|
431
480
|
switch (outcome._tag) {
|
|
432
481
|
case "Stored":
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
postsSkipped: 0,
|
|
437
|
-
errors: []
|
|
438
|
-
});
|
|
482
|
+
storedDelta += 1;
|
|
483
|
+
lastStoredSeq = Option.some(outcome.eventSeq);
|
|
484
|
+
break;
|
|
439
485
|
case "Skipped":
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
postsDeleted: 0,
|
|
443
|
-
postsSkipped: 1,
|
|
444
|
-
errors: []
|
|
445
|
-
});
|
|
486
|
+
skippedDelta += 1;
|
|
487
|
+
break;
|
|
446
488
|
case "Error":
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
errors: [outcome.error]
|
|
452
|
-
});
|
|
489
|
+
skippedDelta += 1;
|
|
490
|
+
errorDelta += 1;
|
|
491
|
+
errorList.push(outcome.error);
|
|
492
|
+
break;
|
|
453
493
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
494
|
+
}
|
|
495
|
+
const delta = SyncResult.make({
|
|
496
|
+
postsAdded: storedDelta,
|
|
497
|
+
postsDeleted: 0,
|
|
498
|
+
postsSkipped: skippedDelta,
|
|
499
|
+
errors: errorList
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const processed = state.processed + preparedBatch.length;
|
|
503
|
+
const stored = state.stored + storedDelta;
|
|
504
|
+
const skipped = state.skipped + skippedDelta;
|
|
505
|
+
const errors = state.errors + errorDelta;
|
|
463
506
|
const now = yield* Clock.currentTimeMillis;
|
|
464
507
|
const shouldReport =
|
|
465
|
-
processed % 100 === 0 || now - state.lastReportAt >=
|
|
508
|
+
processed % 100 === 0 || now - state.lastReportAt >= progressIntervalMs;
|
|
466
509
|
if (shouldReport) {
|
|
467
510
|
const elapsedMs = now - startTime;
|
|
468
511
|
const rate =
|
|
@@ -479,16 +522,21 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
479
522
|
);
|
|
480
523
|
}
|
|
481
524
|
|
|
482
|
-
const nextCursor =
|
|
483
|
-
|
|
484
|
-
|
|
525
|
+
const nextCursor = preparedBatch.reduce(
|
|
526
|
+
(cursor, prepared) =>
|
|
527
|
+
prepared.pageCursor
|
|
528
|
+
? Option.some(prepared.pageCursor)
|
|
529
|
+
: cursor,
|
|
530
|
+
state.latestCursor
|
|
531
|
+
);
|
|
532
|
+
const nextLastEventSeq =
|
|
533
|
+
Option.isSome(lastStoredSeq)
|
|
534
|
+
? lastStoredSeq
|
|
535
|
+
: state.lastEventSeq;
|
|
485
536
|
|
|
486
537
|
const nextState: SyncState = {
|
|
487
538
|
result: SyncResultMonoid.combine(state.result, delta),
|
|
488
|
-
lastEventSeq:
|
|
489
|
-
outcome._tag === "Stored"
|
|
490
|
-
? Option.some(outcome.eventSeq)
|
|
491
|
-
: state.lastEventSeq,
|
|
539
|
+
lastEventSeq: nextLastEventSeq,
|
|
492
540
|
latestCursor: nextCursor,
|
|
493
541
|
processed,
|
|
494
542
|
stored,
|
|
@@ -516,6 +564,9 @@ export class SyncEngine extends Context.Tag("@skygent/SyncEngine")<
|
|
|
516
564
|
})
|
|
517
565
|
),
|
|
518
566
|
Effect.withRequestBatching(true),
|
|
567
|
+
Effect.ensuring(
|
|
568
|
+
Fiber.interrupt(heartbeatFiber)
|
|
569
|
+
),
|
|
519
570
|
Effect.ensuring(
|
|
520
571
|
Ref.get(stateRef).pipe(
|
|
521
572
|
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
|
})
|