@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.
- package/README.md +22 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/spec/cli.md +43 -0
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +565 -108
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
- package/src/serve/watch-reconciliation-fallback.ts +434 -0
- package/src/serve/watch-reconciliation-shared.ts +348 -0
- package/src/serve/watch-reconciliation.ts +129 -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 +443 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +221 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +125 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +266 -306
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +365 -0
- package/src/serve/watch-snapshot-libc.ts +510 -0
- package/src/serve/watch-snapshot-ops.ts +541 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +300 -0
- package/src/serve/watch-snapshot-types.ts +392 -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/browser-extension/artifacts/gno-browser-clipper-v1.34.5.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,198 @@ 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
|
+
getSyncOptions: () => this.#syncOptions,
|
|
351
|
+
buildSnapshot: this.#buildSnapshot,
|
|
352
|
+
},
|
|
353
|
+
collection
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async #flushCollection(collectionName: string): Promise<void> {
|
|
358
|
+
await runOwnedCollectionFlush({
|
|
359
|
+
collectionName,
|
|
360
|
+
disposed: () => this.#disposed,
|
|
361
|
+
collections: () => this.#collections,
|
|
362
|
+
store: this.#store,
|
|
363
|
+
syncOptions: () => this.#syncOptions,
|
|
364
|
+
pendingByCollection: this.#pendingByCollection,
|
|
365
|
+
flushDeadlineAt: this.#flushDeadlineAt,
|
|
366
|
+
syncing: this.#syncing,
|
|
367
|
+
retryScheduled: this.#retryScheduled,
|
|
368
|
+
collectionGenerations: this.#collectionGenerations,
|
|
369
|
+
snapshots: this.#snapshots,
|
|
370
|
+
snapshotReady: this.#snapshotReady,
|
|
371
|
+
snapshotInit: this.#snapshotInit,
|
|
372
|
+
suppressedPaths: this.#suppressedPaths,
|
|
373
|
+
clock: this.#clock,
|
|
374
|
+
queueHost: buildQueueHost(this.#hostState()),
|
|
375
|
+
callbacks: this.#callbacks,
|
|
376
|
+
onAfterSync: (current, relPaths) => {
|
|
377
|
+
this.#afterSync(current, relPaths);
|
|
378
|
+
},
|
|
379
|
+
beginSnapshotInit: (current) => {
|
|
380
|
+
this.#beginSnapshotInit(current);
|
|
381
|
+
},
|
|
382
|
+
clearLifecycleTombstones: (name) => {
|
|
383
|
+
clearLifecycleTombstones(buildLifecycleHost(this.#hostState()), name);
|
|
384
|
+
},
|
|
385
|
+
pruneSuppression: () => {
|
|
386
|
+
pruneSuppressionMap(
|
|
387
|
+
this.#suppressedPaths,
|
|
388
|
+
this.#clock(),
|
|
389
|
+
WATCHER_MAX_SUPPRESSION_ENTRIES
|
|
401
390
|
);
|
|
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
|
-
}
|
|
391
|
+
},
|
|
392
|
+
notifySettledIfIdle: () => {
|
|
393
|
+
this.#notifySettledIfIdle();
|
|
394
|
+
},
|
|
395
|
+
snapshotFs: this.#snapshotFs,
|
|
396
|
+
snapshotEntryCeiling: this.#snapshotEntryCeiling,
|
|
397
|
+
});
|
|
435
398
|
}
|
|
436
399
|
|
|
437
400
|
#notifySettledIfIdle(): void {
|
|
438
401
|
if (
|
|
439
402
|
this.#syncing.size === 0 &&
|
|
440
|
-
![...this.#pendingByCollection.values()].some(
|
|
441
|
-
(
|
|
403
|
+
![...this.#pendingByCollection.values()].some((pending) =>
|
|
404
|
+
pendingHasWork(pending)
|
|
442
405
|
)
|
|
443
406
|
) {
|
|
444
407
|
this.#callbacks?.onSettled?.();
|
|
@@ -449,14 +412,11 @@ export class CollectionWatchService {
|
|
|
449
412
|
if (this.#disposed || relPaths.length === 0) {
|
|
450
413
|
return;
|
|
451
414
|
}
|
|
452
|
-
|
|
453
|
-
this.#lastSyncAt = new Date().toISOString();
|
|
415
|
+
this.#lastSyncAt = new Date(this.#clock()).toISOString();
|
|
454
416
|
this.#scheduler?.notifySyncComplete(relPaths);
|
|
455
|
-
|
|
456
417
|
if (!this.#eventBus) {
|
|
457
418
|
return;
|
|
458
419
|
}
|
|
459
|
-
|
|
460
420
|
for (const relPath of relPaths) {
|
|
461
421
|
const event: DocumentEvent = {
|
|
462
422
|
type: "document-changed",
|
|
@@ -464,7 +424,7 @@ export class CollectionWatchService {
|
|
|
464
424
|
collection: collection.name,
|
|
465
425
|
relPath,
|
|
466
426
|
origin: "watcher",
|
|
467
|
-
changedAt: new Date().toISOString(),
|
|
427
|
+
changedAt: new Date(this.#clock()).toISOString(),
|
|
468
428
|
};
|
|
469
429
|
this.#eventBus.emit(event);
|
|
470
430
|
}
|