@mulmoclaude/core 0.28.0 → 0.30.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 +18 -4
- package/dist/collection/core/backlinks.d.ts +28 -2
- package/dist/collection/core/ontologyGraph.d.ts +6 -2
- package/dist/collection/index.cjs +2 -1
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +2 -2
- 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/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-BVdCgBFN.js → discovery-BbsJwVEq.js} +109 -8
- package/dist/discovery-BbsJwVEq.js.map +1 -0
- package/dist/{discovery-BKovdRpZ.cjs → discovery-Bklck7Ck.cjs} +119 -6
- 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-cZIeiZtB.js → promptSafety-Bugq2kqL.js} +40 -5
- package/dist/promptSafety-Bugq2kqL.js.map +1 -0
- package/dist/{promptSafety-BFt2g_wn.cjs → promptSafety-DbE6eZmP.cjs} +45 -4
- package/dist/promptSafety-DbE6eZmP.cjs.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-B8mvfZSF.js → server-8EZggEg7.js} +180 -15
- package/dist/server-8EZggEg7.js.map +1 -0
- package/dist/{server-rRQkNBvO.cjs → server-BOiz_HDi.cjs} +180 -15
- package/dist/server-BOiz_HDi.cjs.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-BKovdRpZ.cjs.map +0 -1
- package/dist/discovery-BVdCgBFN.js.map +0 -1
- package/dist/promptSafety-BFt2g_wn.cjs.map +0 -1
- package/dist/promptSafety-cZIeiZtB.js.map +0 -1
- package/dist/server-B8mvfZSF.js.map +0 -1
- package/dist/server-rRQkNBvO.cjs.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_ids = require("../../ids-DWmHjm17.cjs");
|
|
3
|
-
const require_discovery = require("../../discovery-
|
|
3
|
+
const require_discovery = require("../../discovery-Bklck7Ck.cjs");
|
|
4
4
|
const require_templatePath = require("../../templatePath-27vUZowm.cjs");
|
|
5
|
-
const require_server = require("../../server-
|
|
5
|
+
const require_server = require("../../server-BOiz_HDi.cjs");
|
|
6
|
+
exports.BackendUnavailableError = require_discovery.BackendUnavailableError;
|
|
6
7
|
exports.COMPUTED_TYPES = require_ids.COMPUTED_TYPES;
|
|
7
8
|
exports.CollectionQueryZ = require_discovery.CollectionQueryZ;
|
|
8
9
|
exports.CollectionSchemaZ = require_discovery.CollectionSchemaZ;
|
|
@@ -40,6 +41,7 @@ exports.firstMutateParamProblem = require_server.firstMutateParamProblem;
|
|
|
40
41
|
exports.formatCivil = require_server.formatCivil;
|
|
41
42
|
exports.generateItemId = require_discovery.generateItemId;
|
|
42
43
|
exports.getWorkspaceRoot = require_discovery.getWorkspaceRoot;
|
|
44
|
+
exports.isBackendUnavailable = require_discovery.isBackendUnavailable;
|
|
43
45
|
exports.isContainedInRoot = require_discovery.isContainedInRoot;
|
|
44
46
|
exports.isContainedInWorkspace = require_discovery.isContainedInWorkspace;
|
|
45
47
|
exports.isRegularFile = require_discovery.isRegularFile;
|
|
@@ -4,6 +4,7 @@ export * from './paths';
|
|
|
4
4
|
export * from './templatePath';
|
|
5
5
|
export * from './io';
|
|
6
6
|
export * from './store';
|
|
7
|
+
export { BackendUnavailableError, isBackendUnavailable } from './backendAvailability';
|
|
7
8
|
export { MAX_CSV_ROWS, encodeCsvRecordId, decodeCsvRecordId, normalizeCsvValue, csvRowToItem, dedupeByRecordId } from './csvStore';
|
|
8
9
|
export { compileCsvQuery, compileJsonlQuery } from './csvQuery';
|
|
9
10
|
export { runQueryOverRows } from './jsonlQuery';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as COMPUTED_TYPES } from "../../ids-D1M1T6KJ.js";
|
|
2
|
-
import { A as
|
|
2
|
+
import { $ as configureCollectionHost, A as buildCollectionActionSeedPrompt, B as resolveCreateItemId, C as compileJsonlQuery, D as BackendUnavailableError, E as MAX_QUERY_ROWS, F as promptPathsFor, G as isContainedInWorkspace, I as readCustomViewHtml, J as resolveTemplatePath, K as itemFilePath, L as readCustomViewI18n, M as generateItemId, N as isRegularFile, O as isBackendUnavailable, P as listItems, R as readItem, S as compileCsvQuery, T as DEFAULT_QUERY_ROWS, U as SCHEMA_FILE, V as writeItem, W as isContainedInRoot, X as safeSlugName, Y as safeRecordId, _ as decodeCsvRecordId, a as toSummary, b as normalizeCsvValue, c as collectionWritable, et as getWorkspaceRoot, f as pageFromFullRead, g as csvRowToItem, i as toDetail, it as setCollectionChangePublisher, j as deleteItem, k as buildActionSeedPrompt, l as readOnlyRefusal, m as MAX_CSV_ROWS, n as discoverCollections, nt as log, o as CollectionSchemaZ, p as projectItemFields, q as resolveDataDir, r as loadCollection, rt as publishCollectionChange, t as acceptParsedSchema, u as storeFor, v as dedupeByRecordId, w as CollectionQueryZ, y as encodeCsvRecordId, z as readSkillTemplate } from "../../discovery-BbsJwVEq.js";
|
|
3
3
|
import { i as isSafeTemplatePath, n as isSafeCustomViewI18nPath, r as isSafeCustomViewPath, t as isSafeActionTemplatePath } from "../../templatePath-k_WNbL_Q.js";
|
|
4
|
-
import { C as validateRecordObject, D as enrichItems, E as runCollectionQuery, O as runQueryOverRows, S as validateCollectionRecords, T as recordFieldProblem, _ as computeCollectionIcon, a as deleteCollection, b as applyMutateAction, c as computeSuccessor, d as isTriggerDue, f as maybeSpawnSuccessor, h as successorId, i as deleteCustomView, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as compileRecordZ, x as firstMutateParamProblem, y as schemaRelations } from "../../server-
|
|
5
|
-
export { COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseCivil, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
|
|
4
|
+
import { C as validateRecordObject, D as enrichItems, E as runCollectionQuery, O as runQueryOverRows, S as validateCollectionRecords, T as recordFieldProblem, _ as computeCollectionIcon, a as deleteCollection, b as applyMutateAction, c as computeSuccessor, d as isTriggerDue, f as maybeSpawnSuccessor, h as successorId, i as deleteCustomView, l as daysInMonth, m as resolveEvery, n as MAX_UNSELECTIVE_ITEMS, o as deleteCollectionRefusalMessage, p as parseCivil, r as makeManageCollectionTool, s as advanceTriggerDate, t as MAX_SCHEMA_ISSUES, u as formatCivil, v as buildWorkspaceOntology, w as compileRecordZ, x as firstMutateParamProblem, y as schemaRelations } from "../../server-8EZggEg7.js";
|
|
5
|
+
export { BackendUnavailableError, COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isBackendUnavailable, isContainedInRoot, isContainedInWorkspace, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, pageFromFullRead, parseCivil, projectItemFields, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, runCollectionQuery, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A doc at/below this size is returned whole — sectioning something the
|
|
2
|
+
* agent can read in one gulp only costs round-trips. This is also what
|
|
3
|
+
* keeps short user-authored workspace copies (config/helps) verbatim. */
|
|
4
|
+
export declare const SCHEMA_DOCS_VERBATIM_LIMIT = 20000;
|
|
5
|
+
/** Ceiling for ANY assembled reply — topic or default — safely inside
|
|
6
|
+
* the agent's per-result limit (the full doc is what overflowed it).
|
|
7
|
+
* A matched section too large to fit degrades to its own prose plus a
|
|
8
|
+
* list of its subsections to fetch individually; an oversized leaf is
|
|
9
|
+
* clipped outright; the default drops whole core sections into a
|
|
10
|
+
* pointer note. Sized so the CURRENT bundled doc's default reply
|
|
11
|
+
* (~33KB) fits without degrading. */
|
|
12
|
+
export declare const SCHEMA_DOCS_RESULT_BUDGET = 36000;
|
|
13
|
+
/** Render the authoring reference for one schemaDocs call. Small docs and
|
|
14
|
+
* docs without headings pass through verbatim — there is nothing better
|
|
15
|
+
* to key a section off. The assembled reply gets one final hard cap: the
|
|
16
|
+
* inner budgeting degrades gracefully, but per-piece overheads (clip
|
|
17
|
+
* markers on thousands of matches, join separators, pointer notes) add
|
|
18
|
+
* up outside any single piece's budget, and the ceiling must hold no
|
|
19
|
+
* matter what shape of document arrives. Only the explicit \`"all"\`
|
|
20
|
+
* opt-in and the small-doc verbatim path may exceed it. */
|
|
21
|
+
export declare function renderSchemaDocs(doc: string, topic?: string): string;
|
|
@@ -30,6 +30,22 @@ export interface CollectionStoreCapabilities {
|
|
|
30
30
|
* (io.ts is the reference).
|
|
31
31
|
* 4. HONEST AGGREGATION — `query`, when present, is computed over the
|
|
32
32
|
* WHOLE data set, never from a capped read. */
|
|
33
|
+
/** What a store reports when its records changed underneath it.
|
|
34
|
+
*
|
|
35
|
+
* Two granularities, because backends genuinely differ: a per-record file
|
|
36
|
+
* store knows WHICH record changed, while a single-artifact backend (one
|
|
37
|
+
* CSV, one db file, one remote snapshot) only knows THAT something did.
|
|
38
|
+
* Naming the difference here is what lets the watcher react uniformly
|
|
39
|
+
* instead of branching on the backend. */
|
|
40
|
+
export type StoreChange = {
|
|
41
|
+
readonly kind: "item";
|
|
42
|
+
readonly itemId: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: "collection";
|
|
45
|
+
};
|
|
46
|
+
export type StoreChangeListener = (change: StoreChange) => void;
|
|
47
|
+
/** Detaches a `watch` subscription. */
|
|
48
|
+
export type StoreUnsubscribe = () => void;
|
|
33
49
|
export interface CollectionStore {
|
|
34
50
|
readonly capabilities: CollectionStoreCapabilities;
|
|
35
51
|
/** Every record, in the store's stable order. CSV store: capped at
|
|
@@ -50,6 +66,27 @@ export interface CollectionStore {
|
|
|
50
66
|
* collection's slug into the publish hook, so no writer can forget it. */
|
|
51
67
|
write?: (itemId: string, item: CollectionItem, opts?: WriteOptions) => Promise<WriteItemResult>;
|
|
52
68
|
delete?: (itemId: string) => Promise<DeleteItemResult>;
|
|
69
|
+
/** Subscribe to changes made OUTSIDE this process — a file edited by the
|
|
70
|
+
* agent, a CSV replaced by the user, a db written by another tool.
|
|
71
|
+
*
|
|
72
|
+
* Resolves to an unsubscribe function, or to `null` when the backend
|
|
73
|
+
* could not arm the watch (no inotify watches left, an unreadable
|
|
74
|
+
* directory). `null` is NOT the same as absent `watch`: absent means the
|
|
75
|
+
* backend never reports changes and the caller should settle for its
|
|
76
|
+
* periodic pass, while `null` means this attempt failed and mounting
|
|
77
|
+
* should be retried. Swallowing the difference strands a collection on
|
|
78
|
+
* stale data until the process restarts.
|
|
79
|
+
*
|
|
80
|
+
* This is the store's job because only it knows where its bytes live and
|
|
81
|
+
* what its own change events look like (which paths to watch, which
|
|
82
|
+
* filenames are noise, whether an atomic replace surfaces as two events).
|
|
83
|
+
* Absorbing that noise — debouncing a replace into one report — belongs
|
|
84
|
+
* here too. What to DO about a change (reconcile bells, sweep, publish)
|
|
85
|
+
* is the watcher's policy and is deliberately NOT expressed here.
|
|
86
|
+
*
|
|
87
|
+
* Absent ⇒ the backend cannot report external changes; the caller falls
|
|
88
|
+
* back to its periodic pass. */
|
|
89
|
+
watch?: (onChange: StoreChangeListener) => Promise<StoreUnsubscribe | null>;
|
|
53
90
|
}
|
|
54
91
|
/** True when the collection accepts UI/tool writes. A `dataSource`
|
|
55
92
|
* collection is read-only: updates happen by editing/replacing the
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FsWatchHandle {
|
|
2
|
+
close: () => void;
|
|
3
|
+
}
|
|
4
|
+
/** Watch `dir`, reporting each accepted filename. `accept` decides what is
|
|
5
|
+
* noise; a null filename always passes (the platform didn't tell us which
|
|
6
|
+
* file, so the caller must assume the worst). */
|
|
7
|
+
export declare function watchDirectory(dir: string, accept: (filename: string) => boolean, onHit: (filename: string | null) => void): Promise<FsWatchHandle | null>;
|
|
8
|
+
/** Watch the single file `absPath` by watching its PARENT directory, so an
|
|
9
|
+
* atomic replace can't strand the watch on a dead inode. `alsoAccept`
|
|
10
|
+
* widens the filter beyond the exact basename (sqlite's `-wal`/`-journal`
|
|
11
|
+
* sidecars). Reports are debounced: one replace, one call. */
|
|
12
|
+
export declare function watchSingleFile(absPath: string, alsoAccept: (basename: string, filename: string) => boolean, onChange: () => void): Promise<FsWatchHandle | null>;
|
|
13
|
+
/** An `FsWatchHandle` as a bare unsubscribe — `null` straight through, so an
|
|
14
|
+
* unarmed watch stays distinguishable from an armed one. Lives here rather
|
|
15
|
+
* than beside the store contract so both `store.ts` and the backends it
|
|
16
|
+
* registers can reach it without importing each other. */
|
|
17
|
+
export declare function closerFor(handle: FsWatchHandle | null): (() => void) | null;
|
|
@@ -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-DbE6eZmP.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-BOiz_HDi.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;
|