@mulmoclaude/core 0.27.0 → 0.29.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/assets/helps/collection-skills.md +4 -2
- package/assets/helps/wiki.md +47 -2
- package/dist/collection/core/ontologyGraph.d.ts +6 -2
- package/dist/collection/index.cjs +4 -4
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +4 -4
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/backendAvailability.d.ts +5 -0
- package/dist/collection/server/csvStore.d.ts +0 -5
- package/dist/collection/server/index.cjs +4 -2
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +3 -3
- package/dist/collection/server/manageTool.d.ts +4 -0
- package/dist/collection/server/schemaDocs.d.ts +21 -0
- package/dist/collection/server/store.d.ts +37 -0
- package/dist/collection/server/watchFs.d.ts +17 -0
- package/dist/collection-watchers/index.cjs +114 -203
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.d.ts +1 -1
- package/dist/collection-watchers/index.js +114 -202
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/watcher.d.ts +8 -13
- package/dist/{discovery-B9Vfojyy.js → discovery-BbsJwVEq.js} +128 -15
- package/dist/discovery-BbsJwVEq.js.map +1 -0
- package/dist/{discovery-BxbJy2VN.cjs → discovery-Bklck7Ck.cjs} +137 -12
- package/dist/discovery-Bklck7Ck.cjs.map +1 -0
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/index.cjs +1 -1
- package/dist/google/index.js +1 -1
- package/dist/{promptSafety-0ZKHX-6J.cjs → promptSafety-BFt2g_wn.cjs} +2 -2
- package/dist/promptSafety-BFt2g_wn.cjs.map +1 -0
- package/dist/{promptSafety-RE1SPxBN.js → promptSafety-cZIeiZtB.js} +3 -3
- package/dist/promptSafety-cZIeiZtB.js.map +1 -0
- package/dist/remote-view/index.cjs +8 -0
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +7 -0
- package/dist/remote-view/index.js +8 -1
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-DOGKfUuD.cjs → server-CghbYZFY.cjs} +199 -22
- package/dist/server-CghbYZFY.cjs.map +1 -0
- package/dist/{server-sGNe-msA.js → server-DlG6ydHO.js} +200 -23
- package/dist/server-DlG6ydHO.js.map +1 -0
- package/dist/whisper/client.cjs.map +1 -1
- package/dist/whisper/client.js.map +1 -1
- package/package.json +1 -1
- package/dist/discovery-B9Vfojyy.js.map +0 -1
- package/dist/discovery-BxbJy2VN.cjs.map +0 -1
- package/dist/promptSafety-0ZKHX-6J.cjs.map +0 -1
- package/dist/promptSafety-RE1SPxBN.js.map +0 -1
- package/dist/server-DOGKfUuD.cjs.map +0 -1
- package/dist/server-sGNe-msA.js.map +0 -1
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
|
|
3
2
|
const require_ids = require("../ids-DWmHjm17.cjs");
|
|
4
|
-
const require_promptSafety = require("../promptSafety-
|
|
3
|
+
const require_promptSafety = require("../promptSafety-BFt2g_wn.cjs");
|
|
5
4
|
require("../collection/index.cjs");
|
|
6
|
-
const require_discovery = require("../discovery-
|
|
7
|
-
const require_server = require("../server-
|
|
5
|
+
const require_discovery = require("../discovery-Bklck7Ck.cjs");
|
|
6
|
+
const require_server = require("../server-CghbYZFY.cjs");
|
|
8
7
|
const require_notifier = require("../notifier-bS8IEeLA.cjs");
|
|
9
|
-
let node_path = require("node:path");
|
|
10
|
-
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
11
|
-
let node_fs = require("node:fs");
|
|
12
8
|
let node_fs_promises = require("node:fs/promises");
|
|
13
9
|
//#region src/collection-watchers/config.ts
|
|
14
10
|
var NOOP_LOG = {
|
|
@@ -302,6 +298,15 @@ var watchers = /* @__PURE__ */ new Map();
|
|
|
302
298
|
var rediscoveryTimer = null;
|
|
303
299
|
var triggerTimer = null;
|
|
304
300
|
var started = false;
|
|
301
|
+
/** Guards the clock tick against overlapping itself. Paired with
|
|
302
|
+
* `watcherEpoch`: a teardown while a pass is in flight bumps the epoch, so
|
|
303
|
+
* that pass's `finally` knows it belongs to a dead generation and must not
|
|
304
|
+
* clear a guard the restarted watcher set now owns. `triggerTickInFlight`
|
|
305
|
+
* is what teardown AWAITS — clearing the flag alone would let a restart run
|
|
306
|
+
* a second pass alongside the first. */
|
|
307
|
+
var triggerTickRunning = false;
|
|
308
|
+
var watcherEpoch = 0;
|
|
309
|
+
var triggerTickInFlight = null;
|
|
305
310
|
/** Discovery options threaded into every `discoverCollections` /
|
|
306
311
|
* `loadCollection` / `sweepStaleActiveEntries` call. Production: empty
|
|
307
312
|
* (live workspace). Tests: `{ workspaceRoot, userSkillsDir }` pointing
|
|
@@ -309,15 +314,10 @@ var started = false;
|
|
|
309
314
|
* without threading through every signature. */
|
|
310
315
|
var discoveryOpts = {};
|
|
311
316
|
var itemSlots = /* @__PURE__ */ new Map();
|
|
312
|
-
/** Per-slug single-flight for a
|
|
313
|
-
*
|
|
314
|
-
* trailing re-run, mirroring the per-item slots
|
|
315
|
-
var
|
|
316
|
-
/** Trailing debounce per dataSource collection: an atomic file replace
|
|
317
|
-
* (Excel save, editor rename) surfaces as 2-3 fs events — collapse them
|
|
318
|
-
* into one change publish so live views refetch once. */
|
|
319
|
-
var DATA_SOURCE_DEBOUNCE_MS = 300;
|
|
320
|
-
var dataSourceTimers = /* @__PURE__ */ new Map();
|
|
317
|
+
/** Per-slug single-flight for a COLLECTION-granularity reconcile — a burst
|
|
318
|
+
* of changes the store couldn't attribute to a record collapses into one
|
|
319
|
+
* pass plus one trailing re-run, mirroring the per-item slots. */
|
|
320
|
+
var collectionSlots = /* @__PURE__ */ new Map();
|
|
321
321
|
/** Boot entry point: sweep stale active entries, then mount watchers for
|
|
322
322
|
* every discovered collection and arm the periodic re-discovery poll.
|
|
323
323
|
* Idempotent — a second call is a no-op. */
|
|
@@ -339,8 +339,15 @@ async function startCollectionWatchers(opts = {}) {
|
|
|
339
339
|
const triggerMs = opts.triggerTickIntervalMs === void 0 ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;
|
|
340
340
|
if (triggerMs !== null) {
|
|
341
341
|
triggerTimer = setInterval(() => {
|
|
342
|
-
|
|
342
|
+
if (triggerTickRunning) return;
|
|
343
|
+
triggerTickRunning = true;
|
|
344
|
+
const epoch = watcherEpoch;
|
|
345
|
+
triggerTickInFlight = tickTimeTriggers().catch((err) => {
|
|
343
346
|
log().warn("watcher trigger tick failed", { error: errMsg(err) });
|
|
347
|
+
}).finally(() => {
|
|
348
|
+
if (epoch !== watcherEpoch) return;
|
|
349
|
+
triggerTickRunning = false;
|
|
350
|
+
triggerTickInFlight = null;
|
|
344
351
|
});
|
|
345
352
|
}, triggerMs);
|
|
346
353
|
triggerTimer.unref();
|
|
@@ -363,20 +370,22 @@ async function stopCollectionWatchers() {
|
|
|
363
370
|
clearInterval(triggerTimer);
|
|
364
371
|
triggerTimer = null;
|
|
365
372
|
}
|
|
373
|
+
await triggerTickInFlight;
|
|
374
|
+
triggerTickInFlight = null;
|
|
375
|
+
watcherEpoch += 1;
|
|
376
|
+
triggerTickRunning = false;
|
|
366
377
|
for (const watcher of watchers.values()) try {
|
|
367
|
-
watcher.
|
|
378
|
+
watcher.unsubscribe();
|
|
368
379
|
} catch {}
|
|
369
380
|
watchers.clear();
|
|
370
381
|
itemSlots.clear();
|
|
371
|
-
|
|
372
|
-
for (const timer of dataSourceTimers.values()) clearTimeout(timer);
|
|
373
|
-
dataSourceTimers.clear();
|
|
382
|
+
collectionSlots.clear();
|
|
374
383
|
discoveryOpts = {};
|
|
375
384
|
started = false;
|
|
376
385
|
}
|
|
377
386
|
/** Test-only: manually trigger one rediscovery + reconcile pass. */
|
|
378
387
|
async function _syncWatchersForTesting() {
|
|
379
|
-
|
|
388
|
+
return syncWatchers();
|
|
380
389
|
}
|
|
381
390
|
/** Test-only: drive one wall-clock tick synchronously, with an optional
|
|
382
391
|
* injected clock. */
|
|
@@ -400,7 +409,6 @@ async function tickTimeTriggers(now = evalNow()) {
|
|
|
400
409
|
});
|
|
401
410
|
continue;
|
|
402
411
|
}
|
|
403
|
-
if (schema.dataSource !== void 0) continue;
|
|
404
412
|
if (!schema.triggerField && !schema.spawn) continue;
|
|
405
413
|
await reconcileAllItems(entry.collection, discoveryOpts, now);
|
|
406
414
|
}
|
|
@@ -416,12 +424,14 @@ async function syncWatchers() {
|
|
|
416
424
|
collections = await require_discovery.discoverCollections(discoveryOpts);
|
|
417
425
|
} catch (err) {
|
|
418
426
|
log().warn("watcher discover failed", { error: errMsg(err) });
|
|
419
|
-
return;
|
|
427
|
+
return false;
|
|
420
428
|
}
|
|
421
429
|
const vanishedMutated = stopVanishedWatchers(new Set(collections.map((collection) => collection.slug)));
|
|
422
430
|
const schemaMutated = await reconcileChangedSchemas(collections);
|
|
423
431
|
const addedMutated = await startNewWatchers(collections);
|
|
424
|
-
if (vanishedMutated
|
|
432
|
+
if (!vanishedMutated && !schemaMutated && !addedMutated) return false;
|
|
433
|
+
await sweepStaleActiveEntries(discoveryOpts);
|
|
434
|
+
return true;
|
|
425
435
|
}
|
|
426
436
|
function stopVanishedWatchers(liveSlugs) {
|
|
427
437
|
let mutated = false;
|
|
@@ -429,7 +439,7 @@ function stopVanishedWatchers(liveSlugs) {
|
|
|
429
439
|
if (liveSlugs.has(slug)) continue;
|
|
430
440
|
const watcher = watchers.get(slug);
|
|
431
441
|
if (watcher) try {
|
|
432
|
-
watcher.
|
|
442
|
+
watcher.unsubscribe();
|
|
433
443
|
} catch {}
|
|
434
444
|
watchers.delete(slug);
|
|
435
445
|
mutated = true;
|
|
@@ -462,7 +472,7 @@ async function reconcileChangedSchemas(collections) {
|
|
|
462
472
|
if (storagePathChanged(existing.schemaJson, collection.schema)) {
|
|
463
473
|
log().info("watcher storage path changed, remounting", { slug: collection.slug });
|
|
464
474
|
try {
|
|
465
|
-
existing.
|
|
475
|
+
existing.unsubscribe();
|
|
466
476
|
} catch {}
|
|
467
477
|
watchers.delete(collection.slug);
|
|
468
478
|
if (collection.schema.dataSource !== void 0) require_discovery.publishCollectionChange({
|
|
@@ -474,199 +484,119 @@ async function reconcileChangedSchemas(collections) {
|
|
|
474
484
|
}
|
|
475
485
|
existing.schemaJson = nextJson;
|
|
476
486
|
existing.collection = collection;
|
|
477
|
-
if (collection.schema.dataSource !== void 0) {
|
|
478
|
-
log().info("dataSource watcher schema changed, publishing", { slug: collection.slug });
|
|
479
|
-
require_discovery.publishCollectionChange({
|
|
480
|
-
slug: collection.slug,
|
|
481
|
-
op: "upsert"
|
|
482
|
-
});
|
|
483
|
-
mutated = true;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
487
|
log().info("watcher schema changed, re-reconciling", { slug: collection.slug });
|
|
487
488
|
await reconcileAllItems(collection, discoveryOpts);
|
|
489
|
+
if (collection.schema.dataSource !== void 0) require_discovery.publishCollectionChange({
|
|
490
|
+
slug: collection.slug,
|
|
491
|
+
op: "upsert"
|
|
492
|
+
});
|
|
488
493
|
mutated = true;
|
|
489
494
|
}
|
|
490
495
|
return mutated;
|
|
491
496
|
}
|
|
497
|
+
/** Mount a watcher for every collection that doesn't have one yet. Returns
|
|
498
|
+
* whether the watcher SET actually changed — the starters report whether
|
|
499
|
+
* they mounted, because ATTEMPTING is not mounting. A start that throws
|
|
500
|
+
* (logged and swallowed inside the starter) leaves `watchers` untouched, so
|
|
501
|
+
* the collection is retried next tick; counting that as a mutation made
|
|
502
|
+
* `syncWatchers` sweep on every tick for as long as the failure persisted. */
|
|
492
503
|
async function startNewWatchers(collections) {
|
|
493
504
|
let mutated = false;
|
|
494
505
|
for (const collection of collections) {
|
|
495
506
|
if (watchers.has(collection.slug)) continue;
|
|
496
|
-
if (
|
|
497
|
-
else if (collection.schema.storage !== void 0) await startStorageWatcher(collection);
|
|
498
|
-
else await startWatcherFor(collection);
|
|
499
|
-
mutated = true;
|
|
507
|
+
if (await startWatcherFor(collection)) mutated = true;
|
|
500
508
|
}
|
|
501
509
|
return mutated;
|
|
502
510
|
}
|
|
503
|
-
/**
|
|
504
|
-
*
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}, DATA_SOURCE_DEBOUNCE_MS);
|
|
515
|
-
timer.unref?.();
|
|
516
|
-
dataSourceTimers.set(slug, timer);
|
|
517
|
-
}
|
|
518
|
-
/** Watch a dataSource collection's external data file. The watch mounts
|
|
519
|
-
* on the PARENT directory (watching the file itself goes stale after an
|
|
520
|
-
* atomic replace — the inode changes) and filters events to the file's
|
|
521
|
-
* basename; a null filename (platform quirk) is treated as a hit. No
|
|
522
|
-
* reconciler involvement — replacing the CSV just refreshes the views. */
|
|
523
|
-
async function startDataSourceWatcher(collection) {
|
|
524
|
-
const file = collection.dataSourceFile;
|
|
525
|
-
if (file === void 0) return;
|
|
526
|
-
const dir = node_path.default.dirname(file);
|
|
527
|
-
const base = node_path.default.basename(file);
|
|
528
|
-
try {
|
|
529
|
-
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
530
|
-
const watcher = (0, node_fs.watch)(dir, { persistent: false }, (_eventType, filename) => {
|
|
531
|
-
if (filename !== null && filename !== base) return;
|
|
532
|
-
scheduleDataSourcePublish(collection.slug);
|
|
533
|
-
});
|
|
534
|
-
watcher.on("error", (err) => {
|
|
535
|
-
log().warn("dataSource watcher error", {
|
|
536
|
-
slug: collection.slug,
|
|
537
|
-
error: errMsg(err)
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
watchers.set(collection.slug, {
|
|
541
|
-
slug: collection.slug,
|
|
542
|
-
dataDir: dir,
|
|
543
|
-
watcher,
|
|
544
|
-
schemaJson: JSON.stringify(collection.schema),
|
|
545
|
-
collection
|
|
546
|
-
});
|
|
547
|
-
log().info("dataSource watcher started", {
|
|
548
|
-
slug: collection.slug,
|
|
549
|
-
file
|
|
550
|
-
});
|
|
551
|
-
} catch (err) {
|
|
552
|
-
log().warn("dataSource watcher start failed", {
|
|
553
|
-
slug: collection.slug,
|
|
554
|
-
error: errMsg(err)
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
/** Watch a `storage` collection's database file. One db file holds every
|
|
559
|
-
* record, so an event can't name WHICH record changed — each (debounced)
|
|
560
|
-
* event runs a full `reconcileAllItems` pass (bells / spawn) plus a
|
|
561
|
-
* change publish so views refetch after EXTERNAL edits too (host writes
|
|
562
|
-
* already publish their own change events; the extra ping is debounced
|
|
563
|
-
* and idempotent). Mounts on the parent dir, same as the dataSource
|
|
564
|
-
* watcher, so an atomic replace can't strand the watch. */
|
|
565
|
-
async function startStorageWatcher(collection) {
|
|
566
|
-
const file = collection.storageFile;
|
|
567
|
-
if (file === void 0) return;
|
|
568
|
-
const dir = node_path.default.dirname(file);
|
|
569
|
-
const base = node_path.default.basename(file);
|
|
511
|
+
/** Mount one collection's change subscription, whatever its backend.
|
|
512
|
+
*
|
|
513
|
+
* The store decides how to detect a change and at what granularity; this
|
|
514
|
+
* decides what to do about one. That split is the point: adding a backend
|
|
515
|
+
* means implementing `watch` on its store, not another branch here.
|
|
516
|
+
*
|
|
517
|
+
* A store without `watch` cannot report external changes at all — it is
|
|
518
|
+
* still registered (so bells reconcile at boot and on the clock tick), just
|
|
519
|
+
* without live updates. */
|
|
520
|
+
async function startWatcherFor(collection) {
|
|
521
|
+
const { slug } = collection;
|
|
570
522
|
try {
|
|
571
|
-
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
572
523
|
await reconcileAllItems(collection, discoveryOpts);
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
slug: collection.slug,
|
|
524
|
+
const store = require_discovery.storeFor(collection, discoveryOpts);
|
|
525
|
+
const unsubscribe = store.watch ? await store.watch((change) => {
|
|
526
|
+
handleStoreChange(slug, change).catch((err) => {
|
|
527
|
+
log().warn("store change handling failed", {
|
|
528
|
+
slug,
|
|
579
529
|
error: errMsg(err)
|
|
580
530
|
});
|
|
581
531
|
});
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
log().warn("
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
dataDir: dir,
|
|
592
|
-
watcher,
|
|
532
|
+
}) : () => {};
|
|
533
|
+
if (unsubscribe === null) {
|
|
534
|
+
log().warn("collection watcher could not arm, retrying next sync", { slug });
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
watchers.set(slug, {
|
|
538
|
+
slug,
|
|
539
|
+
dataDir: collection.dataDir,
|
|
540
|
+
unsubscribe,
|
|
593
541
|
schemaJson: JSON.stringify(collection.schema),
|
|
594
542
|
collection
|
|
595
543
|
});
|
|
596
|
-
log().info("
|
|
597
|
-
slug
|
|
598
|
-
|
|
544
|
+
log().info("collection watcher started", {
|
|
545
|
+
slug,
|
|
546
|
+
live: store.watch !== void 0
|
|
599
547
|
});
|
|
548
|
+
return true;
|
|
600
549
|
} catch (err) {
|
|
601
|
-
log().warn("
|
|
602
|
-
slug
|
|
550
|
+
log().warn("collection watcher start failed", {
|
|
551
|
+
slug,
|
|
603
552
|
error: errMsg(err)
|
|
604
553
|
});
|
|
554
|
+
return false;
|
|
605
555
|
}
|
|
606
556
|
}
|
|
607
|
-
|
|
608
|
-
|
|
557
|
+
/** React to one reported change. Backend-agnostic by construction — it sees
|
|
558
|
+
* only the granularity the store reported.
|
|
559
|
+
*
|
|
560
|
+
* `item`: reconcile just that record, then publish it. `collection`: the
|
|
561
|
+
* store couldn't say which record, so re-derive everything and pair it with
|
|
562
|
+
* a sweep — a record deleted remotely leaves a bell that a walk over the
|
|
563
|
+
* SURVIVING records can never clear. */
|
|
564
|
+
async function handleStoreChange(slug, change) {
|
|
565
|
+
if (change.kind === "collection") {
|
|
566
|
+
await scheduleCollectionReconcile(slug);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const current = watchers.get(slug)?.collection;
|
|
570
|
+
if (!current) return;
|
|
571
|
+
await scheduleItemReconcile(current, change.itemId);
|
|
572
|
+
}
|
|
573
|
+
/** Full re-derivation for a collection-granularity change, single-flighted
|
|
574
|
+
* per slug so a burst of writes collapses into one pass plus one trailing
|
|
575
|
+
* re-run. */
|
|
576
|
+
function scheduleCollectionReconcile(slug) {
|
|
577
|
+
return runSingleFlight(collectionSlots, slug, async () => {
|
|
609
578
|
const collection = await require_discovery.loadCollection(slug, discoveryOpts);
|
|
610
579
|
if (!collection) return;
|
|
611
580
|
await reconcileAllItems(collection, discoveryOpts);
|
|
612
581
|
await sweepStaleActiveEntries(discoveryOpts);
|
|
613
|
-
|
|
582
|
+
}, () => {
|
|
583
|
+
safePublish({
|
|
614
584
|
slug,
|
|
615
585
|
op: "upsert"
|
|
616
586
|
});
|
|
587
|
+
return Promise.resolve();
|
|
617
588
|
});
|
|
618
589
|
}
|
|
619
|
-
/** Test-only:
|
|
620
|
-
|
|
621
|
-
|
|
590
|
+
/** Test-only: feed one store-reported change through the same path a live
|
|
591
|
+
* subscription uses, so a test can pin how a change is reacted to without
|
|
592
|
+
* depending on fs.watch timing. */
|
|
593
|
+
function _handleStoreChangeForTesting(slug, change) {
|
|
594
|
+
return handleStoreChange(slug, change);
|
|
622
595
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
await (0, node_fs_promises.mkdir)(dataDir, { recursive: true });
|
|
627
|
-
await reconcileAllItems(collection, discoveryOpts);
|
|
628
|
-
const watcher = (0, node_fs.watch)(dataDir, { persistent: false }, (_eventType, filename) => {
|
|
629
|
-
onEvent(slug, filename).catch((err) => {
|
|
630
|
-
log().warn("watcher event failed", {
|
|
631
|
-
slug,
|
|
632
|
-
filename,
|
|
633
|
-
error: errMsg(err)
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
watcher.on("error", (err) => {
|
|
638
|
-
log().warn("watcher error", {
|
|
639
|
-
slug,
|
|
640
|
-
error: errMsg(err)
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
watchers.set(slug, {
|
|
644
|
-
slug,
|
|
645
|
-
dataDir,
|
|
646
|
-
watcher,
|
|
647
|
-
schemaJson: JSON.stringify(schema),
|
|
648
|
-
collection
|
|
649
|
-
});
|
|
650
|
-
log().info("watcher started", {
|
|
651
|
-
slug,
|
|
652
|
-
dataDir
|
|
653
|
-
});
|
|
654
|
-
} catch (err) {
|
|
655
|
-
log().warn("watcher start failed", {
|
|
656
|
-
slug,
|
|
657
|
-
error: errMsg(err)
|
|
658
|
-
});
|
|
659
|
-
}
|
|
596
|
+
/** Test-only: drive one collection-granularity reconcile directly. */
|
|
597
|
+
function _scheduleCollectionReconcileForTesting(slug) {
|
|
598
|
+
return scheduleCollectionReconcile(slug);
|
|
660
599
|
}
|
|
661
|
-
/** Test-only: the per-key single-flight scheduler. Exported so test code
|
|
662
|
-
* can drive rapid-fire calls directly and observe the trailing coalesce
|
|
663
|
-
* — `fs.watch` event timing is too flaky to assert against.
|
|
664
|
-
*
|
|
665
|
-
* Single-flight semantics: while a reconcile is in flight for a given
|
|
666
|
-
* (slug, itemId), additional events on the same key set `pending = true`
|
|
667
|
-
* and return — the running reconcile re-runs once after it completes.
|
|
668
|
-
* This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as
|
|
669
|
-
* 2-3 events) into a single reconcile + one trailing re-run. */
|
|
670
600
|
function _scheduleItemReconcileForTesting(collection, itemId) {
|
|
671
601
|
return scheduleItemReconcile(collection, itemId);
|
|
672
602
|
}
|
|
@@ -740,30 +670,11 @@ function safePublish(payload) {
|
|
|
740
670
|
});
|
|
741
671
|
}
|
|
742
672
|
}
|
|
743
|
-
/** Handle a single fs.watch event. Re-loads the collection (schema may
|
|
744
|
-
* have changed since startup), filters out non-record files, and
|
|
745
|
-
* forwards to the single-flighted reconciler. `filename === null` (rare,
|
|
746
|
-
* platform-specific) triggers a full directory rescan to be safe. */
|
|
747
|
-
async function onEvent(slug, filename) {
|
|
748
|
-
const collection = await require_discovery.loadCollection(slug, discoveryOpts);
|
|
749
|
-
if (!collection) return;
|
|
750
|
-
if (filename === null) {
|
|
751
|
-
try {
|
|
752
|
-
await reconcileAllItems(collection, discoveryOpts);
|
|
753
|
-
await sweepStaleActiveEntries(discoveryOpts);
|
|
754
|
-
} finally {
|
|
755
|
-
safePublish({ slug });
|
|
756
|
-
}
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
const name = typeof filename === "string" ? filename : filename.toString("utf-8");
|
|
760
|
-
if (!name.endsWith(".json") || name.startsWith(".")) return;
|
|
761
|
-
await scheduleItemReconcile(collection, name.slice(0, -5));
|
|
762
|
-
}
|
|
763
673
|
//#endregion
|
|
674
|
+
exports._handleStoreChangeForTesting = _handleStoreChangeForTesting;
|
|
764
675
|
exports._resetReconcilerLocksForTesting = _resetReconcilerLocksForTesting;
|
|
676
|
+
exports._scheduleCollectionReconcileForTesting = _scheduleCollectionReconcileForTesting;
|
|
765
677
|
exports._scheduleItemReconcileForTesting = _scheduleItemReconcileForTesting;
|
|
766
|
-
exports._scheduleStorageReconcileForTesting = _scheduleStorageReconcileForTesting;
|
|
767
678
|
exports._syncWatchersForTesting = _syncWatchersForTesting;
|
|
768
679
|
exports._tickTimeTriggersForTesting = _tickTimeTriggersForTesting;
|
|
769
680
|
exports.clearItemNotification = clearItemNotification;
|