@mulmoclaude/core 0.25.0 → 0.26.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/billing-clients-worklog.md +3 -2
- package/assets/helps/collection-skills.md +54 -1
- package/assets/helps/custom-view.md +37 -3
- package/assets/helps/error-recovery.md +30 -0
- package/assets/helps/todo-collection.md +2 -2
- package/dist/collection/core/fieldText.d.ts +11 -0
- package/dist/collection/core/project.d.ts +3 -0
- package/dist/collection/core/schema.d.ts +13 -1
- package/dist/collection/core/schemaZ.d.ts +16 -0
- package/dist/collection/index.cjs +18 -10
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +15 -11
- 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/csvStore.d.ts +8 -1
- package/dist/collection/server/discoveredCollection.d.ts +4 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/index.cjs +20 -17
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/queryRunner.d.ts +4 -0
- package/dist/collection/server/spawn.d.ts +2 -1
- package/dist/collection/server/sqliteStore.d.ts +14 -0
- package/dist/collection/server/store.d.ts +43 -8
- package/dist/collection/server/storePage.d.ts +32 -0
- package/dist/collection-watchers/index.cjs +167 -43
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.d.ts +1 -1
- package/dist/collection-watchers/index.js +168 -45
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +13 -11
- package/dist/collection-watchers/watcher.d.ts +4 -3
- package/dist/{discovery-3K_Wtkvn.js → discovery-QUi_7aNG.js} +882 -7
- package/dist/discovery-QUi_7aNG.js.map +1 -0
- package/dist/{discovery-BZ2ms1tE.cjs → discovery-cwtfyDxl.cjs} +994 -4
- package/dist/discovery-cwtfyDxl.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +26 -14
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +26 -14
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/google/index.cjs +9 -11
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +9 -11
- package/dist/google/index.js.map +1 -1
- package/dist/{ids-BR4U1dJD.cjs → ids-Dl9Ij63K.cjs} +14 -1
- package/dist/ids-Dl9Ij63K.cjs.map +1 -0
- package/dist/{ids-D4e10971.js → ids-P2T89Gou.js} +9 -2
- package/dist/ids-P2T89Gou.js.map +1 -0
- package/dist/{ingestTypes-BsGOjR2l.js → ingestTypes-C-IQB4nw.js} +2 -2
- package/dist/{ingestTypes-BsGOjR2l.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
- package/dist/{ingestTypes-CygUXsjf.cjs → ingestTypes-DZpAC3N_.cjs} +2 -2
- package/dist/{ingestTypes-CygUXsjf.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
- package/dist/project-BWI5w_BT.cjs +17 -0
- package/dist/project-BWI5w_BT.cjs.map +1 -0
- package/dist/project-bU98ycsy.js +12 -0
- package/dist/project-bU98ycsy.js.map +1 -0
- package/dist/{promptSafety-Boz5qOs-.js → promptSafety-B5KPkaNY.js} +32 -9
- package/dist/promptSafety-B5KPkaNY.js.map +1 -0
- package/dist/{promptSafety-Co4cYQ_Z.cjs → promptSafety-BOc1B3im.cjs} +43 -8
- package/dist/promptSafety-BOc1B3im.cjs.map +1 -0
- package/dist/remote-view/index.cjs +5 -3
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +3 -1
- package/dist/remote-view/index.js +5 -3
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-DLRz-U7F.cjs → server-B7mtx4Cd.cjs} +302 -785
- package/dist/server-B7mtx4Cd.cjs.map +1 -0
- package/dist/{server-DZH7OjOD.js → server-CNLZZONa.js} +292 -696
- package/dist/server-CNLZZONa.js.map +1 -0
- package/package.json +1 -1
- package/dist/discovery-3K_Wtkvn.js.map +0 -1
- package/dist/discovery-BZ2ms1tE.cjs.map +0 -1
- package/dist/ids-BR4U1dJD.cjs.map +0 -1
- package/dist/ids-D4e10971.js.map +0 -1
- package/dist/promptSafety-Boz5qOs-.js.map +0 -1
- package/dist/promptSafety-Co4cYQ_Z.cjs.map +0 -1
- package/dist/server-DLRz-U7F.cjs.map +0 -1
- package/dist/server-DZH7OjOD.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { configureCollectionWatchers, resetCollectionWatchersConfig, type CollectionNotificationAdapter, type CollectionWatcherLogger, type CompletionPriority, } from './config.js';
|
|
2
2
|
export { reconcileItem, reconcileAllItems, sweepStaleActiveEntries, clearItemNotification, resolveDisplayLabel, itemIsDone, _resetReconcilerLocksForTesting, } from './reconciler.js';
|
|
3
3
|
export { evalNow } from './clock.js';
|
|
4
|
-
export { startCollectionWatchers, stopCollectionWatchers, _syncWatchersForTesting, _tickTimeTriggersForTesting, _scheduleItemReconcileForTesting, type CollectionWatcherOptions, } from './watcher.js';
|
|
4
|
+
export { startCollectionWatchers, stopCollectionWatchers, _syncWatchersForTesting, _tickTimeTriggersForTesting, _scheduleItemReconcileForTesting, _scheduleStorageReconcileForTesting, type CollectionWatcherOptions, } from './watcher.js';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { S as itemIsDone, j as whenMatches } from "../promptSafety-
|
|
1
|
+
import { M as fieldText, S as itemIsDone, j as whenMatches } from "../promptSafety-B5KPkaNY.js";
|
|
2
2
|
import "../collection/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-
|
|
3
|
+
import { W as itemFilePath, n as discoverCollections, r as loadCollection, tt as publishCollectionChange, u as storeFor } from "../discovery-QUi_7aNG.js";
|
|
4
|
+
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-CNLZZONa.js";
|
|
5
5
|
import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { watch } from "node:fs";
|
|
8
|
-
import { mkdir } from "node:fs/promises";
|
|
8
|
+
import { access, mkdir } from "node:fs/promises";
|
|
9
9
|
//#region src/collection-watchers/config.ts
|
|
10
10
|
var NOOP_LOG = {
|
|
11
11
|
info: () => {},
|
|
@@ -73,9 +73,7 @@ function parseCompletionLegacyId(legacyId) {
|
|
|
73
73
|
function resolveDisplayLabel(schema, item, itemId) {
|
|
74
74
|
const { displayField } = schema;
|
|
75
75
|
if (!displayField) return itemId;
|
|
76
|
-
const
|
|
77
|
-
if (raw === void 0 || raw === null) return itemId;
|
|
78
|
-
const label = String(raw).trim();
|
|
76
|
+
const label = fieldText(item[displayField]).trim();
|
|
79
77
|
return label.length > 0 ? label : itemId;
|
|
80
78
|
}
|
|
81
79
|
/** Every active bell entry whose key matches this (slug, itemId).
|
|
@@ -182,24 +180,26 @@ async function clearItemNotification(slug, itemId) {
|
|
|
182
180
|
});
|
|
183
181
|
}
|
|
184
182
|
}
|
|
185
|
-
/** Reconcile one item to the desired bell state. Re-reads the record
|
|
186
|
-
*
|
|
187
|
-
*
|
|
183
|
+
/** Reconcile one item to the desired bell state. Re-reads the record
|
|
184
|
+
* through the collection's STORE (file, sqlite, …) so the decision is
|
|
185
|
+
* grounded in current truth, not in the event payload. Safe to call when
|
|
186
|
+
* the record is missing (delete path).
|
|
188
187
|
*
|
|
189
|
-
* `ioOpts` flows into
|
|
188
|
+
* `ioOpts` flows into the store's workspace-containment checks —
|
|
190
189
|
* production callers (the watcher) pass nothing; tests pass
|
|
191
190
|
* `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */
|
|
192
|
-
async function reconcileItem(
|
|
191
|
+
async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
|
|
192
|
+
const { slug, schema } = collection;
|
|
193
193
|
if (!schema.completionField) {
|
|
194
194
|
await clearItemNotification(slug, itemId);
|
|
195
195
|
return;
|
|
196
196
|
}
|
|
197
|
-
const item = await
|
|
197
|
+
const item = await storeFor(collection, ioOpts).read(itemId);
|
|
198
198
|
if (item === null) {
|
|
199
199
|
await clearItemNotification(slug, itemId);
|
|
200
200
|
return;
|
|
201
201
|
}
|
|
202
|
-
await maybeSpawnSuccessor(
|
|
202
|
+
await maybeSpawnSuccessor(collection, item, itemId, ioOpts);
|
|
203
203
|
if (itemIsDone(schema, item)) {
|
|
204
204
|
await clearItemNotification(slug, itemId);
|
|
205
205
|
return;
|
|
@@ -223,19 +223,20 @@ async function reconcileItem(slug, schema, dataDir, itemId, ioOpts = {}, now = e
|
|
|
223
223
|
}
|
|
224
224
|
await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));
|
|
225
225
|
}
|
|
226
|
-
/** Boot-time reconcile: walk every record
|
|
227
|
-
* reconcile it. Catches up changes that happened while the
|
|
228
|
-
* down. Deleted items are covered by
|
|
229
|
-
* function (it only sees
|
|
230
|
-
|
|
226
|
+
/** Boot-time reconcile: walk every record of the collection once (through
|
|
227
|
+
* its store) and reconcile it. Catches up changes that happened while the
|
|
228
|
+
* server was down. Deleted items are covered by
|
|
229
|
+
* `sweepStaleActiveEntries`, not this function (it only sees records
|
|
230
|
+
* that exist). */
|
|
231
|
+
async function reconcileAllItems(collection, ioOpts = {}, now = evalNow()) {
|
|
232
|
+
const { slug, schema } = collection;
|
|
231
233
|
if (!schema.completionField) return;
|
|
232
234
|
let items;
|
|
233
235
|
try {
|
|
234
|
-
items = await
|
|
236
|
+
items = await storeFor(collection, ioOpts).list();
|
|
235
237
|
} catch (err) {
|
|
236
238
|
log().warn("reconcile list failed", {
|
|
237
239
|
slug,
|
|
238
|
-
dataDir,
|
|
239
240
|
error: errMsg(err)
|
|
240
241
|
});
|
|
241
242
|
return;
|
|
@@ -244,7 +245,7 @@ async function reconcileAllItems(slug, schema, dataDir, ioOpts = {}, now = evalN
|
|
|
244
245
|
for (const item of items) {
|
|
245
246
|
const raw = item[primaryKey];
|
|
246
247
|
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
247
|
-
await reconcileItem(
|
|
248
|
+
await reconcileItem(collection, raw, ioOpts, now);
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
/** Boot-time sweep over the active bell: drop any entries whose underlying
|
|
@@ -272,7 +273,7 @@ async function sweepStaleActiveEntries(opts = {}) {
|
|
|
272
273
|
await clear(entry.id);
|
|
273
274
|
continue;
|
|
274
275
|
}
|
|
275
|
-
const item = await
|
|
276
|
+
const item = await storeFor(collection, opts).read(itemId);
|
|
276
277
|
if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) await clear(entry.id);
|
|
277
278
|
} catch (err) {
|
|
278
279
|
log().warn("sweep entry failed", {
|
|
@@ -304,6 +305,10 @@ var started = false;
|
|
|
304
305
|
* without threading through every signature. */
|
|
305
306
|
var discoveryOpts = {};
|
|
306
307
|
var itemSlots = /* @__PURE__ */ new Map();
|
|
308
|
+
/** Per-slug single-flight for a storage (db-file) collection's full
|
|
309
|
+
* reconcile pass — a burst of db writes collapses to one pass + one
|
|
310
|
+
* trailing re-run, mirroring the per-item slots of the file watcher. */
|
|
311
|
+
var storageSlots = /* @__PURE__ */ new Map();
|
|
307
312
|
/** Trailing debounce per dataSource collection: an atomic file replace
|
|
308
313
|
* (Excel save, editor rename) surfaces as 2-3 fs events — collapse them
|
|
309
314
|
* into one change publish so live views refetch once. */
|
|
@@ -359,6 +364,7 @@ async function stopCollectionWatchers() {
|
|
|
359
364
|
} catch {}
|
|
360
365
|
watchers.clear();
|
|
361
366
|
itemSlots.clear();
|
|
367
|
+
storageSlots.clear();
|
|
362
368
|
for (const timer of dataSourceTimers.values()) clearTimeout(timer);
|
|
363
369
|
dataSourceTimers.clear();
|
|
364
370
|
discoveryOpts = {};
|
|
@@ -392,7 +398,7 @@ async function tickTimeTriggers(now = evalNow()) {
|
|
|
392
398
|
}
|
|
393
399
|
if (schema.dataSource !== void 0) continue;
|
|
394
400
|
if (!schema.triggerField && !schema.spawn) continue;
|
|
395
|
-
await reconcileAllItems(entry.
|
|
401
|
+
await reconcileAllItems(entry.collection, discoveryOpts, now);
|
|
396
402
|
}
|
|
397
403
|
}
|
|
398
404
|
/** Reconcile the watcher set against the currently-discovered
|
|
@@ -438,7 +444,7 @@ function storagePathChanged(previousJson, next) {
|
|
|
438
444
|
} catch {
|
|
439
445
|
return true;
|
|
440
446
|
}
|
|
441
|
-
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath;
|
|
447
|
+
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.path !== next.storage?.path;
|
|
442
448
|
}
|
|
443
449
|
/** Re-reconcile already-watched collections whose schema changed since
|
|
444
450
|
* the last tick. New collections fall through to `startNewWatchers`. */
|
|
@@ -463,6 +469,7 @@ async function reconcileChangedSchemas(collections) {
|
|
|
463
469
|
continue;
|
|
464
470
|
}
|
|
465
471
|
existing.schemaJson = nextJson;
|
|
472
|
+
existing.collection = collection;
|
|
466
473
|
if (collection.schema.dataSource !== void 0) {
|
|
467
474
|
log().info("dataSource watcher schema changed, publishing", { slug: collection.slug });
|
|
468
475
|
publishCollectionChange({
|
|
@@ -473,7 +480,7 @@ async function reconcileChangedSchemas(collections) {
|
|
|
473
480
|
continue;
|
|
474
481
|
}
|
|
475
482
|
log().info("watcher schema changed, re-reconciling", { slug: collection.slug });
|
|
476
|
-
await reconcileAllItems(collection
|
|
483
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
477
484
|
mutated = true;
|
|
478
485
|
}
|
|
479
486
|
return mutated;
|
|
@@ -483,7 +490,8 @@ async function startNewWatchers(collections) {
|
|
|
483
490
|
for (const collection of collections) {
|
|
484
491
|
if (watchers.has(collection.slug)) continue;
|
|
485
492
|
if (collection.schema.dataSource !== void 0) await startDataSourceWatcher(collection);
|
|
486
|
-
else
|
|
493
|
+
else if (collection.schema.storage !== void 0) await startStorageWatcher(collection);
|
|
494
|
+
else await startWatcherFor(collection);
|
|
487
495
|
mutated = true;
|
|
488
496
|
}
|
|
489
497
|
return mutated;
|
|
@@ -529,7 +537,8 @@ async function startDataSourceWatcher(collection) {
|
|
|
529
537
|
slug: collection.slug,
|
|
530
538
|
dataDir: dir,
|
|
531
539
|
watcher,
|
|
532
|
-
schemaJson: JSON.stringify(collection.schema)
|
|
540
|
+
schemaJson: JSON.stringify(collection.schema),
|
|
541
|
+
collection
|
|
533
542
|
});
|
|
534
543
|
log().info("dataSource watcher started", {
|
|
535
544
|
slug: collection.slug,
|
|
@@ -542,10 +551,76 @@ async function startDataSourceWatcher(collection) {
|
|
|
542
551
|
});
|
|
543
552
|
}
|
|
544
553
|
}
|
|
545
|
-
|
|
554
|
+
/** Watch a `storage` collection's database file. One db file holds every
|
|
555
|
+
* record, so an event can't name WHICH record changed — each (debounced)
|
|
556
|
+
* event runs a full `reconcileAllItems` pass (bells / spawn) plus a
|
|
557
|
+
* change publish so views refetch after EXTERNAL edits too (host writes
|
|
558
|
+
* already publish their own change events; the extra ping is debounced
|
|
559
|
+
* and idempotent). Mounts on the parent dir, same as the dataSource
|
|
560
|
+
* watcher, so an atomic replace can't strand the watch. */
|
|
561
|
+
async function startStorageWatcher(collection) {
|
|
562
|
+
const file = collection.storageFile;
|
|
563
|
+
if (file === void 0) return;
|
|
564
|
+
const dir = path.dirname(file);
|
|
565
|
+
const base = path.basename(file);
|
|
566
|
+
try {
|
|
567
|
+
await mkdir(dir, { recursive: true });
|
|
568
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
569
|
+
const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {
|
|
570
|
+
const filename = rawFilename === null ? null : String(rawFilename);
|
|
571
|
+
if (filename !== null && !filename.startsWith(base)) return;
|
|
572
|
+
scheduleStorageReconcile(collection.slug).catch((err) => {
|
|
573
|
+
log().warn("storage watcher reconcile failed", {
|
|
574
|
+
slug: collection.slug,
|
|
575
|
+
error: errMsg(err)
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
watcher.on("error", (err) => {
|
|
580
|
+
log().warn("storage watcher error", {
|
|
581
|
+
slug: collection.slug,
|
|
582
|
+
error: errMsg(err)
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
watchers.set(collection.slug, {
|
|
586
|
+
slug: collection.slug,
|
|
587
|
+
dataDir: dir,
|
|
588
|
+
watcher,
|
|
589
|
+
schemaJson: JSON.stringify(collection.schema),
|
|
590
|
+
collection
|
|
591
|
+
});
|
|
592
|
+
log().info("storage watcher started", {
|
|
593
|
+
slug: collection.slug,
|
|
594
|
+
file
|
|
595
|
+
});
|
|
596
|
+
} catch (err) {
|
|
597
|
+
log().warn("storage watcher start failed", {
|
|
598
|
+
slug: collection.slug,
|
|
599
|
+
error: errMsg(err)
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function scheduleStorageReconcile(slug) {
|
|
604
|
+
return runSingleFlight(storageSlots, slug, async () => {
|
|
605
|
+
const collection = await loadCollection(slug, discoveryOpts);
|
|
606
|
+
if (!collection) return;
|
|
607
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
608
|
+
await sweepStaleActiveEntries(discoveryOpts);
|
|
609
|
+
publishCollectionChange({
|
|
610
|
+
slug,
|
|
611
|
+
op: "upsert"
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/** Test-only: drive one storage-collection reconcile pass directly. */
|
|
616
|
+
function _scheduleStorageReconcileForTesting(slug) {
|
|
617
|
+
return scheduleStorageReconcile(slug);
|
|
618
|
+
}
|
|
619
|
+
async function startWatcherFor(collection) {
|
|
620
|
+
const { slug, schema, dataDir } = collection;
|
|
546
621
|
try {
|
|
547
622
|
await mkdir(dataDir, { recursive: true });
|
|
548
|
-
await reconcileAllItems(
|
|
623
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
549
624
|
const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {
|
|
550
625
|
onEvent(slug, filename).catch((err) => {
|
|
551
626
|
log().warn("watcher event failed", {
|
|
@@ -565,7 +640,8 @@ async function startWatcherFor(slug, schema, dataDir) {
|
|
|
565
640
|
slug,
|
|
566
641
|
dataDir,
|
|
567
642
|
watcher,
|
|
568
|
-
schemaJson: JSON.stringify(schema)
|
|
643
|
+
schemaJson: JSON.stringify(schema),
|
|
644
|
+
collection
|
|
569
645
|
});
|
|
570
646
|
log().info("watcher started", {
|
|
571
647
|
slug,
|
|
@@ -587,12 +663,22 @@ async function startWatcherFor(slug, schema, dataDir) {
|
|
|
587
663
|
* and return — the running reconcile re-runs once after it completes.
|
|
588
664
|
* This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as
|
|
589
665
|
* 2-3 events) into a single reconcile + one trailing re-run. */
|
|
590
|
-
function _scheduleItemReconcileForTesting(
|
|
591
|
-
return scheduleItemReconcile(
|
|
592
|
-
}
|
|
593
|
-
function scheduleItemReconcile(
|
|
594
|
-
|
|
595
|
-
|
|
666
|
+
function _scheduleItemReconcileForTesting(collection, itemId) {
|
|
667
|
+
return scheduleItemReconcile(collection, itemId);
|
|
668
|
+
}
|
|
669
|
+
function scheduleItemReconcile(collection, itemId) {
|
|
670
|
+
return runSingleFlight(itemSlots, `${collection.slug}\x00${itemId}`, () => reconcileItem(collection, itemId, discoveryOpts), () => publishItemChange(collection, itemId));
|
|
671
|
+
}
|
|
672
|
+
/** The shared single-flight loop behind both schedulers. Re-runs `pass`
|
|
673
|
+
* while events keep arriving — the trailing re-run captures any state
|
|
674
|
+
* change that landed during a prior pass. After each pass we read
|
|
675
|
+
* `pending` and zero it before the next iteration, so an event that
|
|
676
|
+
* fires *during* the last pass's await still triggers one more pass
|
|
677
|
+
* before the slot is freed. `onSettled`, if given, runs once in the
|
|
678
|
+
* `finally` after the slot is freed — the item scheduler uses it to
|
|
679
|
+
* publish exactly one live-refresh event per burst. */
|
|
680
|
+
function runSingleFlight(slots, key, pass, onSettled) {
|
|
681
|
+
const existing = slots.get(key);
|
|
596
682
|
if (existing) {
|
|
597
683
|
existing.pending = true;
|
|
598
684
|
return existing.running;
|
|
@@ -606,16 +692,50 @@ function scheduleItemReconcile(slug, schema, dataDir, itemId) {
|
|
|
606
692
|
let keepGoing = true;
|
|
607
693
|
while (keepGoing) {
|
|
608
694
|
slot.pending = false;
|
|
609
|
-
await
|
|
695
|
+
await pass();
|
|
610
696
|
keepGoing = slot.pending;
|
|
611
697
|
}
|
|
612
698
|
} finally {
|
|
613
|
-
|
|
699
|
+
slots.delete(key);
|
|
700
|
+
if (onSettled) await onSettled();
|
|
614
701
|
}
|
|
615
702
|
})();
|
|
616
|
-
|
|
703
|
+
slots.set(key, slot);
|
|
617
704
|
return slot.running;
|
|
618
705
|
}
|
|
706
|
+
/** Emit the live-refresh event for one record. `op` is derived from
|
|
707
|
+
* whether the file is still there — `fs.watch` reports neither the kind
|
|
708
|
+
* of change nor, reliably, which of `rename`/`change` means what. Only
|
|
709
|
+
* file-backed collections reach here; a `storage` (db) collection has no
|
|
710
|
+
* per-record file and publishes wholesale in `scheduleStorageReconcile`. */
|
|
711
|
+
async function publishItemChange(collection, itemId) {
|
|
712
|
+
const changeOp = await itemFileExists(collection.dataDir, itemId) ? "upsert" : "delete";
|
|
713
|
+
safePublish({
|
|
714
|
+
slug: collection.slug,
|
|
715
|
+
ids: [itemId],
|
|
716
|
+
op: changeOp
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
async function itemFileExists(dataDir, itemId) {
|
|
720
|
+
try {
|
|
721
|
+
await access(itemFilePath(dataDir, itemId));
|
|
722
|
+
return true;
|
|
723
|
+
} catch {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** The publisher is host-supplied, so treat it as untrusted: a throw here
|
|
728
|
+
* runs inside a `finally` and would mask the reconcile's own error. */
|
|
729
|
+
function safePublish(payload) {
|
|
730
|
+
try {
|
|
731
|
+
publishCollectionChange(payload);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
log().warn("collection change publish failed", {
|
|
734
|
+
slug: payload.slug,
|
|
735
|
+
error: errMsg(err)
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
619
739
|
/** Handle a single fs.watch event. Re-loads the collection (schema may
|
|
620
740
|
* have changed since startup), filters out non-record files, and
|
|
621
741
|
* forwards to the single-flighted reconciler. `filename === null` (rare,
|
|
@@ -624,16 +744,19 @@ async function onEvent(slug, filename) {
|
|
|
624
744
|
const collection = await loadCollection(slug, discoveryOpts);
|
|
625
745
|
if (!collection) return;
|
|
626
746
|
if (filename === null) {
|
|
627
|
-
|
|
628
|
-
|
|
747
|
+
try {
|
|
748
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
749
|
+
await sweepStaleActiveEntries(discoveryOpts);
|
|
750
|
+
} finally {
|
|
751
|
+
safePublish({ slug });
|
|
752
|
+
}
|
|
629
753
|
return;
|
|
630
754
|
}
|
|
631
755
|
const name = typeof filename === "string" ? filename : filename.toString("utf-8");
|
|
632
756
|
if (!name.endsWith(".json") || name.startsWith(".")) return;
|
|
633
|
-
|
|
634
|
-
await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);
|
|
757
|
+
await scheduleItemReconcile(collection, name.slice(0, -5));
|
|
635
758
|
}
|
|
636
759
|
//#endregion
|
|
637
|
-
export { _resetReconcilerLocksForTesting, _scheduleItemReconcileForTesting, _syncWatchersForTesting, _tickTimeTriggersForTesting, clearItemNotification, configureCollectionWatchers, evalNow, itemIsDone, reconcileAllItems, reconcileItem, resetCollectionWatchersConfig, resolveDisplayLabel, startCollectionWatchers, stopCollectionWatchers, sweepStaleActiveEntries };
|
|
760
|
+
export { _resetReconcilerLocksForTesting, _scheduleItemReconcileForTesting, _scheduleStorageReconcileForTesting, _syncWatchersForTesting, _tickTimeTriggersForTesting, clearItemNotification, configureCollectionWatchers, evalNow, itemIsDone, reconcileAllItems, reconcileItem, resetCollectionWatchersConfig, resolveDisplayLabel, startCollectionWatchers, stopCollectionWatchers, sweepStaleActiveEntries };
|
|
638
761
|
|
|
639
762
|
//# sourceMappingURL=index.js.map
|