@gmickel/gno 1.34.4 → 1.34.6

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 (37) hide show
  1. package/README.md +11 -1
  2. package/THIRD_PARTY_NOTICES.md +16 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.34.4.zip → gno-browser-clipper-v1.34.6.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +5 -1
  7. package/spec/cli.md +16 -10
  8. package/src/cli/pager.ts +29 -14
  9. package/src/ingestion/sync.ts +368 -84
  10. package/src/serve/public/globals.built.css +1 -1
  11. package/src/serve/watch-reconciliation-fallback-disk.ts +220 -0
  12. package/src/serve/watch-reconciliation-fallback.ts +404 -0
  13. package/src/serve/watch-reconciliation-shared.ts +343 -0
  14. package/src/serve/watch-reconciliation.ts +122 -0
  15. package/src/serve/watch-service-events.ts +261 -0
  16. package/src/serve/watch-service-flush-generation.ts +140 -0
  17. package/src/serve/watch-service-flush-helpers.ts +147 -0
  18. package/src/serve/watch-service-flush.ts +433 -0
  19. package/src/serve/watch-service-hosts.ts +109 -0
  20. package/src/serve/watch-service-lifecycle.ts +219 -0
  21. package/src/serve/watch-service-run-flush.ts +236 -0
  22. package/src/serve/watch-service-snapshot.ts +101 -0
  23. package/src/serve/watch-service-state.ts +146 -0
  24. package/src/serve/watch-service.ts +265 -306
  25. package/src/serve/watch-snapshot-handles.ts +285 -0
  26. package/src/serve/watch-snapshot-libc.ts +391 -0
  27. package/src/serve/watch-snapshot-ops.ts +399 -0
  28. package/src/serve/watch-snapshot-resolve.ts +246 -0
  29. package/src/serve/watch-snapshot-scan.ts +297 -0
  30. package/src/serve/watch-snapshot-types.ts +350 -0
  31. package/src/serve/watch-snapshot.ts +51 -0
  32. package/src/store/index.ts +1 -1
  33. package/src/store/sqlite/adapter.ts +191 -0
  34. package/src/store/types.ts +66 -0
  35. package/vendor/fts5-snowball/README.md +5 -1
  36. package/vendor/fts5-snowball/darwin-x64/fts5stemmer.dylib +0 -0
  37. package/browser-extension/artifacts/gno-browser-clipper-v1.34.4.zip.sha256 +0 -1
@@ -0,0 +1,433 @@
1
+ /**
2
+ * One collection flush: widen vanished exact paths, classify dirty hints,
3
+ * proven-removal inactivation, targeted syncPaths, config-generation reconcile.
4
+ *
5
+ * @module src/serve/watch-service-flush
6
+ */
7
+
8
+ // node:path — Bun has no path utilities
9
+ import { join, normalize } from "node:path";
10
+
11
+ import type { Collection } from "../config/types";
12
+ import type { CollectionSyncResult, SyncOptions } from "../ingestion";
13
+ import type { SqliteAdapter } from "../store/sqlite/adapter";
14
+ import type { PendingForceFlags } from "./watch-service-state";
15
+ import type { WatcherSnapshot, WatcherSnapshotFs } from "./watch-snapshot";
16
+
17
+ import {
18
+ collectionToWalkConfig,
19
+ defaultSyncService,
20
+ matchesWalkPath,
21
+ } from "../ingestion";
22
+ import {
23
+ classifyDirtyHints,
24
+ failedSyncPaths,
25
+ hasFileLevelSyncError,
26
+ mergeSyncPathBatch,
27
+ widenVanishedExactPaths,
28
+ } from "./watch-reconciliation";
29
+ import { runGenerationReconcile } from "./watch-service-flush-generation";
30
+ import {
31
+ computeTargetedRetry,
32
+ contentChangedPaths,
33
+ notifyCompletedSync,
34
+ } from "./watch-service-flush-helpers";
35
+
36
+ /** @deprecated Prefer contentChangedPaths; kept for existing imports. */
37
+ export function changedPaths(
38
+ result: CollectionSyncResult,
39
+ fallbackPaths: string[] = []
40
+ ): string[] {
41
+ return contentChangedPaths(result, fallbackPaths);
42
+ }
43
+
44
+ export interface FlushCollectionInput {
45
+ collection: Collection;
46
+ collectionName: string;
47
+ store: SqliteAdapter;
48
+ syncOptions: SyncOptions;
49
+ exactTaken: string[];
50
+ dirtyTaken: string[];
51
+ forceFallback: boolean;
52
+ overflow: boolean;
53
+ generationReconcile: boolean;
54
+ previousSnapshot: WatcherSnapshot | null;
55
+ /** Ownership token: generation + normalized root at flush start. */
56
+ ownerGeneration: number;
57
+ ownerRoot: string;
58
+ disposed: () => boolean;
59
+ getCurrentCollection: () => Collection | undefined;
60
+ getCurrentGeneration: () => number;
61
+ getCurrentSyncOptions: () => SyncOptions;
62
+ clock: () => number;
63
+ suppressedPaths: Map<string, number>;
64
+ /** Optional injectable FS (tests: unsupported-handle proofs). */
65
+ snapshotFs?: WatcherSnapshotFs;
66
+ /** Test seam: lower snapshot entry ceiling for overflow→full proofs. */
67
+ snapshotEntryCeiling?: number;
68
+ onSyncStart: (relPaths: string[]) => void;
69
+ onSyncComplete: (relPaths: string[], result: CollectionSyncResult) => void;
70
+ onSyncError: (relPaths: string[], error: unknown) => void;
71
+ onAfterSync: (collection: Collection, relPaths: string[]) => void;
72
+ commitSnapshot: (snapshot: WatcherSnapshot) => void;
73
+ invalidateSnapshot: (collection: Collection) => void;
74
+ requeue: (
75
+ exact: string[],
76
+ dirty: string[],
77
+ forceFlags?: PendingForceFlags
78
+ ) => void;
79
+ requeueGeneration: () => void;
80
+ }
81
+
82
+ export type FlushCollectionOutcome =
83
+ | { status: "disposed" }
84
+ | { status: "idle" }
85
+ | { status: "synced" }
86
+ | { status: "failed"; error?: unknown }
87
+ | { status: "stale" };
88
+
89
+ function filterSuppressedPaths(
90
+ rootAbs: string,
91
+ paths: readonly string[],
92
+ suppressed: Map<string, number>,
93
+ nowMs: number
94
+ ): string[] {
95
+ return paths.filter((relPath) => {
96
+ const abs = normalize(join(rootAbs, ...relPath.split("/").filter(Boolean)));
97
+ const until = suppressed.get(abs);
98
+ return !(until && until > nowMs);
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Targeted work ownership: same generation and normalized root as flush start.
104
+ * Generation-reconcile work is separate and may intentionally run after a gen bump.
105
+ */
106
+ function ownsTargeted(input: FlushCollectionInput): boolean {
107
+ if (input.disposed()) {
108
+ return false;
109
+ }
110
+ const current = input.getCurrentCollection();
111
+ if (!current) {
112
+ return false;
113
+ }
114
+ return (
115
+ input.getCurrentGeneration() === input.ownerGeneration &&
116
+ normalize(current.path) === input.ownerRoot
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Run one flush attempt. Caller owns syncing flags and settlement.
122
+ */
123
+ export async function flushCollectionOnce(
124
+ input: FlushCollectionInput
125
+ ): Promise<FlushCollectionOutcome> {
126
+ const walkConfig = collectionToWalkConfig(input.collection, 0);
127
+ const exactPaths = input.exactTaken.filter((relPath) =>
128
+ matchesWalkPath(relPath, walkConfig)
129
+ );
130
+ const rootAbs = normalize(input.collection.path);
131
+ const forceFlags: PendingForceFlags = {
132
+ forceFallback: input.forceFallback || input.overflow,
133
+ overflow: input.overflow,
134
+ };
135
+
136
+ try {
137
+ if (input.disposed()) {
138
+ return { status: "disposed" };
139
+ }
140
+ // Collection gone: drop taken work; do not mutate successor state.
141
+ if (!input.getCurrentCollection()) {
142
+ return { status: "stale" };
143
+ }
144
+
145
+ const widened = await widenVanishedExactPaths(rootAbs, exactPaths);
146
+ if (input.disposed()) {
147
+ return { status: "disposed" };
148
+ }
149
+ if (!ownsTargeted(input)) {
150
+ // Root/config replaced mid-flight: still attempt generation reconcile.
151
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
152
+ }
153
+
154
+ const dirtyHints = [
155
+ ...new Set([...input.dirtyTaken, ...widened.extraDirty]),
156
+ ];
157
+ // Directory exact paths and init-time ambiguous work need store/disk so
158
+ // present eligible children/finals are not absorbed by an unchanged baseline.
159
+ const forceFallback =
160
+ forceFlags.forceFallback || widened.directoryDirty.length > 0;
161
+ if (widened.directoryDirty.length > 0) {
162
+ forceFlags.forceFallback = true;
163
+ }
164
+
165
+ let classifiedCandidates: string[] = [];
166
+ let classifiedRemovals: string[] = [];
167
+ let nextSnapshot: WatcherSnapshot | null = input.previousSnapshot;
168
+ let dirtyFailed = false;
169
+ let classificationOk = dirtyHints.length === 0;
170
+ /** Ambiguous overflow/unsupported → durable full-collection authority. */
171
+ let needsFullFromAmbiguous = false;
172
+
173
+ if (dirtyHints.length > 0) {
174
+ const classified = await classifyDirtyHints({
175
+ collection: input.collection,
176
+ store: input.store,
177
+ rootAbs,
178
+ previous: input.previousSnapshot,
179
+ dirtyHints,
180
+ forceFallback,
181
+ snapshotOptions: {
182
+ ...(input.snapshotFs ? { fs: input.snapshotFs } : {}),
183
+ ...(input.snapshotEntryCeiling !== undefined
184
+ ? { entryCeiling: input.snapshotEntryCeiling }
185
+ : {}),
186
+ },
187
+ });
188
+ if (input.disposed()) {
189
+ return { status: "disposed" };
190
+ }
191
+ if (!ownsTargeted(input)) {
192
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
193
+ }
194
+ if (classified.status === "full_reconcile") {
195
+ // No path-backed scan; convert ambiguous work to durable full sync.
196
+ needsFullFromAmbiguous = true;
197
+ classificationOk = false;
198
+ nextSnapshot = null;
199
+ } else if (classified.status === "error") {
200
+ dirtyFailed = true;
201
+ input.onSyncError(dirtyHints, classified.cause);
202
+ // Classification failure: retain original dirty + force flags.
203
+ input.requeue([], dirtyHints, forceFlags);
204
+ } else {
205
+ classificationOk = true;
206
+ const nowMs = input.clock();
207
+ // After successful dirty classification, drop suppressed abs paths
208
+ // before batching; snapshot generation may still commit.
209
+ classifiedCandidates = filterSuppressedPaths(
210
+ rootAbs,
211
+ classified.candidates,
212
+ input.suppressedPaths,
213
+ nowMs
214
+ );
215
+ classifiedRemovals = filterSuppressedPaths(
216
+ rootAbs,
217
+ classified.removals,
218
+ input.suppressedPaths,
219
+ nowMs
220
+ );
221
+ nextSnapshot = classified.nextSnapshot;
222
+ }
223
+ }
224
+
225
+ const liveExact = widened.keepExact.filter((relPath) =>
226
+ matchesWalkPath(relPath, walkConfig)
227
+ );
228
+ // Present candidates only — proven removals use a distinct inactivation step.
229
+ const presentPaths = mergeSyncPathBatch(
230
+ liveExact,
231
+ classifiedCandidates,
232
+ []
233
+ );
234
+ const generationAuthority =
235
+ needsFullFromAmbiguous || input.generationReconcile;
236
+ const dirtyDerivedRemovals = classifiedRemovals.length > 0;
237
+ const dirtyDerivedCandidates = classifiedCandidates.length > 0;
238
+
239
+ let targetedSynced = false;
240
+ let targetedFailed: { error: unknown } | null = null;
241
+ /** Any dirty-derived candidate/removal op failed — block snapshot commit. */
242
+ let dirtyDerivedFailed = dirtyFailed;
243
+
244
+ // 1) Proven removals: inactivate even when path is now dir/FIFO/device.
245
+ if (classifiedRemovals.length > 0) {
246
+ if (!ownsTargeted(input)) {
247
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
248
+ }
249
+ input.onSyncStart(classifiedRemovals);
250
+ const inactiveResult = await defaultSyncService.inactivateAbsentSources(
251
+ input.collection,
252
+ input.store,
253
+ classifiedRemovals,
254
+ {
255
+ ...input.getCurrentSyncOptions(),
256
+ runUpdateCmd: false,
257
+ }
258
+ );
259
+ if (input.disposed()) {
260
+ return { status: "disposed" };
261
+ }
262
+ if (!ownsTargeted(input)) {
263
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
264
+ }
265
+
266
+ const settled = new Set(
267
+ notifyCompletedSync(input, classifiedRemovals, inactiveResult)
268
+ );
269
+ if (hasFileLevelSyncError(inactiveResult)) {
270
+ dirtyDerivedFailed = true;
271
+ const retry = computeTargetedRetry({
272
+ result: inactiveResult,
273
+ submittedPaths: classifiedRemovals,
274
+ liveExact: [],
275
+ settled,
276
+ dirtyHints,
277
+ dirtyFailed,
278
+ dirtyDerivedSubmission: dirtyDerivedRemovals,
279
+ generationAuthority,
280
+ });
281
+ // Failed proven removals re-enter as dirty so classification can retry.
282
+ // Always retain original dirty hints + force flags on derived failure.
283
+ const failedRemovals = classifiedRemovals.filter(
284
+ (path) => !settled.has(path)
285
+ );
286
+ const error = new Error("One or more paths failed during watcher sync");
287
+ input.onSyncError(
288
+ failedRemovals.length > 0 ? failedRemovals : classifiedRemovals,
289
+ error
290
+ );
291
+ input.requeue(
292
+ retry.retryExact,
293
+ [
294
+ ...new Set([
295
+ ...(retry.retainDirty || dirtyDerivedFailed ? dirtyHints : []),
296
+ ...failedRemovals,
297
+ ]),
298
+ ],
299
+ forceFlags
300
+ );
301
+ if (generationAuthority) {
302
+ input.requeueGeneration();
303
+ }
304
+ // Still attempt present-path sync for eligible children of file→dir.
305
+ targetedFailed = { error };
306
+ } else {
307
+ targetedSynced = true;
308
+ }
309
+ }
310
+
311
+ // 2) Present exact + candidates: content-hash authority via syncPaths.
312
+ if (presentPaths.length > 0) {
313
+ if (!ownsTargeted(input)) {
314
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
315
+ }
316
+ input.onSyncStart(presentPaths);
317
+ const result = await defaultSyncService.syncPaths(
318
+ input.collection,
319
+ input.store,
320
+ presentPaths,
321
+ {
322
+ ...input.getCurrentSyncOptions(),
323
+ runUpdateCmd: false,
324
+ }
325
+ );
326
+ if (input.disposed()) {
327
+ return { status: "disposed" };
328
+ }
329
+
330
+ if (!ownsTargeted(input)) {
331
+ // Do not commit/requeue/callback for the old owner; gen reconcile may run.
332
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
333
+ }
334
+
335
+ if (hasFileLevelSyncError(result)) {
336
+ const settled = new Set(
337
+ notifyCompletedSync(input, presentPaths, result)
338
+ );
339
+ const failed = failedSyncPaths(result, presentPaths).filter(
340
+ (path) => !settled.has(path)
341
+ );
342
+ if (
343
+ dirtyDerivedCandidates &&
344
+ (result.errors.length > 0 ||
345
+ result.filesErrored > 0 ||
346
+ failed.some((path) => classifiedCandidates.includes(path)))
347
+ ) {
348
+ // Candidate batch failed — retain dirty authority, block snapshot.
349
+ dirtyDerivedFailed = true;
350
+ }
351
+ const retry = computeTargetedRetry({
352
+ result,
353
+ submittedPaths: presentPaths,
354
+ liveExact,
355
+ settled,
356
+ dirtyHints,
357
+ dirtyFailed,
358
+ dirtyDerivedSubmission: dirtyDerivedCandidates,
359
+ generationAuthority,
360
+ });
361
+ const error = new Error("One or more paths failed during watcher sync");
362
+ input.onSyncError(failed.length > 0 ? failed : retry.retryExact, error);
363
+ // Retain original dirty hints + force flags — not solely failed exact.
364
+ input.requeue(
365
+ retry.retryExact,
366
+ retry.retainDirty || dirtyDerivedFailed ? dirtyHints : [],
367
+ forceFlags
368
+ );
369
+ if (generationAuthority) {
370
+ input.requeueGeneration();
371
+ }
372
+ return { status: "failed", error };
373
+ }
374
+
375
+ // Commit only after full classified generation (candidates + removals).
376
+ if (
377
+ classificationOk &&
378
+ !dirtyFailed &&
379
+ !dirtyDerivedFailed &&
380
+ !targetedFailed &&
381
+ nextSnapshot
382
+ ) {
383
+ input.commitSnapshot(nextSnapshot);
384
+ }
385
+
386
+ notifyCompletedSync(input, presentPaths, result);
387
+ targetedSynced = true;
388
+ } else if (
389
+ classificationOk &&
390
+ !dirtyFailed &&
391
+ !dirtyDerivedFailed &&
392
+ !targetedFailed &&
393
+ nextSnapshot &&
394
+ ownsTargeted(input)
395
+ ) {
396
+ // Inactive-only / empty candidate classification: still commit snapshot.
397
+ input.commitSnapshot(nextSnapshot);
398
+ if (classifiedRemovals.length === 0 && !targetedSynced) {
399
+ // No path op ran; classification-only settle is idle (no sync result).
400
+ }
401
+ }
402
+
403
+ if (targetedFailed) {
404
+ return { status: "failed", error: targetedFailed.error };
405
+ }
406
+
407
+ // Config-generation / overflow / unsupported-FS full reconciliation.
408
+ const genInput: FlushCollectionInput = needsFullFromAmbiguous
409
+ ? { ...input, generationReconcile: true }
410
+ : input;
411
+ const genOutcome = await runGenerationReconcile(genInput);
412
+ if (genOutcome) {
413
+ return genOutcome;
414
+ }
415
+
416
+ return targetedSynced || input.generationReconcile || needsFullFromAmbiguous
417
+ ? { status: "synced" }
418
+ : { status: "idle" };
419
+ } catch (error) {
420
+ if (input.disposed()) {
421
+ return { status: "disposed" };
422
+ }
423
+ if (!ownsTargeted(input)) {
424
+ return (await runGenerationReconcile(input)) ?? { status: "stale" };
425
+ }
426
+ input.onSyncError(exactPaths, error);
427
+ input.requeue(exactPaths, input.dirtyTaken, forceFlags);
428
+ if (input.generationReconcile) {
429
+ input.requeueGeneration();
430
+ }
431
+ return { status: "failed", error };
432
+ }
433
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Host object builders for CollectionWatchService collaborators.
3
+ *
4
+ * @module src/serve/watch-service-hosts
5
+ */
6
+
7
+ import type { FSWatcher } from "node:fs";
8
+
9
+ import type { Collection } from "../config/types";
10
+ import type { SyncOptions } from "../ingestion";
11
+ import type { WatchEventHost, WatchQueueHost } from "./watch-service-events";
12
+ import type { WatchLifecycleHost } from "./watch-service-lifecycle";
13
+ import type { CollectionPending } from "./watch-service-state";
14
+ import type { WatcherSnapshot } from "./watch-snapshot";
15
+
16
+ export interface WatchServiceHostState {
17
+ disposed: () => boolean;
18
+ getCollections: () => Collection[];
19
+ setCollections: (collections: Collection[]) => void;
20
+ getSyncOptions: () => SyncOptions;
21
+ setSyncOptions: (syncOptions: SyncOptions) => void;
22
+ watchers: Map<string, FSWatcher>;
23
+ watchRoots: Map<string, string>;
24
+ collectionFingerprints: Map<string, string>;
25
+ collectionGenerations: Map<string, number>;
26
+ nextGeneration: () => number;
27
+ failedCollections: Map<string, string>;
28
+ snapshots: Map<string, WatcherSnapshot>;
29
+ snapshotReady: Map<string, boolean>;
30
+ snapshotInit: Map<string, Promise<void>>;
31
+ syncing: Set<string>;
32
+ pendingByCollection: Map<string, CollectionPending>;
33
+ clearCollectionRuntimeState: (collectionName: string) => void;
34
+ beginSnapshotInit: (collection: Collection) => void;
35
+ watchFactory: WatchLifecycleHost["watchFactory"];
36
+ onWatchEvent: WatchLifecycleHost["onWatchEvent"];
37
+ findCollection: (collectionName: string) => Collection | undefined;
38
+ clock: () => number;
39
+ suppressedPaths: Map<string, number>;
40
+ setLastEventAt: (iso: string) => void;
41
+ enqueueExact: (collectionName: string, relPath: string) => void;
42
+ enqueueDirty: (collectionName: string, hint: string) => void;
43
+ flushDebounceMs: number;
44
+ maxFlushDelayMs: number;
45
+ maxExactPaths: number;
46
+ maxDirtyHints: number;
47
+ flushDeadlineAt: Map<string, number>;
48
+ timers: Map<string, ReturnType<typeof setTimeout>>;
49
+ retryScheduled: Set<string>;
50
+ inFlightSyncs: Set<Promise<void>>;
51
+ runFlush: (collectionName: string) => Promise<void>;
52
+ }
53
+
54
+ export function buildLifecycleHost(
55
+ state: WatchServiceHostState
56
+ ): WatchLifecycleHost {
57
+ return {
58
+ disposed: state.disposed,
59
+ getCollections: state.getCollections,
60
+ setCollections: state.setCollections,
61
+ getSyncOptions: state.getSyncOptions,
62
+ setSyncOptions: state.setSyncOptions,
63
+ watchers: state.watchers,
64
+ watchRoots: state.watchRoots,
65
+ collectionFingerprints: state.collectionFingerprints,
66
+ collectionGenerations: state.collectionGenerations,
67
+ nextGeneration: state.nextGeneration,
68
+ failedCollections: state.failedCollections,
69
+ snapshots: state.snapshots,
70
+ snapshotReady: state.snapshotReady,
71
+ snapshotInit: state.snapshotInit,
72
+ syncing: state.syncing,
73
+ pendingByCollection: state.pendingByCollection,
74
+ clearCollectionRuntimeState: state.clearCollectionRuntimeState,
75
+ beginSnapshotInit: state.beginSnapshotInit,
76
+ watchFactory: state.watchFactory,
77
+ onWatchEvent: state.onWatchEvent,
78
+ };
79
+ }
80
+
81
+ export function buildEventHost(state: WatchServiceHostState): WatchEventHost {
82
+ return {
83
+ disposed: state.disposed,
84
+ findCollection: state.findCollection,
85
+ clock: state.clock,
86
+ suppressedPaths: state.suppressedPaths,
87
+ setLastEventAt: state.setLastEventAt,
88
+ enqueueExact: state.enqueueExact,
89
+ enqueueDirty: state.enqueueDirty,
90
+ };
91
+ }
92
+
93
+ export function buildQueueHost(state: WatchServiceHostState): WatchQueueHost {
94
+ return {
95
+ disposed: state.disposed,
96
+ clock: state.clock,
97
+ flushDebounceMs: state.flushDebounceMs,
98
+ maxFlushDelayMs: state.maxFlushDelayMs,
99
+ maxExactPaths: state.maxExactPaths,
100
+ maxDirtyHints: state.maxDirtyHints,
101
+ pendingByCollection: state.pendingByCollection,
102
+ flushDeadlineAt: state.flushDeadlineAt,
103
+ timers: state.timers,
104
+ retryScheduled: state.retryScheduled,
105
+ snapshotReady: state.snapshotReady,
106
+ inFlightSyncs: state.inFlightSyncs,
107
+ runFlush: state.runFlush,
108
+ };
109
+ }