@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.
- package/README.md +11 -1
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.4.zip → gno-browser-clipper-v1.34.6.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +5 -1
- package/spec/cli.md +16 -10
- package/src/cli/pager.ts +29 -14
- package/src/ingestion/sync.ts +368 -84
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +220 -0
- package/src/serve/watch-reconciliation-fallback.ts +404 -0
- package/src/serve/watch-reconciliation-shared.ts +343 -0
- package/src/serve/watch-reconciliation.ts +122 -0
- package/src/serve/watch-service-events.ts +261 -0
- package/src/serve/watch-service-flush-generation.ts +140 -0
- package/src/serve/watch-service-flush-helpers.ts +147 -0
- package/src/serve/watch-service-flush.ts +433 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +219 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +101 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +265 -306
- package/src/serve/watch-snapshot-handles.ts +285 -0
- package/src/serve/watch-snapshot-libc.ts +391 -0
- package/src/serve/watch-snapshot-ops.ts +399 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +297 -0
- package/src/serve/watch-snapshot-types.ts +350 -0
- package/src/serve/watch-snapshot.ts +51 -0
- package/src/store/index.ts +1 -1
- package/src/store/sqlite/adapter.ts +191 -0
- package/src/store/types.ts +66 -0
- package/vendor/fts5-snowball/README.md +5 -1
- package/vendor/fts5-snowball/darwin-x64/fts5stemmer.dylib +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.4.zip.sha256 +0 -1
|
@@ -1,17 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared resident collection filesystem watcher for serve and daemon.
|
|
3
|
+
* Exact eligible paths content-hash via syncPaths; ambiguous events use
|
|
4
|
+
* snapshot/fallback reconciliation before a targeted batch.
|
|
5
|
+
*
|
|
6
|
+
* @module src/serve/watch-service
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { watch, type FSWatcher } from "node:fs";
|
|
2
|
-
|
|
10
|
+
// node:path — Bun has no path utilities
|
|
11
|
+
import { normalize, sep } from "node:path";
|
|
3
12
|
|
|
4
13
|
import type { Collection } from "../config/types";
|
|
5
14
|
import type { CollectionSyncResult, SyncOptions } from "../ingestion";
|
|
6
15
|
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
7
16
|
import type { DocumentEvent, DocumentEventBus } from "./doc-events";
|
|
8
17
|
import type { EmbedScheduler } from "./embed-scheduler";
|
|
18
|
+
import type {
|
|
19
|
+
WatcherSnapshot,
|
|
20
|
+
WatcherSnapshotBuildResult,
|
|
21
|
+
WatcherSnapshotFs,
|
|
22
|
+
WatcherSnapshotOptions,
|
|
23
|
+
} from "./watch-snapshot";
|
|
9
24
|
|
|
10
25
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
WATCHER_MAX_DIRTY_HINTS,
|
|
27
|
+
WATCHER_MAX_EXACT_PATHS,
|
|
28
|
+
WATCHER_MAX_SUPPRESSION_ENTRIES,
|
|
29
|
+
pruneSuppressionMap,
|
|
30
|
+
} from "./watch-reconciliation";
|
|
31
|
+
import {
|
|
32
|
+
enqueueDirtyHint,
|
|
33
|
+
enqueueExactPath,
|
|
34
|
+
handleWatchEvent,
|
|
35
|
+
scheduleFlush,
|
|
36
|
+
WATCHER_FLUSH_DEBOUNCE_MS,
|
|
37
|
+
WATCHER_MAX_FLUSH_DELAY_MS,
|
|
38
|
+
} from "./watch-service-events";
|
|
39
|
+
import {
|
|
40
|
+
buildEventHost,
|
|
41
|
+
buildLifecycleHost,
|
|
42
|
+
buildQueueHost,
|
|
43
|
+
type WatchServiceHostState,
|
|
44
|
+
} from "./watch-service-hosts";
|
|
45
|
+
import {
|
|
46
|
+
applyCollectionUpdate,
|
|
47
|
+
clearLifecycleTombstones,
|
|
48
|
+
} from "./watch-service-lifecycle";
|
|
49
|
+
import { runOwnedCollectionFlush } from "./watch-service-run-flush";
|
|
50
|
+
import { beginSnapshotInit } from "./watch-service-snapshot";
|
|
51
|
+
import { pendingHasWork, type CollectionPending } from "./watch-service-state";
|
|
15
52
|
|
|
16
53
|
export interface CollectionWatchState {
|
|
17
54
|
expectedCollections: string[];
|
|
@@ -47,41 +84,30 @@ interface CollectionWatchServiceOptions {
|
|
|
47
84
|
callbacks?: CollectionWatchCallbacks;
|
|
48
85
|
syncOptions?: SyncOptions;
|
|
49
86
|
watchFactory?: typeof watch;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fallbackPaths: string[] = []
|
|
75
|
-
): string[] {
|
|
76
|
-
if (result.files) {
|
|
77
|
-
return result.files
|
|
78
|
-
.filter((file) => file.status === "added" || file.status === "updated")
|
|
79
|
-
.map((file) => file.relPath);
|
|
80
|
-
}
|
|
81
|
-
return result.filesAdded + result.filesUpdated + result.filesMarkedInactive >
|
|
82
|
-
0
|
|
83
|
-
? fallbackPaths
|
|
84
|
-
: [];
|
|
87
|
+
/** Test overrides for debounce / hard deadline (milliseconds). */
|
|
88
|
+
flushDebounceMs?: number;
|
|
89
|
+
maxFlushDelayMs?: number;
|
|
90
|
+
maxExactPaths?: number;
|
|
91
|
+
maxDirtyHints?: number;
|
|
92
|
+
clock?: () => number;
|
|
93
|
+
/**
|
|
94
|
+
* Injectable snapshot baseline builder (tests: hung/slow init). Production
|
|
95
|
+
* uses the default buildWatcherSnapshot path via beginSnapshotInit.
|
|
96
|
+
*/
|
|
97
|
+
buildSnapshot?: (
|
|
98
|
+
rootAbs: string,
|
|
99
|
+
options?: WatcherSnapshotOptions
|
|
100
|
+
) => Promise<WatcherSnapshotBuildResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Injectable snapshot/fallback FS (tests: unsupported-handle proofs).
|
|
103
|
+
* Production uses the platform default via classifyDirtyHints.
|
|
104
|
+
*/
|
|
105
|
+
snapshotFs?: WatcherSnapshotFs;
|
|
106
|
+
/**
|
|
107
|
+
* Test seam: lower snapshot entry ceiling to force overflow→full reconcile.
|
|
108
|
+
* Production leaves this unset (uses WATCHER_SNAPSHOT_ENTRY_CEILING).
|
|
109
|
+
*/
|
|
110
|
+
snapshotEntryCeiling?: number;
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
export class CollectionWatchService {
|
|
@@ -95,13 +121,31 @@ export class CollectionWatchService {
|
|
|
95
121
|
readonly #watchRoots = new Map<string, string>();
|
|
96
122
|
readonly #collectionGenerations = new Map<string, number>();
|
|
97
123
|
readonly #collectionFingerprints = new Map<string, string>();
|
|
98
|
-
readonly #pendingByCollection = new Map<string,
|
|
124
|
+
readonly #pendingByCollection = new Map<string, CollectionPending>();
|
|
99
125
|
readonly #timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
126
|
+
readonly #flushDeadlineAt = new Map<string, number>();
|
|
127
|
+
readonly #retryScheduled = new Set<string>();
|
|
100
128
|
readonly #syncing = new Set<string>();
|
|
101
129
|
readonly #inFlightSyncs = new Set<Promise<void>>();
|
|
102
130
|
readonly #suppressedPaths = new Map<string, number>();
|
|
131
|
+
readonly #snapshots = new Map<string, WatcherSnapshot>();
|
|
132
|
+
readonly #snapshotReady = new Map<string, boolean>();
|
|
133
|
+
readonly #snapshotInit = new Map<string, Promise<void>>();
|
|
103
134
|
readonly #watchFactory: typeof watch;
|
|
104
135
|
readonly #failedCollections = new Map<string, string>();
|
|
136
|
+
readonly #flushDebounceMs: number;
|
|
137
|
+
readonly #maxFlushDelayMs: number;
|
|
138
|
+
readonly #maxExactPaths: number;
|
|
139
|
+
readonly #maxDirtyHints: number;
|
|
140
|
+
readonly #clock: () => number;
|
|
141
|
+
readonly #buildSnapshot:
|
|
142
|
+
| ((
|
|
143
|
+
rootAbs: string,
|
|
144
|
+
options?: WatcherSnapshotOptions
|
|
145
|
+
) => Promise<WatcherSnapshotBuildResult>)
|
|
146
|
+
| undefined;
|
|
147
|
+
readonly #snapshotFs: WatcherSnapshotFs | undefined;
|
|
148
|
+
readonly #snapshotEntryCeiling: number | undefined;
|
|
105
149
|
#nextCollectionGeneration = 0;
|
|
106
150
|
#disposed = false;
|
|
107
151
|
#lastEventAt: string | null = null;
|
|
@@ -115,124 +159,43 @@ export class CollectionWatchService {
|
|
|
115
159
|
this.#callbacks = options.callbacks ?? null;
|
|
116
160
|
this.#syncOptions = options.syncOptions ?? {};
|
|
117
161
|
this.#watchFactory = options.watchFactory ?? watch;
|
|
162
|
+
this.#flushDebounceMs =
|
|
163
|
+
options.flushDebounceMs ?? WATCHER_FLUSH_DEBOUNCE_MS;
|
|
164
|
+
this.#maxFlushDelayMs =
|
|
165
|
+
options.maxFlushDelayMs ?? WATCHER_MAX_FLUSH_DELAY_MS;
|
|
166
|
+
this.#maxExactPaths = options.maxExactPaths ?? WATCHER_MAX_EXACT_PATHS;
|
|
167
|
+
this.#maxDirtyHints = options.maxDirtyHints ?? WATCHER_MAX_DIRTY_HINTS;
|
|
168
|
+
this.#clock = options.clock ?? Date.now;
|
|
169
|
+
this.#buildSnapshot = options.buildSnapshot;
|
|
170
|
+
this.#snapshotFs = options.snapshotFs;
|
|
171
|
+
this.#snapshotEntryCeiling = options.snapshotEntryCeiling;
|
|
118
172
|
}
|
|
119
173
|
|
|
120
174
|
start(): void {
|
|
121
|
-
if (this.#disposed) {
|
|
122
|
-
|
|
175
|
+
if (!this.#disposed) {
|
|
176
|
+
this.updateCollections(this.#collections);
|
|
123
177
|
}
|
|
124
|
-
this.updateCollections(this.#collections);
|
|
125
178
|
}
|
|
126
179
|
|
|
127
180
|
updateCollections(
|
|
128
181
|
collections: Collection[],
|
|
129
182
|
syncOptions?: SyncOptions
|
|
130
183
|
): void {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.#syncOptions = syncOptions;
|
|
136
|
-
}
|
|
137
|
-
const nextByName = new Map(
|
|
138
|
-
collections.map((collection) => [collection.name, collection])
|
|
184
|
+
applyCollectionUpdate(
|
|
185
|
+
buildLifecycleHost(this.#hostState()),
|
|
186
|
+
collections,
|
|
187
|
+
syncOptions
|
|
139
188
|
);
|
|
140
|
-
|
|
141
|
-
for (const [collectionName, watcher] of this.#watchers) {
|
|
142
|
-
const nextCollection = nextByName.get(collectionName);
|
|
143
|
-
const nextRoot = nextCollection
|
|
144
|
-
? normalize(nextCollection.path)
|
|
145
|
-
: undefined;
|
|
146
|
-
if (
|
|
147
|
-
nextRoot === undefined ||
|
|
148
|
-
nextRoot !== this.#watchRoots.get(collectionName)
|
|
149
|
-
) {
|
|
150
|
-
watcher.close();
|
|
151
|
-
this.#watchers.delete(collectionName);
|
|
152
|
-
this.#watchRoots.delete(collectionName);
|
|
153
|
-
this.#failedCollections.delete(collectionName);
|
|
154
|
-
this.#pendingByCollection.delete(collectionName);
|
|
155
|
-
const timer = this.#timers.get(collectionName);
|
|
156
|
-
if (timer) {
|
|
157
|
-
clearTimeout(timer);
|
|
158
|
-
this.#timers.delete(collectionName);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
for (const collectionName of this.#collectionFingerprints.keys()) {
|
|
164
|
-
if (!nextByName.has(collectionName)) {
|
|
165
|
-
this.#collectionFingerprints.delete(collectionName);
|
|
166
|
-
this.#collectionGenerations.set(
|
|
167
|
-
collectionName,
|
|
168
|
-
++this.#nextCollectionGeneration
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
this.#collections = collections;
|
|
174
|
-
for (const collection of collections) {
|
|
175
|
-
const fingerprint = watcherCollectionFingerprint(
|
|
176
|
-
collection,
|
|
177
|
-
this.#syncOptions
|
|
178
|
-
);
|
|
179
|
-
if (this.#collectionFingerprints.get(collection.name) !== fingerprint) {
|
|
180
|
-
this.#collectionFingerprints.set(collection.name, fingerprint);
|
|
181
|
-
this.#collectionGenerations.set(
|
|
182
|
-
collection.name,
|
|
183
|
-
++this.#nextCollectionGeneration
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const collection of this.#collections) {
|
|
189
|
-
if (this.#watchers.has(collection.name)) {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
const watchedRoot = normalize(collection.path);
|
|
194
|
-
const watcher = this.#watchFactory(
|
|
195
|
-
collection.path,
|
|
196
|
-
{ recursive: true },
|
|
197
|
-
(_eventType, filename) => {
|
|
198
|
-
if (this.#disposed || !filename) return;
|
|
199
|
-
const relPath = filename.toString().replaceAll("\\", "/");
|
|
200
|
-
const currentCollection = this.#collections.find(
|
|
201
|
-
(entry) => entry.name === collection.name
|
|
202
|
-
);
|
|
203
|
-
if (
|
|
204
|
-
!currentCollection ||
|
|
205
|
-
normalize(currentCollection.path) !== watchedRoot ||
|
|
206
|
-
!matchesWalkPath(
|
|
207
|
-
relPath,
|
|
208
|
-
collectionToWalkConfig(currentCollection, 0)
|
|
209
|
-
)
|
|
210
|
-
) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const fullPath = normalize(join(watchedRoot, relPath));
|
|
214
|
-
const suppressedUntil = this.#suppressedPaths.get(fullPath);
|
|
215
|
-
if (suppressedUntil && suppressedUntil > Date.now()) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
this.#lastEventAt = new Date().toISOString();
|
|
219
|
-
this.#queueChange(collection.name, relPath);
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
this.#watchers.set(collection.name, watcher);
|
|
223
|
-
this.#watchRoots.set(collection.name, watchedRoot);
|
|
224
|
-
this.#failedCollections.delete(collection.name);
|
|
225
|
-
} catch (error) {
|
|
226
|
-
this.#failedCollections.set(
|
|
227
|
-
collection.name,
|
|
228
|
-
error instanceof Error ? error.message : "watch unavailable"
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
189
|
}
|
|
233
190
|
|
|
234
191
|
suppress(absPath: string, ms = 5_000): void {
|
|
235
|
-
|
|
192
|
+
const now = this.#clock();
|
|
193
|
+
this.#suppressedPaths.set(normalize(absPath), now + ms);
|
|
194
|
+
pruneSuppressionMap(
|
|
195
|
+
this.#suppressedPaths,
|
|
196
|
+
now,
|
|
197
|
+
WATCHER_MAX_SUPPRESSION_ENTRIES
|
|
198
|
+
);
|
|
236
199
|
}
|
|
237
200
|
|
|
238
201
|
async dispose(): Promise<void> {
|
|
@@ -247,198 +210,197 @@ export class CollectionWatchService {
|
|
|
247
210
|
watcher.close();
|
|
248
211
|
}
|
|
249
212
|
this.#timers.clear();
|
|
213
|
+
this.#flushDeadlineAt.clear();
|
|
214
|
+
this.#retryScheduled.clear();
|
|
250
215
|
this.#watchers.clear();
|
|
251
216
|
this.#watchRoots.clear();
|
|
252
217
|
this.#collectionGenerations.clear();
|
|
253
218
|
this.#collectionFingerprints.clear();
|
|
254
219
|
this.#collections = [];
|
|
255
220
|
this.#pendingByCollection.clear();
|
|
221
|
+
this.#snapshots.clear();
|
|
222
|
+
this.#snapshotReady.clear();
|
|
223
|
+
this.#snapshotInit.clear();
|
|
224
|
+
this.#suppressedPaths.clear();
|
|
256
225
|
await Promise.allSettled(this.#inFlightSyncs);
|
|
257
226
|
this.#syncing.clear();
|
|
258
227
|
}
|
|
259
228
|
|
|
260
229
|
getState(): CollectionWatchState {
|
|
261
230
|
return {
|
|
262
|
-
expectedCollections: this.#collections.map(
|
|
263
|
-
(collection) => collection.name
|
|
264
|
-
),
|
|
231
|
+
expectedCollections: this.#collections.map((c) => c.name),
|
|
265
232
|
activeCollections: [...this.#watchers.keys()],
|
|
266
233
|
failedCollections: [...this.#failedCollections.entries()].map(
|
|
267
234
|
([collection, reason]) => ({ collection, reason })
|
|
268
235
|
),
|
|
269
236
|
queuedCollections: [...this.#pendingByCollection.entries()]
|
|
270
|
-
.filter(([,
|
|
271
|
-
.map(([
|
|
237
|
+
.filter(([, pending]) => pendingHasWork(pending))
|
|
238
|
+
.map(([name]) => name),
|
|
272
239
|
syncingCollections: [...this.#syncing],
|
|
273
240
|
lastEventAt: this.#lastEventAt,
|
|
274
241
|
lastSyncAt: this.#lastSyncAt,
|
|
275
242
|
};
|
|
276
243
|
}
|
|
277
244
|
|
|
278
|
-
#
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
245
|
+
#hostState(): WatchServiceHostState {
|
|
246
|
+
return {
|
|
247
|
+
disposed: () => this.#disposed,
|
|
248
|
+
getCollections: () => this.#collections,
|
|
249
|
+
setCollections: (collections) => {
|
|
250
|
+
this.#collections = collections;
|
|
251
|
+
},
|
|
252
|
+
getSyncOptions: () => this.#syncOptions,
|
|
253
|
+
setSyncOptions: (syncOptions) => {
|
|
254
|
+
this.#syncOptions = syncOptions;
|
|
255
|
+
},
|
|
256
|
+
watchers: this.#watchers,
|
|
257
|
+
watchRoots: this.#watchRoots,
|
|
258
|
+
collectionFingerprints: this.#collectionFingerprints,
|
|
259
|
+
collectionGenerations: this.#collectionGenerations,
|
|
260
|
+
nextGeneration: () => ++this.#nextCollectionGeneration,
|
|
261
|
+
failedCollections: this.#failedCollections,
|
|
262
|
+
snapshots: this.#snapshots,
|
|
263
|
+
snapshotReady: this.#snapshotReady,
|
|
264
|
+
snapshotInit: this.#snapshotInit,
|
|
265
|
+
syncing: this.#syncing,
|
|
266
|
+
pendingByCollection: this.#pendingByCollection,
|
|
267
|
+
clearCollectionRuntimeState: (name) => {
|
|
268
|
+
this.#clearCollectionRuntimeState(name);
|
|
269
|
+
},
|
|
270
|
+
beginSnapshotInit: (collection) => {
|
|
271
|
+
this.#beginSnapshotInit(collection);
|
|
272
|
+
},
|
|
273
|
+
watchFactory: this.#watchFactory,
|
|
274
|
+
onWatchEvent: (name, root, filename) => {
|
|
275
|
+
handleWatchEvent(
|
|
276
|
+
buildEventHost(this.#hostState()),
|
|
277
|
+
name,
|
|
278
|
+
root,
|
|
279
|
+
filename
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
findCollection: (name) =>
|
|
283
|
+
this.#collections.find((entry) => entry.name === name),
|
|
284
|
+
clock: this.#clock,
|
|
285
|
+
suppressedPaths: this.#suppressedPaths,
|
|
286
|
+
setLastEventAt: (iso) => {
|
|
287
|
+
this.#lastEventAt = iso;
|
|
288
|
+
},
|
|
289
|
+
enqueueExact: (name, relPath) => {
|
|
290
|
+
enqueueExactPath(buildQueueHost(this.#hostState()), name, relPath);
|
|
291
|
+
},
|
|
292
|
+
enqueueDirty: (name, hint) => {
|
|
293
|
+
enqueueDirtyHint(buildQueueHost(this.#hostState()), name, hint);
|
|
294
|
+
},
|
|
295
|
+
flushDebounceMs: this.#flushDebounceMs,
|
|
296
|
+
maxFlushDelayMs: this.#maxFlushDelayMs,
|
|
297
|
+
maxExactPaths: this.#maxExactPaths,
|
|
298
|
+
maxDirtyHints: this.#maxDirtyHints,
|
|
299
|
+
flushDeadlineAt: this.#flushDeadlineAt,
|
|
300
|
+
timers: this.#timers,
|
|
301
|
+
retryScheduled: this.#retryScheduled,
|
|
302
|
+
inFlightSyncs: this.#inFlightSyncs,
|
|
303
|
+
runFlush: (name) => this.#flushCollection(name),
|
|
304
|
+
};
|
|
297
305
|
}
|
|
298
306
|
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
307
|
+
#clearCollectionRuntimeState(collectionName: string): void {
|
|
308
|
+
this.#watchRoots.delete(collectionName);
|
|
309
|
+
this.#pendingByCollection.delete(collectionName);
|
|
310
|
+
this.#snapshots.delete(collectionName);
|
|
311
|
+
this.#snapshotReady.delete(collectionName);
|
|
312
|
+
this.#snapshotInit.delete(collectionName);
|
|
313
|
+
this.#flushDeadlineAt.delete(collectionName);
|
|
314
|
+
this.#retryScheduled.delete(collectionName);
|
|
315
|
+
const timer = this.#timers.get(collectionName);
|
|
316
|
+
if (timer) {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
this.#timers.delete(collectionName);
|
|
302
319
|
}
|
|
303
|
-
const sync = this.#flushCollection(collectionName);
|
|
304
|
-
this.#inFlightSyncs.add(sync);
|
|
305
|
-
void sync
|
|
306
|
-
.finally(() => {
|
|
307
|
-
this.#inFlightSyncs.delete(sync);
|
|
308
|
-
})
|
|
309
|
-
.catch(() => undefined);
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
matchesWalkPath(relPath, collectionToWalkConfig(collection, 0))
|
|
334
|
-
);
|
|
335
|
-
let syncGeneration = this.#collectionGenerations.get(collectionName) ?? 0;
|
|
336
|
-
this.#pendingByCollection.set(collectionName, new Set<string>());
|
|
337
|
-
if (relPaths.length === 0) {
|
|
338
|
-
this.#notifySettledIfIdle();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
this.#syncing.add(collectionName);
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
this.#callbacks?.onSyncStart?.({
|
|
345
|
-
collection: collection.name,
|
|
346
|
-
relPaths,
|
|
347
|
-
});
|
|
348
|
-
let result = await defaultSyncService.syncPaths(
|
|
349
|
-
collection,
|
|
350
|
-
this.#store,
|
|
351
|
-
relPaths,
|
|
352
|
-
{
|
|
353
|
-
...this.#syncOptions,
|
|
354
|
-
runUpdateCmd: false,
|
|
355
|
-
}
|
|
356
|
-
);
|
|
357
|
-
if (this.#disposed) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
this.#callbacks?.onSyncComplete?.({
|
|
361
|
-
collection: collection.name,
|
|
362
|
-
relPaths,
|
|
363
|
-
result,
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
let completionCollection = collection;
|
|
367
|
-
let completionPaths = changedPaths(result, relPaths);
|
|
368
|
-
while (true) {
|
|
369
|
-
const currentCollection = this.#collections.find(
|
|
370
|
-
(entry) => entry.name === collectionName
|
|
371
|
-
);
|
|
372
|
-
if (!currentCollection) {
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
const currentGeneration =
|
|
376
|
-
this.#collectionGenerations.get(collectionName) ?? 0;
|
|
377
|
-
if (currentGeneration === syncGeneration) {
|
|
378
|
-
const currentRelPaths =
|
|
379
|
-
normalize(currentCollection.path) ===
|
|
380
|
-
normalize(completionCollection.path)
|
|
381
|
-
? completionPaths.filter((relPath) =>
|
|
382
|
-
matchesWalkPath(
|
|
383
|
-
relPath,
|
|
384
|
-
collectionToWalkConfig(currentCollection, 0)
|
|
385
|
-
)
|
|
386
|
-
)
|
|
387
|
-
: [];
|
|
388
|
-
if (currentRelPaths.length > 0) {
|
|
389
|
-
this.#afterSync(currentCollection, currentRelPaths);
|
|
322
|
+
#beginSnapshotInit(collection: Collection): void {
|
|
323
|
+
beginSnapshotInit(
|
|
324
|
+
{
|
|
325
|
+
disposed: () => this.#disposed,
|
|
326
|
+
getGeneration: (name) => this.#collectionGenerations.get(name) ?? 0,
|
|
327
|
+
getRoot: (name) => this.#watchRoots.get(name),
|
|
328
|
+
setSnapshot: (name, snapshot) => {
|
|
329
|
+
this.#snapshots.set(name, snapshot);
|
|
330
|
+
},
|
|
331
|
+
clearSnapshot: (name) => {
|
|
332
|
+
this.#snapshots.delete(name);
|
|
333
|
+
},
|
|
334
|
+
setReady: (name, ready) => {
|
|
335
|
+
this.#snapshotReady.set(name, ready);
|
|
336
|
+
},
|
|
337
|
+
getInit: (name) => this.#snapshotInit.get(name),
|
|
338
|
+
setInit: (name, init) => {
|
|
339
|
+
if (init) {
|
|
340
|
+
this.#snapshotInit.set(name, init);
|
|
341
|
+
} else {
|
|
342
|
+
this.#snapshotInit.delete(name);
|
|
390
343
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
currentCollection,
|
|
396
|
-
this.#store,
|
|
397
|
-
{
|
|
398
|
-
...this.#syncOptions,
|
|
399
|
-
runUpdateCmd: false,
|
|
344
|
+
},
|
|
345
|
+
onReadyWithPending: (name) => {
|
|
346
|
+
if (pendingHasWork(this.#pendingByCollection.get(name))) {
|
|
347
|
+
scheduleFlush(buildQueueHost(this.#hostState()), name);
|
|
400
348
|
}
|
|
349
|
+
},
|
|
350
|
+
buildSnapshot: this.#buildSnapshot,
|
|
351
|
+
},
|
|
352
|
+
collection
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async #flushCollection(collectionName: string): Promise<void> {
|
|
357
|
+
await runOwnedCollectionFlush({
|
|
358
|
+
collectionName,
|
|
359
|
+
disposed: () => this.#disposed,
|
|
360
|
+
collections: () => this.#collections,
|
|
361
|
+
store: this.#store,
|
|
362
|
+
syncOptions: () => this.#syncOptions,
|
|
363
|
+
pendingByCollection: this.#pendingByCollection,
|
|
364
|
+
flushDeadlineAt: this.#flushDeadlineAt,
|
|
365
|
+
syncing: this.#syncing,
|
|
366
|
+
retryScheduled: this.#retryScheduled,
|
|
367
|
+
collectionGenerations: this.#collectionGenerations,
|
|
368
|
+
snapshots: this.#snapshots,
|
|
369
|
+
snapshotReady: this.#snapshotReady,
|
|
370
|
+
snapshotInit: this.#snapshotInit,
|
|
371
|
+
suppressedPaths: this.#suppressedPaths,
|
|
372
|
+
clock: this.#clock,
|
|
373
|
+
queueHost: buildQueueHost(this.#hostState()),
|
|
374
|
+
callbacks: this.#callbacks,
|
|
375
|
+
onAfterSync: (current, relPaths) => {
|
|
376
|
+
this.#afterSync(current, relPaths);
|
|
377
|
+
},
|
|
378
|
+
beginSnapshotInit: (current) => {
|
|
379
|
+
this.#beginSnapshotInit(current);
|
|
380
|
+
},
|
|
381
|
+
clearLifecycleTombstones: (name) => {
|
|
382
|
+
clearLifecycleTombstones(buildLifecycleHost(this.#hostState()), name);
|
|
383
|
+
},
|
|
384
|
+
pruneSuppression: () => {
|
|
385
|
+
pruneSuppressionMap(
|
|
386
|
+
this.#suppressedPaths,
|
|
387
|
+
this.#clock(),
|
|
388
|
+
WATCHER_MAX_SUPPRESSION_ENTRIES
|
|
401
389
|
);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
collection: currentCollection.name,
|
|
410
|
-
relPaths: completionPaths,
|
|
411
|
-
result,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
} catch (error) {
|
|
415
|
-
if (this.#disposed) {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
this.#callbacks?.onSyncError?.({
|
|
419
|
-
collection: collection.name,
|
|
420
|
-
relPaths,
|
|
421
|
-
error,
|
|
422
|
-
});
|
|
423
|
-
throw error;
|
|
424
|
-
} finally {
|
|
425
|
-
this.#syncing.delete(collectionName);
|
|
426
|
-
if (!this.#disposed) {
|
|
427
|
-
const remaining = this.#pendingByCollection.get(collectionName);
|
|
428
|
-
if (remaining && remaining.size > 0) {
|
|
429
|
-
this.#startFlush(collectionName);
|
|
430
|
-
} else {
|
|
431
|
-
this.#notifySettledIfIdle();
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
390
|
+
},
|
|
391
|
+
notifySettledIfIdle: () => {
|
|
392
|
+
this.#notifySettledIfIdle();
|
|
393
|
+
},
|
|
394
|
+
snapshotFs: this.#snapshotFs,
|
|
395
|
+
snapshotEntryCeiling: this.#snapshotEntryCeiling,
|
|
396
|
+
});
|
|
435
397
|
}
|
|
436
398
|
|
|
437
399
|
#notifySettledIfIdle(): void {
|
|
438
400
|
if (
|
|
439
401
|
this.#syncing.size === 0 &&
|
|
440
|
-
![...this.#pendingByCollection.values()].some(
|
|
441
|
-
(
|
|
402
|
+
![...this.#pendingByCollection.values()].some((pending) =>
|
|
403
|
+
pendingHasWork(pending)
|
|
442
404
|
)
|
|
443
405
|
) {
|
|
444
406
|
this.#callbacks?.onSettled?.();
|
|
@@ -449,14 +411,11 @@ export class CollectionWatchService {
|
|
|
449
411
|
if (this.#disposed || relPaths.length === 0) {
|
|
450
412
|
return;
|
|
451
413
|
}
|
|
452
|
-
|
|
453
|
-
this.#lastSyncAt = new Date().toISOString();
|
|
414
|
+
this.#lastSyncAt = new Date(this.#clock()).toISOString();
|
|
454
415
|
this.#scheduler?.notifySyncComplete(relPaths);
|
|
455
|
-
|
|
456
416
|
if (!this.#eventBus) {
|
|
457
417
|
return;
|
|
458
418
|
}
|
|
459
|
-
|
|
460
419
|
for (const relPath of relPaths) {
|
|
461
420
|
const event: DocumentEvent = {
|
|
462
421
|
type: "document-changed",
|
|
@@ -464,7 +423,7 @@ export class CollectionWatchService {
|
|
|
464
423
|
collection: collection.name,
|
|
465
424
|
relPath,
|
|
466
425
|
origin: "watcher",
|
|
467
|
-
changedAt: new Date().toISOString(),
|
|
426
|
+
changedAt: new Date(this.#clock()).toISOString(),
|
|
468
427
|
};
|
|
469
428
|
this.#eventBus.emit(event);
|
|
470
429
|
}
|