@gmickel/gno 1.34.5 → 1.35.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.
Files changed (49) hide show
  1. package/README.md +22 -1
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +4 -1
  6. package/spec/cli.md +43 -0
  7. package/spec/output-schemas/mcp-job-status.schema.json +6 -2
  8. package/src/config/index.ts +4 -0
  9. package/src/config/types.ts +14 -0
  10. package/src/core/path-rules.ts +34 -0
  11. package/src/ingestion/index.ts +21 -0
  12. package/src/ingestion/record-container.ts +23 -1
  13. package/src/ingestion/source-availability/darwin-io.ts +295 -0
  14. package/src/ingestion/source-availability/darwin-path.ts +58 -0
  15. package/src/ingestion/source-availability/directory.ts +402 -0
  16. package/src/ingestion/source-availability/index.ts +74 -0
  17. package/src/ingestion/source-availability/readers.ts +360 -0
  18. package/src/ingestion/source-availability/resolve.ts +28 -0
  19. package/src/ingestion/source-availability/types.ts +170 -0
  20. package/src/ingestion/sync.ts +565 -108
  21. package/src/ingestion/types.ts +45 -3
  22. package/src/ingestion/walker.ts +263 -5
  23. package/src/serve/public/globals.built.css +1 -1
  24. package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
  25. package/src/serve/watch-reconciliation-fallback.ts +434 -0
  26. package/src/serve/watch-reconciliation-shared.ts +348 -0
  27. package/src/serve/watch-reconciliation.ts +129 -0
  28. package/src/serve/watch-service-events.ts +261 -0
  29. package/src/serve/watch-service-flush-generation.ts +140 -0
  30. package/src/serve/watch-service-flush-helpers.ts +147 -0
  31. package/src/serve/watch-service-flush.ts +443 -0
  32. package/src/serve/watch-service-hosts.ts +109 -0
  33. package/src/serve/watch-service-lifecycle.ts +221 -0
  34. package/src/serve/watch-service-run-flush.ts +236 -0
  35. package/src/serve/watch-service-snapshot.ts +125 -0
  36. package/src/serve/watch-service-state.ts +146 -0
  37. package/src/serve/watch-service.ts +266 -306
  38. package/src/serve/watch-snapshot-availability.ts +51 -0
  39. package/src/serve/watch-snapshot-handles.ts +365 -0
  40. package/src/serve/watch-snapshot-libc.ts +510 -0
  41. package/src/serve/watch-snapshot-ops.ts +541 -0
  42. package/src/serve/watch-snapshot-resolve.ts +246 -0
  43. package/src/serve/watch-snapshot-scan.ts +300 -0
  44. package/src/serve/watch-snapshot-types.ts +392 -0
  45. package/src/serve/watch-snapshot.ts +51 -0
  46. package/src/store/index.ts +1 -1
  47. package/src/store/sqlite/adapter.ts +191 -0
  48. package/src/store/types.ts +66 -0
  49. package/browser-extension/artifacts/gno-browser-clipper-v1.34.5.zip.sha256 +0 -1
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Collection watcher lifecycle: fingerprinting and updateCollections.
3
+ *
4
+ * @module src/serve/watch-service-lifecycle
5
+ */
6
+
7
+ import type { FSWatcher } from "node:fs";
8
+
9
+ // node:path — Bun has no path utilities
10
+ import { normalize } from "node:path";
11
+
12
+ import type { Collection } from "../config/types";
13
+ import type { SyncOptions } from "../ingestion";
14
+ import type { WatcherSnapshot } from "./watch-snapshot";
15
+
16
+ import { resolveSourceAvailability } from "../ingestion/source-availability";
17
+ import { emptyPending, type CollectionPending } from "./watch-service-state";
18
+
19
+ export interface WatchLifecycleHost {
20
+ disposed: () => boolean;
21
+ getCollections: () => Collection[];
22
+ setCollections: (collections: Collection[]) => void;
23
+ getSyncOptions: () => SyncOptions;
24
+ setSyncOptions: (syncOptions: SyncOptions) => void;
25
+ watchers: Map<string, FSWatcher>;
26
+ watchRoots: Map<string, string>;
27
+ collectionFingerprints: Map<string, string>;
28
+ collectionGenerations: Map<string, number>;
29
+ nextGeneration: () => number;
30
+ failedCollections: Map<string, string>;
31
+ snapshots: Map<string, WatcherSnapshot>;
32
+ snapshotReady: Map<string, boolean>;
33
+ snapshotInit: Map<string, Promise<void>>;
34
+ /** Collections currently mid-flush (ABA ownership). */
35
+ syncing: Set<string>;
36
+ /**
37
+ * Pending work by collection. Config/generation invalidation marks dirty work
38
+ * forceFallback + durable generation reconcile so a new baseline cannot absorb it.
39
+ */
40
+ pendingByCollection: Map<string, CollectionPending>;
41
+ clearCollectionRuntimeState: (collectionName: string) => void;
42
+ beginSnapshotInit: (collection: Collection) => void;
43
+ watchFactory: (
44
+ path: string,
45
+ options: { recursive: boolean },
46
+ listener: (
47
+ eventType: string,
48
+ filename: string | Buffer | null | undefined
49
+ ) => void
50
+ ) => FSWatcher;
51
+ onWatchEvent: (
52
+ collectionName: string,
53
+ watchedRoot: string,
54
+ filename: string | Buffer | null | undefined
55
+ ) => void;
56
+ }
57
+
58
+ export function watcherCollectionFingerprint(
59
+ collection: Collection,
60
+ syncOptions: SyncOptions
61
+ ): string {
62
+ return JSON.stringify({
63
+ path: normalize(collection.path),
64
+ pattern: collection.pattern,
65
+ include: collection.include,
66
+ exclude: collection.exclude,
67
+ languageHint: collection.languageHint ?? null,
68
+ recordAdapters: collection.recordAdapters ?? null,
69
+ sourceAvailability: resolveSourceAvailability(collection, syncOptions),
70
+ limits: syncOptions.limits ?? null,
71
+ concurrency: syncOptions.concurrency ?? null,
72
+ contentTypeRules: syncOptions.contentTypeRules ?? null,
73
+ contentTypeRulesFingerprint:
74
+ syncOptions.contentTypeRulesFingerprint ?? null,
75
+ projectTypedEdges: syncOptions.projectTypedEdges ?? null,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Drop generation/failed tombstones when the name is no longer configured and
81
+ * no in-flight flush still needs the ownership token.
82
+ */
83
+ export function clearLifecycleTombstones(
84
+ host: WatchLifecycleHost,
85
+ collectionName: string
86
+ ): void {
87
+ if (host.syncing.has(collectionName)) {
88
+ return;
89
+ }
90
+ if (host.getCollections().some((entry) => entry.name === collectionName)) {
91
+ return;
92
+ }
93
+ host.collectionGenerations.delete(collectionName);
94
+ host.failedCollections.delete(collectionName);
95
+ host.collectionFingerprints.delete(collectionName);
96
+ }
97
+
98
+ /**
99
+ * Reconcile active watchers with the desired collection set.
100
+ * Closes removed/moved roots, bumps generations on config change, and starts
101
+ * new recursive watchers (capturing events before snapshot baseline init).
102
+ */
103
+ export function applyCollectionUpdate(
104
+ host: WatchLifecycleHost,
105
+ collections: Collection[],
106
+ syncOptions?: SyncOptions
107
+ ): void {
108
+ if (host.disposed()) {
109
+ return;
110
+ }
111
+ if (syncOptions) {
112
+ host.setSyncOptions(syncOptions);
113
+ }
114
+ const nextByName = new Map(
115
+ collections.map((collection) => [collection.name, collection])
116
+ );
117
+
118
+ for (const [collectionName, watcher] of host.watchers) {
119
+ const nextCollection = nextByName.get(collectionName);
120
+ const nextRoot = nextCollection
121
+ ? normalize(nextCollection.path)
122
+ : undefined;
123
+ if (
124
+ nextRoot === undefined ||
125
+ nextRoot !== host.watchRoots.get(collectionName)
126
+ ) {
127
+ watcher.close();
128
+ host.watchers.delete(collectionName);
129
+ host.clearCollectionRuntimeState(collectionName);
130
+ if (nextRoot === undefined) {
131
+ // Removed: bump generation for in-flight ABA, then clear if idle.
132
+ host.collectionGenerations.set(collectionName, host.nextGeneration());
133
+ host.failedCollections.delete(collectionName);
134
+ clearLifecycleTombstones(host, collectionName);
135
+ } else {
136
+ host.failedCollections.delete(collectionName);
137
+ }
138
+ }
139
+ }
140
+
141
+ // Fingerprints / generations / failed for names no longer configured.
142
+ for (const collectionName of [
143
+ ...host.collectionFingerprints.keys(),
144
+ ...host.collectionGenerations.keys(),
145
+ ...host.failedCollections.keys(),
146
+ ]) {
147
+ if (nextByName.has(collectionName)) {
148
+ continue;
149
+ }
150
+ host.collectionFingerprints.delete(collectionName);
151
+ if (!host.collectionGenerations.has(collectionName)) {
152
+ host.collectionGenerations.set(collectionName, host.nextGeneration());
153
+ } else if (!host.syncing.has(collectionName)) {
154
+ host.collectionGenerations.set(collectionName, host.nextGeneration());
155
+ }
156
+ host.clearCollectionRuntimeState(collectionName);
157
+ clearLifecycleTombstones(host, collectionName);
158
+ }
159
+
160
+ host.setCollections(collections);
161
+ for (const collection of collections) {
162
+ const fingerprint = watcherCollectionFingerprint(
163
+ collection,
164
+ host.getSyncOptions()
165
+ );
166
+ const previousFingerprint = host.collectionFingerprints.get(
167
+ collection.name
168
+ );
169
+ if (previousFingerprint === fingerprint) {
170
+ continue;
171
+ }
172
+ host.collectionFingerprints.set(collection.name, fingerprint);
173
+ host.collectionGenerations.set(collection.name, host.nextGeneration());
174
+ host.snapshots.delete(collection.name);
175
+ host.snapshotReady.set(collection.name, false);
176
+ host.snapshotInit.delete(collection.name);
177
+ // Initial start (no prior fingerprint): snapshot only — no full sync.
178
+ // Every later material change (idle same-root, root replacement, exact-only
179
+ // pending, dirty work) durably enqueues generation reconciliation.
180
+ if (previousFingerprint !== undefined) {
181
+ const pending =
182
+ host.pendingByCollection.get(collection.name) ?? emptyPending();
183
+ pending.generationReconcile = true;
184
+ // Queued ambiguous work must not be absorbed by the new baseline.
185
+ if (pending.dirty.size > 0 || pending.overflow || pending.forceFallback) {
186
+ pending.forceFallback = true;
187
+ }
188
+ host.pendingByCollection.set(collection.name, pending);
189
+ }
190
+ if (host.watchers.has(collection.name)) {
191
+ host.beginSnapshotInit(collection);
192
+ }
193
+ }
194
+
195
+ for (const collection of host.getCollections()) {
196
+ if (host.watchers.has(collection.name)) {
197
+ continue;
198
+ }
199
+ try {
200
+ const watchedRoot = normalize(collection.path);
201
+ // Capture events BEFORE async snapshot baseline construction.
202
+ const watcher = host.watchFactory(
203
+ collection.path,
204
+ { recursive: true },
205
+ (_eventType, filename) => {
206
+ host.onWatchEvent(collection.name, watchedRoot, filename);
207
+ }
208
+ );
209
+ host.watchers.set(collection.name, watcher);
210
+ host.watchRoots.set(collection.name, watchedRoot);
211
+ host.failedCollections.delete(collection.name);
212
+ host.snapshotReady.set(collection.name, false);
213
+ host.beginSnapshotInit(collection);
214
+ } catch (error) {
215
+ host.failedCollections.set(
216
+ collection.name,
217
+ error instanceof Error ? error.message : "watch unavailable"
218
+ );
219
+ }
220
+ }
221
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Owned collection flush orchestration for CollectionWatchService.
3
+ *
4
+ * @module src/serve/watch-service-run-flush
5
+ */
6
+
7
+ // node:path — Bun has no path utilities
8
+ import { normalize } from "node:path";
9
+
10
+ import type { Collection } from "../config/types";
11
+ import type { CollectionSyncResult, SyncOptions } from "../ingestion";
12
+ import type { SqliteAdapter } from "../store/sqlite/adapter";
13
+ import type { WatchQueueHost } from "./watch-service-events";
14
+ import type { CollectionPending } from "./watch-service-state";
15
+ import type { WatcherSnapshot, WatcherSnapshotFs } from "./watch-snapshot";
16
+
17
+ import {
18
+ requeueAfterFailure,
19
+ requeueGenerationReconcile,
20
+ startFlush,
21
+ } from "./watch-service-events";
22
+ import { flushCollectionOnce } from "./watch-service-flush";
23
+ import {
24
+ emptyPending,
25
+ pendingHasWork,
26
+ takePending,
27
+ } from "./watch-service-state";
28
+
29
+ export interface RunFlushContext {
30
+ collectionName: string;
31
+ disposed: () => boolean;
32
+ collections: () => Collection[];
33
+ store: SqliteAdapter;
34
+ syncOptions: () => SyncOptions;
35
+ pendingByCollection: Map<string, CollectionPending>;
36
+ flushDeadlineAt: Map<string, number>;
37
+ syncing: Set<string>;
38
+ retryScheduled: Set<string>;
39
+ collectionGenerations: Map<string, number>;
40
+ snapshots: Map<string, WatcherSnapshot>;
41
+ snapshotReady: Map<string, boolean>;
42
+ snapshotInit: Map<string, Promise<void>>;
43
+ suppressedPaths: Map<string, number>;
44
+ clock: () => number;
45
+ queueHost: WatchQueueHost;
46
+ callbacks: {
47
+ onSyncStart?: (event: { collection: string; relPaths: string[] }) => void;
48
+ onSyncComplete?: (event: {
49
+ collection: string;
50
+ relPaths: string[];
51
+ result: CollectionSyncResult;
52
+ }) => void;
53
+ onSyncError?: (event: {
54
+ collection: string;
55
+ relPaths: string[];
56
+ error: unknown;
57
+ }) => void;
58
+ } | null;
59
+ onAfterSync: (collection: Collection, relPaths: string[]) => void;
60
+ beginSnapshotInit: (collection: Collection) => void;
61
+ clearLifecycleTombstones: (collectionName: string) => void;
62
+ pruneSuppression: () => void;
63
+ notifySettledIfIdle: () => void;
64
+ /** Optional injectable FS for unsupported-handle / test seams. */
65
+ snapshotFs?: WatcherSnapshotFs;
66
+ /** Test seam: lower snapshot entry ceiling for overflow→full proofs. */
67
+ snapshotEntryCeiling?: number;
68
+ }
69
+
70
+ /**
71
+ * Drain one collection's pending work under a generation+root ownership token.
72
+ */
73
+ export async function runOwnedCollectionFlush(
74
+ ctx: RunFlushContext
75
+ ): Promise<void> {
76
+ const { collectionName } = ctx;
77
+ if (ctx.disposed()) {
78
+ return;
79
+ }
80
+ const pending = ctx.pendingByCollection.get(collectionName);
81
+ if (!pendingHasWork(pending)) {
82
+ ctx.flushDeadlineAt.delete(collectionName);
83
+ return;
84
+ }
85
+ if (ctx.syncing.has(collectionName) || !pending) {
86
+ return;
87
+ }
88
+
89
+ const collection = ctx
90
+ .collections()
91
+ .find((entry) => entry.name === collectionName);
92
+ if (!collection) {
93
+ ctx.pendingByCollection.delete(collectionName);
94
+ ctx.flushDeadlineAt.delete(collectionName);
95
+ return;
96
+ }
97
+
98
+ const taken = takePending(pending);
99
+ ctx.pendingByCollection.set(collectionName, emptyPending());
100
+ ctx.flushDeadlineAt.delete(collectionName);
101
+ ctx.syncing.add(collectionName);
102
+
103
+ const ownerGeneration = ctx.collectionGenerations.get(collectionName) ?? 0;
104
+ const ownerRoot = normalize(collection.path);
105
+ const stillOwner = (): boolean => {
106
+ if (ctx.disposed()) {
107
+ return false;
108
+ }
109
+ const current = ctx
110
+ .collections()
111
+ .find((entry) => entry.name === collectionName);
112
+ return (
113
+ current !== undefined &&
114
+ (ctx.collectionGenerations.get(collectionName) ?? 0) ===
115
+ ownerGeneration &&
116
+ normalize(current.path) === ownerRoot
117
+ );
118
+ };
119
+
120
+ try {
121
+ const outcome = await flushCollectionOnce({
122
+ collection,
123
+ collectionName,
124
+ store: ctx.store,
125
+ syncOptions: ctx.syncOptions(),
126
+ exactTaken: taken.exact,
127
+ dirtyTaken: taken.dirty,
128
+ forceFallback: taken.forceFallback,
129
+ overflow: taken.overflow,
130
+ generationReconcile: taken.generationReconcile,
131
+ previousSnapshot: ctx.snapshots.get(collectionName) ?? null,
132
+ ownerGeneration,
133
+ ownerRoot,
134
+ disposed: ctx.disposed,
135
+ getCurrentCollection: () =>
136
+ ctx.collections().find((entry) => entry.name === collectionName),
137
+ getCurrentGeneration: () =>
138
+ ctx.collectionGenerations.get(collectionName) ?? 0,
139
+ getCurrentSyncOptions: ctx.syncOptions,
140
+ clock: ctx.clock,
141
+ suppressedPaths: ctx.suppressedPaths,
142
+ snapshotFs: ctx.snapshotFs,
143
+ snapshotEntryCeiling: ctx.snapshotEntryCeiling,
144
+ onSyncStart: (relPaths) => {
145
+ if (!stillOwner()) {
146
+ return;
147
+ }
148
+ ctx.callbacks?.onSyncStart?.({
149
+ collection: collection.name,
150
+ relPaths,
151
+ });
152
+ },
153
+ onSyncComplete: (relPaths, result) => {
154
+ ctx.callbacks?.onSyncComplete?.({
155
+ collection: collectionName,
156
+ relPaths,
157
+ result,
158
+ });
159
+ },
160
+ onSyncError: (relPaths, error) => {
161
+ ctx.callbacks?.onSyncError?.({
162
+ collection: collectionName,
163
+ relPaths,
164
+ error,
165
+ });
166
+ },
167
+ onAfterSync: ctx.onAfterSync,
168
+ commitSnapshot: (snapshot) => {
169
+ if (!stillOwner()) {
170
+ return;
171
+ }
172
+ ctx.snapshots.set(collectionName, snapshot);
173
+ },
174
+ invalidateSnapshot: (current) => {
175
+ const live = ctx
176
+ .collections()
177
+ .find((entry) => entry.name === collectionName);
178
+ if (!live || normalize(live.path) !== normalize(current.path)) {
179
+ return;
180
+ }
181
+ ctx.snapshots.delete(collectionName);
182
+ ctx.snapshotReady.set(collectionName, false);
183
+ ctx.snapshotInit.delete(collectionName);
184
+ ctx.beginSnapshotInit(current);
185
+ },
186
+ requeue: (exact, dirty, forceFlags) => {
187
+ if (!stillOwner()) {
188
+ return;
189
+ }
190
+ requeueAfterFailure(
191
+ ctx.queueHost,
192
+ collectionName,
193
+ exact,
194
+ dirty,
195
+ forceFlags
196
+ );
197
+ },
198
+ requeueGeneration: () => {
199
+ const live = ctx
200
+ .collections()
201
+ .find((entry) => entry.name === collectionName);
202
+ if (!live) {
203
+ return;
204
+ }
205
+ requeueGenerationReconcile(ctx.queueHost, collectionName);
206
+ },
207
+ });
208
+ if (
209
+ outcome.status === "failed" &&
210
+ outcome.error &&
211
+ !(
212
+ outcome.error instanceof Error &&
213
+ (outcome.error.message ===
214
+ "One or more paths failed during watcher sync" ||
215
+ outcome.error.message ===
216
+ "One or more paths failed during watcher generation reconcile")
217
+ )
218
+ ) {
219
+ throw outcome.error;
220
+ }
221
+ } finally {
222
+ ctx.syncing.delete(collectionName);
223
+ ctx.clearLifecycleTombstones(collectionName);
224
+ ctx.pruneSuppression();
225
+ if (!ctx.disposed()) {
226
+ if (
227
+ pendingHasWork(ctx.pendingByCollection.get(collectionName)) &&
228
+ !ctx.retryScheduled.has(collectionName)
229
+ ) {
230
+ startFlush(ctx.queueHost, collectionName);
231
+ } else if (!pendingHasWork(ctx.pendingByCollection.get(collectionName))) {
232
+ ctx.notifySettledIfIdle();
233
+ }
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Per-collection snapshot baseline initialization for the resident watcher.
3
+ *
4
+ * @module src/serve/watch-service-snapshot
5
+ */
6
+
7
+ // node:path — Bun has no path utilities
8
+ import { normalize } from "node:path";
9
+
10
+ import type { Collection } from "../config/types";
11
+ import type { SyncOptions } from "../ingestion";
12
+ import type {
13
+ WatcherSnapshot,
14
+ WatcherSnapshotBuildResult,
15
+ WatcherSnapshotOptions,
16
+ } from "./watch-snapshot";
17
+
18
+ import {
19
+ createDirectoryAvailability,
20
+ memoizeDirectoryAvailability,
21
+ resolveSourceAvailability,
22
+ } from "../ingestion";
23
+ import { buildWatcherSnapshot } from "./watch-snapshot";
24
+
25
+ export interface SnapshotInitHost {
26
+ disposed: () => boolean;
27
+ getGeneration: (collectionName: string) => number;
28
+ getRoot: (collectionName: string) => string | undefined;
29
+ setSnapshot: (collectionName: string, snapshot: WatcherSnapshot) => void;
30
+ clearSnapshot: (collectionName: string) => void;
31
+ setReady: (collectionName: string, ready: boolean) => void;
32
+ getInit: (collectionName: string) => Promise<void> | undefined;
33
+ setInit: (collectionName: string, init: Promise<void> | undefined) => void;
34
+ onReadyWithPending: (collectionName: string) => void;
35
+ /** Current run-level overrides; omitted by narrow unit-test hosts. */
36
+ getSyncOptions?: () => SyncOptions;
37
+ /** Optional injectable builder for hung/slow-init tests. */
38
+ buildSnapshot?: (
39
+ rootAbs: string,
40
+ options?: WatcherSnapshotOptions
41
+ ) => Promise<WatcherSnapshotBuildResult>;
42
+ }
43
+
44
+ /** Start (or supersede) baseline construction; events may already be buffering. */
45
+ export function beginSnapshotInit(
46
+ host: SnapshotInitHost,
47
+ collection: Collection
48
+ ): void {
49
+ if (host.disposed()) {
50
+ return;
51
+ }
52
+ const generation = host.getGeneration(collection.name);
53
+ const root = normalize(collection.path);
54
+ let init!: Promise<void>;
55
+ init = runSnapshotInit(
56
+ host,
57
+ collection.name,
58
+ root,
59
+ generation,
60
+ () => init,
61
+ collection
62
+ );
63
+ host.setInit(collection.name, init);
64
+ void init.catch(() => undefined);
65
+ }
66
+
67
+ async function runSnapshotInit(
68
+ host: SnapshotInitHost,
69
+ collectionName: string,
70
+ root: string,
71
+ generation: number,
72
+ getInit: () => Promise<void>,
73
+ collection: Collection
74
+ ): Promise<void> {
75
+ try {
76
+ const builder = host.buildSnapshot ?? buildWatcherSnapshot;
77
+ const availabilityMode = resolveSourceAvailability(
78
+ collection,
79
+ host.getSyncOptions?.()
80
+ );
81
+ const built = await builder(root, {
82
+ directoryAvailability: memoizeDirectoryAvailability(
83
+ createDirectoryAvailability(availabilityMode)
84
+ ),
85
+ });
86
+ if (host.disposed()) {
87
+ return;
88
+ }
89
+ if (
90
+ host.getGeneration(collectionName) !== generation ||
91
+ host.getRoot(collectionName) !== root
92
+ ) {
93
+ return;
94
+ }
95
+ if (built.status === "ok") {
96
+ host.setSnapshot(collectionName, built.snapshot);
97
+ } else {
98
+ host.clearSnapshot(collectionName);
99
+ }
100
+ } catch {
101
+ if (
102
+ !host.disposed() &&
103
+ host.getGeneration(collectionName) === generation &&
104
+ host.getRoot(collectionName) === root
105
+ ) {
106
+ host.clearSnapshot(collectionName);
107
+ }
108
+ }
109
+
110
+ if (host.getInit(collectionName) !== getInit()) {
111
+ return;
112
+ }
113
+ host.setInit(collectionName, undefined);
114
+ if (host.disposed()) {
115
+ return;
116
+ }
117
+ // Readiness flips even on init failure so forceFallback classification can run.
118
+ if (
119
+ host.getGeneration(collectionName) === generation &&
120
+ host.getRoot(collectionName) === root
121
+ ) {
122
+ host.setReady(collectionName, true);
123
+ host.onReadyWithPending(collectionName);
124
+ }
125
+ }