@objectstack/metadata-fs 17.0.0-rc.6 → 17.1.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/repository.ts","../src/layout.ts","../src/jsonl-log.ts","../src/sync.ts","../src/watch-iterable.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport * from './repository.js';\nexport { JsonlLog } from './jsonl-log.js';\nexport type { FsLayout } from './layout.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `FileSystemRepository` — Node-only implementation of\n * `MetadataRepository` backed by JSON files plus a JSONL change log.\n *\n * See `README.md` for the on-disk layout and ADR-0008 §10 PR-4 for the\n * design rationale.\n *\n * Invariants\n * ──────────\n * - All `put` / `delete` ops serialize per-key via `KeyedMutex`.\n * - The change-log JSONL is the durable source of `seq`. On boot we\n * scan the log to learn the next seq value.\n * - Body files (`<type>/<name>.json`) are the source of truth; the\n * log is a denormalised history index.\n * - chokidar-driven external edits are translated into MetadataEvents\n * by hashing the new content and comparing to the last-known hash.\n * - The root directory is created **on the first write, not on attach**\n * (#7000). Attaching and reading a repository whose root does not exist\n * is legal and answers \"empty\"; see `start()` / `ensureRoot()`.\n */\n\nimport fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\nimport type { FSWatcher } from 'chokidar';\nimport chokidar from 'chokidar';\nimport {\n type MetadataRepository,\n type MetaRef,\n type MetadataItem,\n type MetadataItemHeader,\n type MetadataEvent,\n type PutOptions,\n type PutResult,\n type DeleteOptions,\n type DeleteResult,\n type ListFilter,\n type WatchFilter,\n type HistoryOptions,\n type MetadataType,\n hashSpec,\n ConflictError,\n refKey,\n} from '@objectstack/metadata-core';\nimport {\n type FsLayout,\n itemPath,\n parseItemPath,\n typeDir,\n logFile,\n} from './layout.js';\nimport { JsonlLog } from './jsonl-log.js';\nimport { KeyedMutex, createBroker, type EventBroker } from './sync.js';\nimport { createWatchIterable } from './watch-iterable.js';\n\nexport interface FileSystemRepositoryOptions {\n /** Absolute path to the metadata root directory. */\n root: string;\n /** Tenant/org. */\n org: string;\n /** Identity reported in events that originate from external FS edits. */\n fsActor?: string;\n /** Disable chokidar watcher (e.g. for read-only contexts). */\n disableWatch?: boolean;\n /** Optional clock injection for deterministic tests. */\n now?: () => Date;\n}\n\nconst matchRefFilter = (\n ref: MetaRef,\n filter: { org?: string; type?: MetadataType; name?: string },\n): boolean => {\n if (filter.org && filter.org !== ref.org) return false;\n if (filter.type && filter.type !== ref.type) return false;\n if (filter.name && filter.name !== ref.name) return false;\n return true;\n};\n\nconst matchEvent = (evt: MetadataEvent, filter: WatchFilter): boolean => matchRefFilter(evt.ref, filter);\n\nexport class FileSystemRepository implements MetadataRepository {\n private readonly layout: FsLayout;\n private readonly org: string;\n private readonly fsActor: string;\n private readonly disableWatch: boolean;\n private readonly now: () => Date;\n private readonly log: JsonlLog;\n private readonly mutex = new KeyedMutex();\n private readonly broker: EventBroker = createBroker(matchEvent);\n\n /** In-memory index: refKey → current hash (HEAD). */\n private readonly heads = new Map<string, string>();\n /** Next seq counter, hydrated from the log on `start()`. */\n private nextSeq = 1;\n /** Paths we wrote ourselves; suppress the resulting chokidar event. */\n private readonly selfWrites = new Set<string>();\n private watcher: FSWatcher | null = null;\n private started = false;\n\n constructor(opts: FileSystemRepositoryOptions) {\n this.org = opts.org;\n this.fsActor = opts.fsActor ?? 'fs';\n this.disableWatch = opts.disableWatch ?? false;\n this.now = opts.now ?? (() => new Date());\n this.layout = { root: path.resolve(opts.root) };\n this.log = new JsonlLog(logFile(this.layout));\n }\n\n // ── Lifecycle ───────────────────────────────────────────────────────\n\n /**\n * Attach the repository. **Creates nothing on disk** (#7000).\n *\n * Attaching is not a write. `start()` used to `mkdir` both the root and\n * `<root>/.objectstack/.log` unconditionally, which meant every read-only\n * boot that merely attaches a repository left a skeleton behind — most\n * visibly `os migrate plan`, a declared dry run, on a project that has\n * never been started. That is the same property #6743 ruled on for\n * `.objectstack/data/`: a dry run leaves nothing behind, and the existence\n * of `.objectstack/` has to stay a usable \"this project has been started\"\n * signal.\n *\n * Every read path below already treats a missing root as an empty\n * repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on\n * `existsSync`, `get` guards on `existsSync`), so the root is materialized\n * by `ensureRoot()` on the first write instead.\n */\n async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n\n // 1) Scan body files to build the head index. No-op on a missing root.\n await this.scanHeads();\n\n // 2) Hydrate nextSeq from the existing log. No-op on a missing log.\n const highest = await this.log.highestSeq();\n this.nextSeq = highest + 1;\n\n // 3) Start the watcher (unless disabled). chokidar cannot watch a path\n // that does not exist yet: measured on chokidar 5 with `usePolling`,\n // a root created AFTER `watch()` produces no events at all, ever. So\n // when the root is absent the watcher is armed later, by the\n // `ensureRoot()` call that brings the root into existence — otherwise\n // dropping the `mkdir` above would silently kill external-edit\n // detection for the whole life of the process.\n if (!this.disableWatch && existsSync(this.layout.root)) this.startWatcher();\n }\n\n /**\n * Bring the repository root into existence. Called by every write path\n * immediately before it touches the disk — `start()` deliberately does not\n * create it (#7000), so this is the single seam where the root appears.\n *\n * It is also where a watcher that `start()` could not arm (missing root)\n * gets armed, so \"external edits are detected\" survives the change.\n */\n private async ensureRoot(): Promise<void> {\n await fs.mkdir(this.layout.root, { recursive: true });\n if (this.started && !this.disableWatch && !this.watcher) this.startWatcher();\n }\n\n async close(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n this.started = false;\n }\n\n // ── Read API ────────────────────────────────────────────────────────\n\n async get(ref: MetaRef): Promise<MetadataItem | null> {\n this.assertScope(ref);\n const file = itemPath(this.layout, ref.type, ref.name);\n if (!existsSync(file)) return null;\n const body = await readJson(file);\n if (!body) return null;\n const hash = hashSpec(body);\n if (ref.version && ref.version !== hash) return null;\n // Walk back through the log to populate parent/authoredBy/seq.\n const meta = await this.findMetaForHash(ref, hash);\n return {\n ref: { ...ref, version: undefined },\n body: body as Record<string, unknown>,\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? new Date(0).toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n };\n }\n\n async getByHash(ref: MetaRef, hash: string): Promise<MetadataItem | null> {\n // FS repo stores only HEAD bodies on disk; the JSONL log records\n // events (hashes) but not historical bodies. Resolve only if the\n // requested hash matches HEAD.\n const head = await this.get(ref);\n if (!head || head.hash !== hash) return null;\n return head;\n }\n\n async *list(filter: ListFilter): AsyncIterable<MetadataItemHeader> {\n const limit = filter.limit ?? Infinity;\n let yielded = 0;\n for (const [key, hash] of this.heads) {\n const ref = parseRefKey(key);\n if (!ref) continue;\n if (!matchRefFilter(ref, filter)) continue;\n if (filter.nameContains && !ref.name.includes(filter.nameContains)) continue;\n const meta = await this.findMetaForHash(ref, hash);\n const header: MetadataItemHeader = {\n ref: { ...ref, version: undefined },\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? new Date(0).toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n };\n yield header;\n if (++yielded >= limit) return;\n }\n }\n\n async *history(ref: MetaRef, opts: HistoryOptions = {}): AsyncIterable<MetadataEvent> {\n this.assertScope(ref);\n const since = opts.sinceSeq ?? -1;\n const limit = opts.limit ?? Infinity;\n let yielded = 0;\n for await (const evt of this.log.readAll()) {\n if (evt.seq <= since) continue;\n if (evt.ref.type !== ref.type || evt.ref.name !== ref.name) continue;\n if (evt.ref.org !== ref.org) continue;\n yield evt;\n if (++yielded >= limit) return;\n }\n }\n\n watch(filter: WatchFilter, since?: number): AsyncIterable<MetadataEvent> {\n // Eagerly snapshot the existing log for replay; new events route via broker.\n const replay: MetadataEvent[] = [];\n const promise = (async () => {\n for await (const evt of this.log.readAll()) {\n if (matchEvent(evt, filter)) replay.push(evt);\n }\n })();\n // We must await replay before returning, but the public API is\n // sync-returning AsyncIterable. Wrap in a deferred iterable.\n return deferredIterable(promise.then(() =>\n createWatchIterable({\n filter,\n since,\n replay,\n broker: this.broker,\n matches: matchEvent,\n branchKeyOf: (e) => e.ref.org,\n }),\n ));\n }\n\n // ── Write API ───────────────────────────────────────────────────────\n\n put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise<PutResult> {\n this.assertScope(ref);\n return this.mutex.run(refKey(ref), async () => {\n const key = refKey(ref);\n const currentHead = this.heads.get(key) ?? null;\n if ((opts.parentVersion ?? null) !== currentHead) {\n throw new ConflictError(ref, opts.parentVersion ?? null, currentHead);\n }\n const hash = hashSpec(spec);\n if (currentHead === hash) {\n // No-op write — same content.\n const meta = await this.findMetaForHash(ref, hash);\n return {\n version: hash,\n seq: meta?.seq ?? 0,\n item: {\n ref: { ...ref, version: undefined },\n body: spec as Record<string, unknown>,\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? this.now().toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n },\n };\n }\n\n const seq = this.nextSeq++;\n const ts = this.now().toISOString();\n const file = itemPath(this.layout, ref.type, ref.name);\n // First write of the process materializes the root (#7000).\n await this.ensureRoot();\n await fs.mkdir(typeDir(this.layout, ref.type), { recursive: true });\n this.selfWrites.add(file);\n try {\n await writeJsonAtomic(file, spec);\n } finally {\n // Hold the suppression until chokidar has had a chance to emit;\n // we keep it in selfWrites for one debounce tick.\n setTimeout(() => this.selfWrites.delete(file), 200);\n }\n // The watcher must not depend on its own directory scan to notice a\n // path we created ourselves (#7282). See `trackWrittenPath`.\n this.trackWrittenPath(file);\n this.heads.set(key, hash);\n\n const evt: MetadataEvent = {\n seq,\n op: currentHead ? 'update' : 'create',\n ref: { ...ref, version: undefined },\n hash,\n parentHash: currentHead,\n actor: opts.actor,\n message: opts.message,\n ts,\n source: opts.source ?? 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n\n return {\n version: hash,\n seq,\n item: {\n ref: { ...ref, version: undefined },\n body: spec as Record<string, unknown>,\n hash,\n parentHash: currentHead,\n authoredBy: opts.actor,\n authoredAt: ts,\n message: opts.message,\n seq,\n },\n };\n });\n }\n\n delete(ref: MetaRef, opts: DeleteOptions): Promise<DeleteResult> {\n this.assertScope(ref);\n return this.mutex.run(refKey(ref), async () => {\n const key = refKey(ref);\n const currentHead = this.heads.get(key) ?? null;\n if (currentHead !== opts.parentVersion) {\n throw new ConflictError(ref, opts.parentVersion, currentHead);\n }\n const file = itemPath(this.layout, ref.type, ref.name);\n // A delete appends a tombstone to the change log, so it is a write too.\n await this.ensureRoot();\n this.selfWrites.add(file);\n try {\n if (existsSync(file)) await fs.unlink(file);\n } finally {\n setTimeout(() => this.selfWrites.delete(file), 200);\n }\n this.heads.delete(key);\n const seq = this.nextSeq++;\n const ts = this.now().toISOString();\n const evt: MetadataEvent = {\n seq,\n op: 'delete',\n ref: { ...ref, version: undefined },\n hash: null,\n parentHash: currentHead,\n actor: opts.actor,\n message: opts.message,\n ts,\n source: opts.source ?? 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n return { seq };\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────\n\n private assertScope(ref: MetaRef): void {\n if (ref.org !== this.org) {\n throw new Error(\n `FileSystemRepository scope mismatch: expected org=${this.org}, got org=${ref.org}`,\n );\n }\n }\n\n private async scanHeads(): Promise<void> {\n this.heads.clear();\n // Walk one level deep: <root>/<type>/<name>.json\n let entries: import('node:fs').Dirent[] = [];\n try {\n entries = await fs.readdir(this.layout.root, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith('.')) continue;\n const type = entry.name;\n const dir = path.join(this.layout.root, type);\n let files: string[] = [];\n try {\n files = await fs.readdir(dir);\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const name = file.slice(0, -'.json'.length);\n const ref: MetaRef = {\n org: this.org,\n type: type as MetadataType,\n name,\n };\n const body = await readJson(path.join(dir, file));\n if (!body) continue;\n this.heads.set(refKey(ref), hashSpec(body));\n }\n }\n }\n\n private async findMetaForHash(\n ref: MetaRef,\n hash: string,\n ): Promise<MetadataEvent | null> {\n let last: MetadataEvent | null = null;\n for await (const evt of this.log.readAll()) {\n if (evt.ref.type !== ref.type || evt.ref.name !== ref.name) continue;\n if (evt.ref.org !== ref.org) continue;\n if (evt.hash === hash) last = evt;\n }\n return last;\n }\n\n /**\n * Register a path this repository just wrote with the watcher (#7282).\n *\n * chokidar's initial scan is asynchronous, and every write path here can be\n * running **while it is still walking the tree** — `start()` arms the watcher\n * and the caller may `put()` on the next tick, and `ensureRoot()` arms it in\n * the middle of the very first write. With `usePolling` that combination has\n * a permanently-blinding interleaving, measured on chokidar 5 with this\n * repository's own options:\n *\n * 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic\n * `rename` in `writeJsonAtomic` has not landed yet.\n * 2. the rename lands; the directory's mtime changes.\n * 3. chokidar calls `watchFile()` on that directory, and libuv takes its\n * polling baseline stat — which already reflects step 2.\n *\n * From then on the directory's stat never changes again, so no poll ever\n * fires for it, `_handleRead` never re-runs, the item file is never added to\n * the watched set, and no per-file watcher is ever created. chokidar emits\n * neither `add` nor `change` for that path **for the life of the process** —\n * `getWatched()` reports the type directory as `[]` forever while the file\n * sits in it. That is the whole of #7282: the four merge-queue ejections all\n * waited out their deadlines (20s, then 25541ms against 25s) on an event that\n * was never going to be delivered, which is why widening the deadline and\n * widening the pre-edit sleep both changed nothing, and why lowering\n * `interval` would change nothing either — a shorter poll re-compares against\n * the same unchanged directory stat.\n *\n * The window is exactly \"files that exist at baseline time but were absent\n * from the snapshot read a moment earlier\", and the only writer that can be\n * inside it is us. So we close it at the source: tell the watcher explicitly\n * about every path we create, instead of hoping its scan happened to see it.\n *\n * `add()` is idempotent here — `_handleFile` returns early when the parent\n * directory already tracks the basename — and it emits nothing, because\n * chokidar treats an explicit `add()` as an initial add and `ignoreInitial`\n * is set. Its effect is the one we need: `_watchWithNodeFs` registers the\n * basename with the parent directory (without which chokidar drops `change`\n * events for the file) and starts the per-file poll.\n */\n private trackWrittenPath(file: string): void {\n const w = this.watcher;\n // `add()` clears `closed`, so never hand a closing watcher a new path.\n if (!w || w.closed) return;\n w.add(file);\n }\n\n private startWatcher(): void {\n const root = this.layout.root;\n const w = chokidar.watch(root, {\n // Skip dotfiles under the root — including the repository's own\n // `.objectstack/` bookkeeping subtree — matched on the path RELATIVE\n // to the watch root (#7150). See `isIgnoredWatchPath`.\n ignored: (p: string) => isIgnoredWatchPath(root, p),\n ignoreInitial: true,\n depth: 2,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },\n // Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.\n // The depth-2 recursion would otherwise wire native watches across\n // the entire customization tree.\n usePolling: true,\n interval: 1000,\n binaryInterval: 2000,\n });\n w.on('add', (p) => void this.handleFsChange(p, 'add'));\n w.on('change', (p) => void this.handleFsChange(p, 'change'));\n w.on('unlink', (p) => void this.handleFsChange(p, 'unlink'));\n this.watcher = w;\n }\n\n private async handleFsChange(absPath: string, kind: 'add' | 'change' | 'unlink'): Promise<void> {\n if (this.selfWrites.has(absPath)) return; // Suppress our own writes.\n const parsed = parseItemPath(this.layout, absPath);\n if (!parsed) return;\n const ref: MetaRef = {\n org: this.org,\n type: parsed.type as MetadataType,\n name: parsed.name,\n };\n const key = refKey(ref);\n await this.mutex.run(key, async () => {\n if (kind === 'unlink') {\n const currentHead = this.heads.get(key) ?? null;\n if (!currentHead) return;\n this.heads.delete(key);\n const seq = this.nextSeq++;\n const evt: MetadataEvent = {\n seq,\n op: 'delete',\n ref: { ...ref, version: undefined },\n hash: null,\n parentHash: currentHead,\n actor: this.fsActor,\n ts: this.now().toISOString(),\n source: 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n return;\n }\n const body = await readJson(absPath);\n if (!body) return;\n const hash = hashSpec(body);\n const currentHead = this.heads.get(key) ?? null;\n if (currentHead === hash) return; // No content change.\n this.heads.set(key, hash);\n const seq = this.nextSeq++;\n const evt: MetadataEvent = {\n seq,\n op: currentHead ? 'update' : 'create',\n ref: { ...ref, version: undefined },\n hash,\n parentHash: currentHead,\n actor: this.fsActor,\n ts: this.now().toISOString(),\n source: 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n });\n }\n}\n\n// ── Utilities ─────────────────────────────────────────────────────────\n\n/**\n * Watcher ignore matcher — \"everything under the root, except the\n * repository's own bookkeeping\" (#7150).\n *\n * chokidar hands its matcher **absolute** paths, and applies it to the\n * watched root itself as well as to entries discovered underneath it. The\n * previous matcher was a bare dotfile regex (`/(^|[\\\\/])\\../`), which\n * therefore matched the `.objectstack` segment of the root path the plugin\n * actually uses (`<project>/.objectstack/metadata`, `REPO_SUBDIR` in\n * `packages/metadata/src/plugin.ts`) and ignored the whole watch. Measured on\n * chokidar 5 with this repository's own options, two identical trees\n * differing only in whether the root sits under a dot-directory:\n *\n * plain root getWatched: ['<root>', 'view'] events: add+change\n * dot-rooted getWatched: [] events: none\n *\n * So the intent is kept and only the *frame of reference* is fixed: judge the\n * path relative to the root, so dot segments belonging to the root itself are\n * never considered.\n *\n * Why not drop the matcher entirely and lean on `parseItemPath`, which already\n * rejects `.objectstack`? Measured: `parseItemPath` rejects that ONE name, so\n * a dot-directory at the type level leaks — `<root>/.cache/x.json` parses as\n * type `.cache`, and `<root>/view/.scratch.json` as an item named `.scratch`.\n * Both would be published as `MetadataEvent`s while `scanHeads` skips every\n * dot entry on boot, leaving the boot scan and the watcher disagreeing about\n * what the repository contains. Dropping it also puts `.objectstack/.log/` in\n * the poll set, so every one of the repository's own log appends wakes\n * `handleFsChange` only to be discarded.\n */\nfunction isIgnoredWatchPath(root: string, absPath: string): boolean {\n const rel = path.relative(root, absPath);\n // The watched root itself, and anything outside it, are not ours to judge.\n if (rel === '' || rel.startsWith('..')) return false;\n return rel.split(/[\\\\/]/).some((segment) => segment.startsWith('.'));\n}\n\nasync function readJson(file: string): Promise<unknown | null> {\n try {\n const text = await fs.readFile(file, 'utf8');\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nasync function writeJsonAtomic(file: string, body: unknown): Promise<void> {\n const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(body, null, 2) + '\\n', 'utf8');\n await fs.rename(tmp, file);\n}\n\nfunction parseRefKey(key: string): MetaRef | null {\n const parts = key.split('/');\n if (parts.length !== 3) return null;\n return {\n org: parts[0]!,\n type: parts[1]! as MetadataType,\n name: parts[2]!,\n };\n}\n\n/**\n * Wrap a Promise<AsyncIterable<T>> as a sync-returning AsyncIterable<T>.\n * The first `.next()` awaits the promise.\n */\nfunction deferredIterable<T>(promise: Promise<AsyncIterable<T>>): AsyncIterable<T> {\n return {\n [Symbol.asyncIterator]() {\n let inner: AsyncIterator<T> | null = null;\n return {\n async next() {\n if (!inner) {\n const iterable = await promise;\n inner = iterable[Symbol.asyncIterator]();\n }\n return inner.next();\n },\n async return(value?: unknown) {\n if (!inner) {\n const iterable = await promise;\n inner = iterable[Symbol.asyncIterator]();\n }\n if (inner.return) return inner.return(value);\n return { value: undefined, done: true };\n },\n } as AsyncIterator<T>;\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Disk layout helpers — see ADR-0008 §10 PR-4 / packages/metadata-fs README.\n *\n * <root>/<type>/<name>.json — canonical body\n * <root>/.objectstack/.log/main.jsonl — append-only change log\n */\n\nimport path from 'node:path';\nimport type { MetadataType } from '@objectstack/metadata-core';\n\nexport interface FsLayout {\n /** Absolute path to the metadata root. */\n root: string;\n}\n\nexport function itemPath(layout: FsLayout, type: MetadataType, name: string): string {\n return path.join(layout.root, type, `${name}.json`);\n}\n\nexport function typeDir(layout: FsLayout, type: MetadataType): string {\n return path.join(layout.root, type);\n}\n\nexport function logDir(layout: FsLayout): string {\n return path.join(layout.root, '.objectstack', '.log');\n}\n\nexport function logFile(layout: FsLayout): string {\n // Single change log per filesystem root (branching is a Git concern,\n // not a metadata-layer concern).\n return path.join(logDir(layout), `main.jsonl`);\n}\n\n/** Parse a path like \".../view/case_grid.json\" into {type, name}. */\nexport function parseItemPath(\n layout: FsLayout,\n absPath: string,\n): { type: string; name: string } | null {\n const rel = path.relative(layout.root, absPath);\n if (rel.startsWith('..') || rel.startsWith('.objectstack')) return null;\n const segments = rel.split(path.sep);\n if (segments.length !== 2) return null;\n const type = segments[0]!;\n const file = segments[1]!;\n if (!file.endsWith('.json')) return null;\n const name = file.slice(0, -'.json'.length);\n return { type, name };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Append-only JSONL change log writer / reader. Each line is a single\n * `MetadataEvent` serialized via `JSON.stringify`.\n *\n * Durability strategy\n * ───────────────────\n * - Append with `O_APPEND` semantics (Node's `fs.appendFile` is\n * atomic for sub-PIPE_BUF-sized writes; events are well under 4 KiB).\n * - Read by streaming the file line-by-line and JSON.parse-ing each.\n * - On a corrupt line we skip and continue — the body files are the\n * source of truth; the log is a denormalised history index.\n */\n\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport readline from 'node:readline';\nimport { createReadStream, existsSync } from 'node:fs';\nimport type { MetadataEvent } from '@objectstack/metadata-core';\n\nexport class JsonlLog {\n constructor(private readonly file: string) {}\n\n async append(evt: MetadataEvent): Promise<void> {\n await fs.mkdir(path.dirname(this.file), { recursive: true });\n await fs.appendFile(this.file, JSON.stringify(evt) + '\\n', 'utf8');\n }\n\n /** Read all events in seq order (i.e. file order). */\n async *readAll(): AsyncIterable<MetadataEvent> {\n if (!existsSync(this.file)) return;\n const rl = readline.createInterface({\n input: createReadStream(this.file, { encoding: 'utf8' }),\n crlfDelay: Infinity,\n });\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n yield JSON.parse(line) as MetadataEvent;\n } catch {\n // Skip corrupt line.\n }\n }\n } finally {\n rl.close();\n }\n }\n\n /** Return the highest seq number in the log, or 0 if empty. */\n async highestSeq(): Promise<number> {\n let max = 0;\n for await (const evt of this.readAll()) {\n if (typeof evt.seq === 'number' && evt.seq > max) max = evt.seq;\n }\n return max;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Mutex / event-broker primitives used by FileSystemRepository.\n *\n * `KeyedMutex` serializes operations on the same key (refKey). The\n * broker re-uses the same manual-AsyncIterator pattern as\n * InMemoryRepository so that consumer `return()` reliably unblocks.\n */\n\nimport type { MetadataEvent, WatchFilter } from '@objectstack/metadata-core';\n\nexport class KeyedMutex {\n private readonly tails = new Map<string, Promise<unknown>>();\n\n async run<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const prev = this.tails.get(key) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n // Save the swallowed-error tail so successive runs don't reject on\n // an unrelated prior failure.\n const swallowed = next.catch(() => undefined);\n this.tails.set(key, swallowed);\n try {\n return await next;\n } finally {\n // Best-effort cleanup: drop the entry if nothing newer was queued.\n if (this.tails.get(key) === swallowed) {\n this.tails.delete(key);\n }\n }\n }\n}\n\nexport interface BrokerSubscriber {\n filter: WatchFilter;\n closed: boolean;\n push(evt: MetadataEvent): void;\n}\n\nexport interface EventBroker {\n subscribe(sub: BrokerSubscriber): void;\n unsubscribe(sub: BrokerSubscriber): void;\n publish(evt: MetadataEvent): void;\n}\n\nexport function createBroker(matches: (evt: MetadataEvent, filter: WatchFilter) => boolean): EventBroker {\n const subs = new Set<BrokerSubscriber>();\n return {\n subscribe: (s) => { subs.add(s); },\n unsubscribe: (s) => { subs.delete(s); },\n publish: (evt) => {\n for (const s of subs) {\n if (s.closed) continue;\n if (!matches(evt, s.filter)) continue;\n s.push(evt);\n }\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Manual `AsyncIterator` factory for `repo.watch()`. Mirrors the\n * pattern used in `@objectstack/metadata-core`'s `InMemoryRepository`:\n * async generators do NOT run `finally` when paused on an unresolved\n * `await`, so we cannot use them to implement `watch()`.\n */\n\nimport type { MetadataEvent, WatchFilter } from '@objectstack/metadata-core';\nimport { type EventBroker, type BrokerSubscriber } from './sync.js';\n\nexport interface CreateWatchIteratorArgs {\n filter: WatchFilter;\n since: number | undefined;\n replay: MetadataEvent[];\n broker: EventBroker;\n /** Returns true if `evt.ref` matches `filter`. */\n matches: (evt: MetadataEvent, filter: WatchFilter) => boolean;\n branchKeyOf: (evt: MetadataEvent) => string;\n}\n\nexport function createWatchIterable(\n args: CreateWatchIteratorArgs,\n): AsyncIterable<MetadataEvent> {\n const queue: MetadataEvent[] = [];\n let waiter: ((evt: IteratorResult<MetadataEvent>) => void) | null = null;\n let closed = false;\n const delivered = new Set<string>();\n const evtKey = (e: MetadataEvent) => `${args.branchKeyOf(e)}#${e.seq}`;\n\n const subscriber: BrokerSubscriber = {\n filter: args.filter,\n closed: false,\n push: (evt) => {\n if (subscriber.closed) return;\n const k = evtKey(evt);\n if (delivered.has(k)) return;\n if (waiter) {\n delivered.add(k);\n const w = waiter;\n waiter = null;\n w({ value: clone(evt), done: false });\n } else {\n queue.push(evt);\n }\n },\n };\n args.broker.subscribe(subscriber);\n\n const replay = [...args.replay].sort((a, b) => a.seq - b.seq);\n let replayIdx = 0;\n\n const drain = (): IteratorResult<MetadataEvent> | null => {\n while (replayIdx < replay.length) {\n const evt = replay[replayIdx++]!;\n if (typeof args.since === 'number' && evt.seq <= args.since) continue;\n const k = evtKey(evt);\n if (delivered.has(k)) continue;\n delivered.add(k);\n return { value: clone(evt), done: false };\n }\n while (queue.length > 0) {\n const evt = queue.shift()!;\n const k = evtKey(evt);\n if (delivered.has(k)) continue;\n delivered.add(k);\n return { value: clone(evt), done: false };\n }\n return null;\n };\n\n const close = (): IteratorResult<MetadataEvent> => {\n if (!closed) {\n closed = true;\n subscriber.closed = true;\n args.broker.unsubscribe(subscriber);\n if (waiter) {\n const w = waiter;\n waiter = null;\n w({ value: undefined, done: true });\n }\n }\n return { value: undefined, done: true };\n };\n\n const iterator: AsyncIterator<MetadataEvent> = {\n next: () => {\n if (closed) return Promise.resolve({ value: undefined, done: true });\n const immediate = drain();\n if (immediate) return Promise.resolve(immediate);\n return new Promise<IteratorResult<MetadataEvent>>((resolve) => {\n waiter = resolve;\n });\n },\n return: () => Promise.resolve(close()),\n throw: (err) => {\n close();\n return Promise.reject(err);\n },\n };\n return { [Symbol.asyncIterator]: () => iterator };\n}\n\nfunction clone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n"],"mappings":"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","names":["import_promises","import_node_fs","import_node_path","path","import_node_path","fs","path","readline","path","fs","chokidar","currentHead","seq","evt"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/repository.ts","../src/layout.ts","../src/jsonl-log.ts","../src/sync.ts","../src/watch-iterable.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport * from './repository.js';\nexport { JsonlLog } from './jsonl-log.js';\nexport type { FsLayout } from './layout.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `FileSystemRepository` — Node-only implementation of\n * `MetadataRepository` backed by JSON files plus a JSONL change log.\n *\n * See `README.md` for the on-disk layout and ADR-0008 §10 PR-4 for the\n * design rationale.\n *\n * Invariants\n * ──────────\n * - All `put` / `delete` ops serialize per-key via `KeyedMutex`.\n * - The change-log JSONL is the durable source of `seq`. On boot we\n * scan the log to learn the next seq value.\n * - Body files (`<type>/<name>.json`) are the source of truth; the\n * log is a denormalised history index.\n * - chokidar-driven external edits are translated into MetadataEvents\n * by hashing the new content and comparing to the last-known hash.\n * - The root directory is created **on the first write, not on attach**\n * (#7000). Attaching and reading a repository whose root does not exist\n * is legal and answers \"empty\"; see `start()` / `ensureRoot()`.\n */\n\nimport fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\nimport type { FSWatcher } from 'chokidar';\nimport chokidar from 'chokidar';\nimport {\n type MetadataRepository,\n type MetaRef,\n type MetadataItem,\n type MetadataItemHeader,\n type MetadataEvent,\n type PutOptions,\n type PutResult,\n type DeleteOptions,\n type DeleteResult,\n type ListFilter,\n type WatchFilter,\n type HistoryOptions,\n type MetadataType,\n hashSpec,\n ConflictError,\n refKey,\n} from '@objectstack/metadata-core';\nimport {\n type FsLayout,\n itemPath,\n parseItemPath,\n typeDir,\n logFile,\n} from './layout.js';\nimport { JsonlLog } from './jsonl-log.js';\nimport { KeyedMutex, createBroker, type EventBroker } from './sync.js';\nimport { createWatchIterable } from './watch-iterable.js';\n\nexport interface FileSystemRepositoryOptions {\n /** Absolute path to the metadata root directory. */\n root: string;\n /** Tenant/org. */\n org: string;\n /** Identity reported in events that originate from external FS edits. */\n fsActor?: string;\n /** Disable chokidar watcher (e.g. for read-only contexts). */\n disableWatch?: boolean;\n /** Optional clock injection for deterministic tests. */\n now?: () => Date;\n}\n\nconst matchRefFilter = (\n ref: MetaRef,\n filter: { org?: string; type?: MetadataType; name?: string },\n): boolean => {\n if (filter.org && filter.org !== ref.org) return false;\n if (filter.type && filter.type !== ref.type) return false;\n if (filter.name && filter.name !== ref.name) return false;\n return true;\n};\n\nconst matchEvent = (evt: MetadataEvent, filter: WatchFilter): boolean => matchRefFilter(evt.ref, filter);\n\n/**\n * Cadence of the content-keyed reconciliation sweep (#9339).\n *\n * Twice the watcher's own 1000ms poll interval: long enough that the watcher\n * normally delivers first and the sweep finds nothing to do, short enough that\n * a delivery the watcher lost is recovered in the same order of magnitude as a\n * poll rather than at the next process restart.\n *\n * It is deliberately NOT derived from `interval` at runtime. The two are\n * independent knobs — the poll interval sets detection latency for the fast\n * path, this sets the worst-case latency of the backstop — and coupling them\n * would make a future change to one silently retune the other.\n */\nconst RESYNC_INTERVAL_MS = 2_000;\n\n/**\n * The ONE errno that is a truthful \"there is nothing here\" for a directory\n * read, as opposed to \"the read could not run\" (#8895 — discriminate or\n * propagate). A path that does not exist holds no items, so answering with an\n * empty listing states a fact. Every other code — EACCES, EIO, ENOTDIR, and\n * above all EMFILE/ENFILE under fd exhaustion — means the answer was never\n * obtained, and inventing an empty one there is the defect itself.\n */\nconst isEnoent = (err: unknown): boolean =>\n (err as NodeJS.ErrnoException | null)?.code === 'ENOENT';\n\nexport class FileSystemRepository implements MetadataRepository {\n private readonly layout: FsLayout;\n private readonly org: string;\n private readonly fsActor: string;\n private readonly disableWatch: boolean;\n private readonly now: () => Date;\n private readonly log: JsonlLog;\n private readonly mutex = new KeyedMutex();\n private readonly broker: EventBroker = createBroker(matchEvent);\n\n /** In-memory index: refKey → current hash (HEAD). */\n private readonly heads = new Map<string, string>();\n /** Next seq counter, hydrated from the log on `start()`. */\n private nextSeq = 1;\n private watcher: FSWatcher | null = null;\n private started = false;\n /** Pending reconciliation sweep (#9339). Chained, never overlapping. */\n private resyncTimer: ReturnType<typeof setTimeout> | null = null;\n /** False before the watcher is armed and from `close()` onwards. */\n private resyncEnabled = false;\n /**\n * Sweep read faults already reported, keyed `CODE @ path`, so a standing\n * fault is announced once rather than every 2s (AGENTS.md: say it once, at\n * the first degradation). An entry is cleared when that path reads again.\n */\n private readonly resyncFaults = new Set<string>();\n\n constructor(opts: FileSystemRepositoryOptions) {\n this.org = opts.org;\n this.fsActor = opts.fsActor ?? 'fs';\n this.disableWatch = opts.disableWatch ?? false;\n this.now = opts.now ?? (() => new Date());\n this.layout = { root: path.resolve(opts.root) };\n this.log = new JsonlLog(logFile(this.layout));\n }\n\n // ── Lifecycle ───────────────────────────────────────────────────────\n\n /**\n * Attach the repository. **Creates nothing on disk** (#7000).\n *\n * Attaching is not a write. `start()` used to `mkdir` both the root and\n * `<root>/.objectstack/.log` unconditionally, which meant every read-only\n * boot that merely attaches a repository left a skeleton behind — most\n * visibly `os migrate plan`, a declared dry run, on a project that has\n * never been started. That is the same property #6743 ruled on for\n * `.objectstack/data/`: a dry run leaves nothing behind, and the existence\n * of `.objectstack/` has to stay a usable \"this project has been started\"\n * signal.\n *\n * Every read path below already treats a missing root as an empty\n * repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on\n * `existsSync`, `get` guards on `existsSync`), so the root is materialized\n * by `ensureRoot()` on the first write instead.\n */\n async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n\n // 1) Scan body files to build the head index. No-op on a missing root.\n await this.scanHeads();\n\n // 2) Hydrate nextSeq from the existing log. No-op on a missing log.\n const highest = await this.log.highestSeq();\n this.nextSeq = highest + 1;\n\n // 3) Start the watcher (unless disabled). chokidar cannot watch a path\n // that does not exist yet: measured on chokidar 5 with `usePolling`,\n // a root created AFTER `watch()` produces no events at all, ever. So\n // when the root is absent the watcher is armed later, by the\n // `ensureRoot()` call that brings the root into existence — otherwise\n // dropping the `mkdir` above would silently kill external-edit\n // detection for the whole life of the process.\n if (!this.disableWatch && existsSync(this.layout.root)) this.startWatcher();\n }\n\n /**\n * Bring the repository root into existence. Called by every write path\n * immediately before it touches the disk — `start()` deliberately does not\n * create it (#7000), so this is the single seam where the root appears.\n *\n * It is also where a watcher that `start()` could not arm (missing root)\n * gets armed, so \"external edits are detected\" survives the change.\n */\n private async ensureRoot(): Promise<void> {\n await fs.mkdir(this.layout.root, { recursive: true });\n if (this.started && !this.disableWatch && !this.watcher) this.startWatcher();\n }\n\n async close(): Promise<void> {\n // Retire the sweep BEFORE awaiting the watcher, so a sweep that lands\n // during `watcher.close()` cannot reschedule itself behind our back.\n this.stopResync();\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n this.started = false;\n }\n\n // ── Read API ────────────────────────────────────────────────────────\n\n async get(ref: MetaRef): Promise<MetadataItem | null> {\n this.assertScope(ref);\n const file = itemPath(this.layout, ref.type, ref.name);\n if (!existsSync(file)) return null;\n const body = await readJson(file);\n if (!body) return null;\n const hash = hashSpec(body);\n if (ref.version && ref.version !== hash) return null;\n // Walk back through the log to populate parent/authoredBy/seq.\n const meta = await this.findMetaForHash(ref, hash);\n return {\n ref: { ...ref, version: undefined },\n body: body as Record<string, unknown>,\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? new Date(0).toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n };\n }\n\n async getByHash(ref: MetaRef, hash: string): Promise<MetadataItem | null> {\n // FS repo stores only HEAD bodies on disk; the JSONL log records\n // events (hashes) but not historical bodies. Resolve only if the\n // requested hash matches HEAD.\n const head = await this.get(ref);\n if (!head || head.hash !== hash) return null;\n return head;\n }\n\n async *list(filter: ListFilter): AsyncIterable<MetadataItemHeader> {\n const limit = filter.limit ?? Infinity;\n let yielded = 0;\n for (const [key, hash] of this.heads) {\n const ref = parseRefKey(key);\n if (!ref) continue;\n if (!matchRefFilter(ref, filter)) continue;\n if (filter.nameContains && !ref.name.includes(filter.nameContains)) continue;\n const meta = await this.findMetaForHash(ref, hash);\n const header: MetadataItemHeader = {\n ref: { ...ref, version: undefined },\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? new Date(0).toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n };\n yield header;\n if (++yielded >= limit) return;\n }\n }\n\n async *history(ref: MetaRef, opts: HistoryOptions = {}): AsyncIterable<MetadataEvent> {\n this.assertScope(ref);\n const since = opts.sinceSeq ?? -1;\n const limit = opts.limit ?? Infinity;\n let yielded = 0;\n for await (const evt of this.log.readAll()) {\n if (evt.seq <= since) continue;\n if (evt.ref.type !== ref.type || evt.ref.name !== ref.name) continue;\n if (evt.ref.org !== ref.org) continue;\n yield evt;\n if (++yielded >= limit) return;\n }\n }\n\n watch(filter: WatchFilter, since?: number): AsyncIterable<MetadataEvent> {\n // Eagerly snapshot the existing log for replay; new events route via broker.\n const replay: MetadataEvent[] = [];\n const promise = (async () => {\n for await (const evt of this.log.readAll()) {\n if (matchEvent(evt, filter)) replay.push(evt);\n }\n })();\n // We must await replay before returning, but the public API is\n // sync-returning AsyncIterable. Wrap in a deferred iterable.\n return deferredIterable(promise.then(() =>\n createWatchIterable({\n filter,\n since,\n replay,\n broker: this.broker,\n matches: matchEvent,\n branchKeyOf: (e) => e.ref.org,\n }),\n ));\n }\n\n // ── Write API ───────────────────────────────────────────────────────\n\n put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise<PutResult> {\n this.assertScope(ref);\n return this.mutex.run(refKey(ref), async () => {\n const key = refKey(ref);\n const currentHead = this.heads.get(key) ?? null;\n if ((opts.parentVersion ?? null) !== currentHead) {\n throw new ConflictError(ref, opts.parentVersion ?? null, currentHead);\n }\n const hash = hashSpec(spec);\n if (currentHead === hash) {\n // No-op write — same content.\n const meta = await this.findMetaForHash(ref, hash);\n return {\n version: hash,\n seq: meta?.seq ?? 0,\n item: {\n ref: { ...ref, version: undefined },\n body: spec as Record<string, unknown>,\n hash,\n parentHash: meta?.parentHash ?? null,\n authoredBy: meta?.actor ?? this.fsActor,\n authoredAt: meta?.ts ?? this.now().toISOString(),\n message: meta?.message,\n seq: meta?.seq ?? 0,\n },\n };\n }\n\n const seq = this.nextSeq++;\n const ts = this.now().toISOString();\n const file = itemPath(this.layout, ref.type, ref.name);\n // First write of the process materializes the root (#7000).\n await this.ensureRoot();\n await fs.mkdir(typeDir(this.layout, ref.type), { recursive: true });\n await writeJsonAtomic(file, spec);\n // The watcher must not depend on its own directory scan to notice a\n // path we created ourselves (#7282). See `trackWrittenPath`.\n this.trackWrittenPath(file);\n // Publishing the new head here is what suppresses the watcher event this\n // write is about to produce — see `handleFsChange` (#7335). It runs in\n // the same continuation as the `rename` above, and `awaitWriteFinish`\n // holds any event for a further `stabilityThreshold`, so the index is\n // always current by the time an event for this path can be delivered.\n this.heads.set(key, hash);\n\n const evt: MetadataEvent = {\n seq,\n op: currentHead ? 'update' : 'create',\n ref: { ...ref, version: undefined },\n hash,\n parentHash: currentHead,\n actor: opts.actor,\n message: opts.message,\n ts,\n source: opts.source ?? 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n\n return {\n version: hash,\n seq,\n item: {\n ref: { ...ref, version: undefined },\n body: spec as Record<string, unknown>,\n hash,\n parentHash: currentHead,\n authoredBy: opts.actor,\n authoredAt: ts,\n message: opts.message,\n seq,\n },\n };\n });\n }\n\n delete(ref: MetaRef, opts: DeleteOptions): Promise<DeleteResult> {\n this.assertScope(ref);\n return this.mutex.run(refKey(ref), async () => {\n const key = refKey(ref);\n const currentHead = this.heads.get(key) ?? null;\n if (currentHead !== opts.parentVersion) {\n throw new ConflictError(ref, opts.parentVersion, currentHead);\n }\n const file = itemPath(this.layout, ref.type, ref.name);\n // A delete appends a tombstone to the change log, so it is a write too.\n await this.ensureRoot();\n // Retire the head BEFORE touching the disk, not after (#7335).\n //\n // `awaitWriteFinish` only debounces `add`/`change`; chokidar emits\n // `unlink` with no stability delay at all, so — unlike `put()` — this\n // face has no cushion between the disk mutation and the event it\n // produces. Clearing the index first makes `handleFsChange`'s\n // `if (!currentHead) return` a total suppression for our own removal\n // rather than a race against the poll callback.\n this.heads.delete(key);\n try {\n if (existsSync(file)) await fs.unlink(file);\n } catch (err) {\n // The disk still holds the item, so the index must too — otherwise a\n // failed delete would leave the repository claiming a file it can\n // still read. Restores exactly the pre-call state before rethrowing.\n if (currentHead !== null) this.heads.set(key, currentHead);\n throw err;\n }\n const seq = this.nextSeq++;\n const ts = this.now().toISOString();\n const evt: MetadataEvent = {\n seq,\n op: 'delete',\n ref: { ...ref, version: undefined },\n hash: null,\n parentHash: currentHead,\n actor: opts.actor,\n message: opts.message,\n ts,\n source: opts.source ?? 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n return { seq };\n });\n }\n\n // ── Internals ───────────────────────────────────────────────────────\n\n private assertScope(ref: MetaRef): void {\n if (ref.org !== this.org) {\n throw new Error(\n `FileSystemRepository scope mismatch: expected org=${this.org}, got org=${ref.org}`,\n );\n }\n }\n\n private async scanHeads(): Promise<void> {\n this.heads.clear();\n // Walk one level deep: <root>/<type>/<name>.json\n let entries: import('node:fs').Dirent[] = [];\n try {\n entries = await fs.readdir(this.layout.root, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith('.')) continue;\n const type = entry.name;\n const dir = path.join(this.layout.root, type);\n let files: string[] = [];\n try {\n files = await fs.readdir(dir);\n } catch {\n continue;\n }\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const name = file.slice(0, -'.json'.length);\n const ref: MetaRef = {\n org: this.org,\n type: type as MetadataType,\n name,\n };\n const body = await readJson(path.join(dir, file));\n if (!body) continue;\n this.heads.set(refKey(ref), hashSpec(body));\n }\n }\n }\n\n private async findMetaForHash(\n ref: MetaRef,\n hash: string,\n ): Promise<MetadataEvent | null> {\n let last: MetadataEvent | null = null;\n for await (const evt of this.log.readAll()) {\n if (evt.ref.type !== ref.type || evt.ref.name !== ref.name) continue;\n if (evt.ref.org !== ref.org) continue;\n if (evt.hash === hash) last = evt;\n }\n return last;\n }\n\n /**\n * Register a path this repository just wrote with the watcher (#7282).\n *\n * chokidar's initial scan is asynchronous, and every write path here can be\n * running **while it is still walking the tree** — `start()` arms the watcher\n * and the caller may `put()` on the next tick, and `ensureRoot()` arms it in\n * the middle of the very first write. With `usePolling` that combination has\n * a permanently-blinding interleaving, measured on chokidar 5 with this\n * repository's own options:\n *\n * 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic\n * `rename` in `writeJsonAtomic` has not landed yet.\n * 2. the rename lands; the directory's mtime changes.\n * 3. chokidar calls `watchFile()` on that directory, and libuv takes its\n * polling baseline stat — which already reflects step 2.\n *\n * From then on the directory's stat never changes again, so no poll ever\n * fires for it, `_handleRead` never re-runs, the item file is never added to\n * the watched set, and no per-file watcher is ever created. chokidar emits\n * neither `add` nor `change` for that path **for the life of the process** —\n * `getWatched()` reports the type directory as `[]` forever while the file\n * sits in it. That is the whole of #7282: the four merge-queue ejections all\n * waited out their deadlines (20s, then 25541ms against 25s) on an event that\n * was never going to be delivered, which is why widening the deadline and\n * widening the pre-edit sleep both changed nothing, and why lowering\n * `interval` would change nothing either — a shorter poll re-compares against\n * the same unchanged directory stat.\n *\n * The window is exactly \"files that exist at baseline time but were absent\n * from the snapshot read a moment earlier\", and the only writer that can be\n * inside it is us. So we close it at the source: tell the watcher explicitly\n * about every path we create, instead of hoping its scan happened to see it.\n *\n * `add()` is idempotent here — `_handleFile` returns early when the parent\n * directory already tracks the basename — and it emits nothing, because\n * chokidar treats an explicit `add()` as an initial add and `ignoreInitial`\n * is set. Its effect is the one we need: `_watchWithNodeFs` registers the\n * basename with the parent directory (without which chokidar drops `change`\n * events for the file) and starts the per-file poll.\n */\n private trackWrittenPath(file: string): void {\n const w = this.watcher;\n // `add()` clears `closed`, so never hand a closing watcher a new path.\n if (!w || w.closed) return;\n w.add(file);\n }\n\n private startWatcher(): void {\n const root = this.layout.root;\n const w = chokidar.watch(root, {\n // Skip dotfiles under the root — including the repository's own\n // `.objectstack/` bookkeeping subtree — matched on the path RELATIVE\n // to the watch root (#7150). See `isIgnoredWatchPath`.\n ignored: (p: string) => isIgnoredWatchPath(root, p),\n ignoreInitial: true,\n depth: 2,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },\n // Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.\n // The depth-2 recursion would otherwise wire native watches across\n // the entire customization tree.\n usePolling: true,\n interval: 1000,\n binaryInterval: 2000,\n });\n w.on('add', (p) => void this.handleFsChange(p, 'add'));\n w.on('change', (p) => void this.handleFsChange(p, 'change'));\n w.on('unlink', (p) => void this.handleFsChange(p, 'unlink'));\n this.watcher = w;\n // The watcher is the fast path, not the guarantee (#9339). See `resync`.\n this.startResync();\n }\n\n /**\n * Publish the `delete` face of an externally-observed removal.\n *\n * Extracted from `handleFsChange` unchanged so the reconciliation sweep\n * (#9339) can reuse it **verbatim** rather than growing a second copy of the\n * event shape. The one-line invariant: the caller already holds the per-key\n * mutex, and `!currentHead` is the content-keyed suppression that makes our\n * own `delete()` a no-op here.\n */\n private async publishExternalDelete(ref: MetaRef, key: string): Promise<void> {\n const currentHead = this.heads.get(key) ?? null;\n if (!currentHead) return;\n this.heads.delete(key);\n const seq = this.nextSeq++;\n const evt: MetadataEvent = {\n seq,\n op: 'delete',\n ref: { ...ref, version: undefined },\n hash: null,\n parentHash: currentHead,\n actor: this.fsActor,\n ts: this.now().toISOString(),\n source: 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n }\n\n private startResync(): void {\n this.resyncEnabled = true;\n this.scheduleResync();\n }\n\n private stopResync(): void {\n this.resyncEnabled = false;\n if (this.resyncTimer) {\n clearTimeout(this.resyncTimer);\n this.resyncTimer = null;\n }\n }\n\n /**\n * Schedule the next sweep — chained, never `setInterval` (#9339).\n *\n * A chained timeout cannot stack: the next sweep is armed only once the\n * previous one has finished, so a saturated runner degrades to *fewer*\n * sweeps instead of a growing backlog of overlapping tree walks. The timer\n * is `unref`ed because a backstop must never be the reason a process stays\n * alive.\n */\n private scheduleResync(): void {\n if (!this.resyncEnabled || this.resyncTimer) return;\n const timer = setTimeout(() => {\n this.resyncTimer = null;\n void this.resync().finally(() => this.scheduleResync());\n }, RESYNC_INTERVAL_MS);\n timer.unref?.();\n this.resyncTimer = timer;\n }\n\n /**\n * Announce a sweep read that could not run — the non-silence half of #8895's\n * \"discriminate or propagate\".\n *\n * ## Why `error` and not `warn`\n *\n * AGENTS.md decides the level with one question: *after the degradation, does\n * the system still look \"normal\" from the outside while something it claims\n * is persisted has not actually landed?* Here it does. Nothing throws, the\n * watcher stays armed, `getWatched()` stays populated, `start()` succeeded —\n * and the repository's index quietly stops tracking what is on disk. That is\n * the rule's second limb verbatim (\"persisted state and runtime state\n * disagree\"), not the functional-degradation limb: no capability is visibly\n * smaller, so nobody finds out by using the missing thing.\n *\n * The counter-argument — *this is only a backstop, the watcher is still the\n * fast path* — is why the level is arguable, and it does not survive the\n * failing errno. The sharp case is fd exhaustion: EMFILE/ENFILE break this\n * `readdir` and chokidar's `fs.watchFile` polling **at the same time and for\n * the same reason**, so the fast path is not an independent fallback under\n * precisely the load that produces this fault. A backstop that is silently\n * absent whenever it is most needed is a durability-shaped degradation.\n *\n * ⚠️ AGENTS.md also warns against over-applying `error`, and the discipline\n * that answers it is the ledger, not a quieter level: an `error` owes the\n * consequence and the fix, said **once** at the first degradation rather than\n * once per failed read. A sweep runs every 2s forever, so an unlatched\n * `console.error` here would be the mirror-image failure the same rule names.\n *\n * ⛔ It deliberately does NOT throw. This runs on a background timer; taking\n * a process down on a transient EACCES would be worse than the bug. The bar\n * met here is non-silence, not propagation.\n *\n * The channel is `console.error` because this class has no logger: nothing is\n * injected through `FileSystemRepositoryOptions`, and widening that public\n * surface to carry one is out of scope for this fix.\n */\n private reportResyncFault(target: string, err: unknown): void {\n const code = (err as NodeJS.ErrnoException | null)?.code ?? 'UNKNOWN';\n const key = `${code} @ ${target}`;\n if (this.resyncFaults.has(key)) return;\n this.resyncFaults.add(key);\n console.error(\n `[FileSystemRepository] metadata reconciliation sweep could not read ${target} (${code}). ` +\n `CONSEQUENCE: external edits under this path are no longer reconciled, so this ` +\n `repository's index and its watch() subscribers can drift from what is on disk while ` +\n `everything keeps reporting healthy. The chokidar watcher is not an independent ` +\n `fallback here — fd exhaustion degrades both. ` +\n `FIX: restore read access to the path; the sweep recovers by itself on the first ` +\n `successful read. Reported once per path and error code.`,\n );\n }\n\n /** Re-arm reporting for a path that reads again, so a recurrence is heard. */\n private clearResyncFault(target: string): void {\n if (this.resyncFaults.size === 0) return;\n const suffix = ` @ ${target}`;\n for (const key of this.resyncFaults) {\n if (key.endsWith(suffix)) this.resyncFaults.delete(key);\n }\n }\n\n /**\n * Content-keyed reconciliation sweep — the backstop that makes external-edit\n * detection a guarantee rather than a single chance (#9339, #7282).\n *\n * ## Why the watcher alone cannot be the guarantee\n *\n * An external write to `<root>/<type>/<name>.json` reaches a subscriber only\n * if chokidar notices it, and under `usePolling` it gets **exactly one**\n * opportunity to do so: the write advances the type directory's mtime once,\n * and chokidar re-reads a directory only when its stat *strictly advances*,\n * so every later poll compares an unchanged stat and can never rediscover\n * the file. Measured on #9339 with a fault-injection harness: with the one\n * read suppressed, fifteen further poll ticks never find the new file, and a\n * 20s deadline and a 200s deadline buy the same single attempt. That is the\n * structural reason behind #7282's empirical finding that the event is\n * \"never delivered, not slow\", and why widening the deadline (#7208) and\n * lowering `interval` were both spent before they were tried.\n *\n * At least six independent one-shot gates sit on that single attempt,\n * spanning three layers — the kernel timestamp (the directory mtime does not\n * strictly advance), chokidar's readdir throttle and readdir snapshot, and\n * chokidar's emit gates (`_throttle('add')`, a stale `_pendingWrites` entry,\n * the `awaitWriteFinish` ENOENT early return). Each one produces a\n * byte-identical observable: no event, ever, for that path.\n *\n * ## Why this shape, and not a narrower one\n *\n * ⚠️ The six are indistinguishable at the point of failure, so **any fix\n * that has to name which gate fired is a fix for one member of a family** —\n * which is exactly how #7282 was closed and exactly why it reopened. This\n * sweep never asks. It compares what is on disk against `heads`, the index\n * that already defines what this repository believes it holds, and publishes\n * the divergence through the same `handleFsChange` the watcher feeds. It is\n * therefore robust across all six *by construction*, and equally across a\n * seventh nobody has found: the only property it relies on is that the bytes\n * on disk stopped matching the index.\n *\n * `put()` is unaffected and keeps its direct registration (`trackWrittenPath`\n * calls `watcher.add` and bypasses the whole chain, which is why the `put()`\n * half of this family was already closed by #7336 and the external-write half\n * was not).\n *\n * ## Cost, and why it is bounded\n *\n * One pass over `<root>/<type>/*.json` per sweep — the same walk `start()`\n * already performs once — with no retry loop inside it and no work at all\n * when nothing diverged. Sweeps are chained, so they cannot overlap; the\n * timer is `unref`ed and dies with `close()`; and it is armed only alongside\n * the watcher, so a `disableWatch` repository pays nothing.\n *\n * Discovery is by content, never by stat: a stat pre-filter would reintroduce\n * a time key of exactly the kind this replaces.\n */\n private async resync(): Promise<void> {\n const root = this.layout.root;\n let entries: import('node:fs').Dirent[] = [];\n try {\n entries = await fs.readdir(root, { withFileTypes: true });\n this.clearResyncFault(root);\n } catch (err) {\n // ENOENT is truthful: a root that does not exist holds nothing to\n // reconcile, and the next sweep sees whatever replaces it. Any other\n // errno means the read could not RUN, and staying silent about that\n // would make this backstop absent for the life of the process exactly\n // when the load-dependent loss it exists to catch is most likely —\n // EMFILE/ENFILE degrade this read and chokidar's own polling together.\n if (!isEnoent(err)) this.reportResyncFault(root, err);\n return;\n }\n const onDisk = new Set<string>();\n /**\n * Type directories whose listing could not be obtained. Their keys are\n * missing from `onDisk` for a reason that is NOT \"the files are gone\", so\n * the delete pass below must not read that absence as a removal.\n */\n const unreadableTypes = new Set<string>();\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n // Same dot-entry rule as `scanHeads` and `isIgnoredWatchPath`, so the\n // boot scan, the watcher and this sweep agree on what the repository\n // contains (#7150).\n if (entry.name.startsWith('.')) continue;\n const dir = path.join(root, entry.name);\n let files: string[] = [];\n try {\n files = await fs.readdir(dir);\n this.clearResyncFault(dir);\n } catch (err) {\n // The same discrimination at type granularity. An unreadable type\n // directory silently stops reconciling EVERY item of that type, which\n // is exactly the invented-emptiness shape #8895 rules on.\n if (!isEnoent(err)) {\n this.reportResyncFault(dir, err);\n unreadableTypes.add(entry.name);\n }\n continue;\n }\n for (const file of files) {\n if (!file.endsWith('.json') || file.startsWith('.')) continue;\n const abs = path.join(dir, file);\n const parsed = parseItemPath(this.layout, abs);\n if (!parsed) continue;\n const ref: MetaRef = {\n org: this.org,\n type: parsed.type as MetadataType,\n name: parsed.name,\n };\n const key = refKey(ref);\n onDisk.add(key);\n const before = this.heads.get(key);\n await this.handleFsChange(abs, 'add');\n if (this.heads.get(key) !== before) {\n // We just published a change the watcher never delivered, so the\n // watcher may not know this path at all (the loss can be upstream of\n // chokidar's `_handleFile`). Re-arm it through the same seam `put()`\n // uses, so the fast path is restored instead of leaving every future\n // edit to this file dependent on the sweep.\n this.trackWrittenPath(abs);\n }\n }\n }\n for (const key of [...this.heads.keys()]) {\n if (onDisk.has(key)) continue;\n const ref = parseRefKey(key);\n if (!ref) continue;\n // Absent from `onDisk` because we could not look, not because it is gone.\n if (unreadableTypes.has(ref.type)) continue;\n const file = itemPath(this.layout, ref.type, ref.name);\n await this.mutex.run(key, async () => {\n // Re-checked UNDER the lock. The enumeration above ran outside it, so\n // a `put()` that created this file in between would otherwise be\n // reported as an external delete.\n if (existsSync(file)) return;\n await this.publishExternalDelete(ref, key);\n });\n }\n }\n\n /**\n * Translate a watcher event into a `MetadataEvent`, or drop it.\n *\n * ## Self-writes are suppressed by content identity, never by a clock (#7335)\n *\n * This used to open with `if (this.selfWrites.has(absPath)) return;` — a\n * `Set` that `put()`/`delete()` added the path to and a `setTimeout(…, 200)`\n * cleared. That check discarded **every** event for a recently-written path\n * without ever looking at what the watcher had actually observed, which is\n * the whole defect: with `usePolling`, chokidar compares state once per\n * `interval`, so our write and an external edit landing between two ticks\n * are delivered as **one** event carrying the *external* content. Dropping\n * it on a wall clock destroyed the only notification that edit would ever\n * produce.\n *\n * Measured on `origin/main` @ `69fde55`, 40 iterations, poll phase\n * randomised so the delivery lag samples `[0, interval)` uniformly:\n *\n * delivery lag < 200ms → 7 runs → external edit SWALLOWED, every time\n * delivery lag > 200ms → 33 runs → external edit delivered, every time\n *\n * A perfect split on the wall-clock boundary, and the reason earlier\n * instrumentation saw 0/360: a *fixed* pre-edit sleep phase-locks the poll,\n * pinning the lag (measured: 519–585ms across 25 runs) safely outside the\n * window. Nothing about the window was rare — it was unsampled.\n *\n * What remains is the check that was already doing the real work one step\n * down, and it needs no timer because it compares the content the watcher\n * **read** against the index:\n *\n * - `add`/`change` — `currentHead === hash` drops the event when the bytes\n * on disk are the bytes we last published. `put()` sets that head in the\n * same continuation as its `rename`, and `awaitWriteFinish` holds the\n * event for a further `stabilityThreshold`, so it is never late.\n * - `unlink` — `!currentHead` drops the event when the index already\n * agrees the item is gone. `delete()` retires the head *before* it\n * unlinks, precisely because this face gets no `awaitWriteFinish` delay.\n *\n * Both faces are pinned together in `test/self-write-suppression.test.ts`.\n *\n * Note the deliberate limit: identity is judged on what round-trips through\n * the file, so a spec whose in-memory form does not (a `Date`, which\n * canonicalises to `{}` in memory but to an ISO string once written and\n * re-read) is republished as an external `update`. That predates this change\n * and is independent of it — such a spec already fails `put().version ===\n * get().hash`, and the 200ms window never covered it either, expiring some\n * 360ms before the event it would have had to catch.\n */\n private async handleFsChange(absPath: string, kind: 'add' | 'change' | 'unlink'): Promise<void> {\n const parsed = parseItemPath(this.layout, absPath);\n if (!parsed) return;\n const ref: MetaRef = {\n org: this.org,\n type: parsed.type as MetadataType,\n name: parsed.name,\n };\n const key = refKey(ref);\n await this.mutex.run(key, async () => {\n if (kind === 'unlink') {\n await this.publishExternalDelete(ref, key);\n return;\n }\n const body = await readJson(absPath);\n if (!body) return;\n const hash = hashSpec(body);\n const currentHead = this.heads.get(key) ?? null;\n if (currentHead === hash) return; // No content change.\n this.heads.set(key, hash);\n const seq = this.nextSeq++;\n const evt: MetadataEvent = {\n seq,\n op: currentHead ? 'update' : 'create',\n ref: { ...ref, version: undefined },\n hash,\n parentHash: currentHead,\n actor: this.fsActor,\n ts: this.now().toISOString(),\n source: 'fs',\n };\n await this.log.append(evt);\n this.broker.publish(evt);\n });\n }\n}\n\n// ── Utilities ─────────────────────────────────────────────────────────\n\n/**\n * Watcher ignore matcher — \"everything under the root, except the\n * repository's own bookkeeping\" (#7150).\n *\n * chokidar hands its matcher **absolute** paths, and applies it to the\n * watched root itself as well as to entries discovered underneath it. The\n * previous matcher was a bare dotfile regex (`/(^|[\\\\/])\\../`), which\n * therefore matched the `.objectstack` segment of the root path the plugin\n * actually uses (`<project>/.objectstack/metadata`, `REPO_SUBDIR` in\n * `packages/metadata/src/plugin.ts`) and ignored the whole watch. Measured on\n * chokidar 5 with this repository's own options, two identical trees\n * differing only in whether the root sits under a dot-directory:\n *\n * plain root getWatched: ['<root>', 'view'] events: add+change\n * dot-rooted getWatched: [] events: none\n *\n * So the intent is kept and only the *frame of reference* is fixed: judge the\n * path relative to the root, so dot segments belonging to the root itself are\n * never considered.\n *\n * Why not drop the matcher entirely and lean on `parseItemPath`, which already\n * rejects `.objectstack`? Measured: `parseItemPath` rejects that ONE name, so\n * a dot-directory at the type level leaks — `<root>/.cache/x.json` parses as\n * type `.cache`, and `<root>/view/.scratch.json` as an item named `.scratch`.\n * Both would be published as `MetadataEvent`s while `scanHeads` skips every\n * dot entry on boot, leaving the boot scan and the watcher disagreeing about\n * what the repository contains. Dropping it also puts `.objectstack/.log/` in\n * the poll set, so every one of the repository's own log appends wakes\n * `handleFsChange` only to be discarded.\n */\nfunction isIgnoredWatchPath(root: string, absPath: string): boolean {\n const rel = path.relative(root, absPath);\n // The watched root itself, and anything outside it, are not ours to judge.\n if (rel === '' || rel.startsWith('..')) return false;\n return rel.split(/[\\\\/]/).some((segment) => segment.startsWith('.'));\n}\n\nasync function readJson(file: string): Promise<unknown | null> {\n try {\n const text = await fs.readFile(file, 'utf8');\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nasync function writeJsonAtomic(file: string, body: unknown): Promise<void> {\n const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;\n await fs.writeFile(tmp, JSON.stringify(body, null, 2) + '\\n', 'utf8');\n await fs.rename(tmp, file);\n}\n\nfunction parseRefKey(key: string): MetaRef | null {\n const parts = key.split('/');\n if (parts.length !== 3) return null;\n return {\n org: parts[0]!,\n type: parts[1]! as MetadataType,\n name: parts[2]!,\n };\n}\n\n/**\n * Wrap a Promise<AsyncIterable<T>> as a sync-returning AsyncIterable<T>.\n * The first `.next()` awaits the promise.\n */\nfunction deferredIterable<T>(promise: Promise<AsyncIterable<T>>): AsyncIterable<T> {\n return {\n [Symbol.asyncIterator]() {\n let inner: AsyncIterator<T> | null = null;\n return {\n async next() {\n if (!inner) {\n const iterable = await promise;\n inner = iterable[Symbol.asyncIterator]();\n }\n return inner.next();\n },\n async return(value?: unknown) {\n if (!inner) {\n const iterable = await promise;\n inner = iterable[Symbol.asyncIterator]();\n }\n if (inner.return) return inner.return(value);\n return { value: undefined, done: true };\n },\n } as AsyncIterator<T>;\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Disk layout helpers — see ADR-0008 §10 PR-4 / packages/metadata-fs README.\n *\n * <root>/<type>/<name>.json — canonical body\n * <root>/.objectstack/.log/main.jsonl — append-only change log\n */\n\nimport path from 'node:path';\nimport type { MetadataType } from '@objectstack/metadata-core';\n\nexport interface FsLayout {\n /** Absolute path to the metadata root. */\n root: string;\n}\n\nexport function itemPath(layout: FsLayout, type: MetadataType, name: string): string {\n return path.join(layout.root, type, `${name}.json`);\n}\n\nexport function typeDir(layout: FsLayout, type: MetadataType): string {\n return path.join(layout.root, type);\n}\n\nexport function logDir(layout: FsLayout): string {\n return path.join(layout.root, '.objectstack', '.log');\n}\n\nexport function logFile(layout: FsLayout): string {\n // Single change log per filesystem root (branching is a Git concern,\n // not a metadata-layer concern).\n return path.join(logDir(layout), `main.jsonl`);\n}\n\n/** Parse a path like \".../view/case_grid.json\" into {type, name}. */\nexport function parseItemPath(\n layout: FsLayout,\n absPath: string,\n): { type: string; name: string } | null {\n const rel = path.relative(layout.root, absPath);\n if (rel.startsWith('..') || rel.startsWith('.objectstack')) return null;\n const segments = rel.split(path.sep);\n if (segments.length !== 2) return null;\n const type = segments[0]!;\n const file = segments[1]!;\n if (!file.endsWith('.json')) return null;\n const name = file.slice(0, -'.json'.length);\n return { type, name };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Append-only JSONL change log writer / reader. Each line is a single\n * `MetadataEvent` serialized via `JSON.stringify`.\n *\n * Durability strategy\n * ───────────────────\n * - Append with `O_APPEND` semantics (Node's `fs.appendFile` is\n * atomic for sub-PIPE_BUF-sized writes; events are well under 4 KiB).\n * - Read by streaming the file line-by-line and JSON.parse-ing each.\n * - On a corrupt line we skip and continue — the body files are the\n * source of truth; the log is a denormalised history index.\n */\n\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport readline from 'node:readline';\nimport { createReadStream, existsSync } from 'node:fs';\nimport type { MetadataEvent } from '@objectstack/metadata-core';\n\nexport class JsonlLog {\n constructor(private readonly file: string) {}\n\n async append(evt: MetadataEvent): Promise<void> {\n await fs.mkdir(path.dirname(this.file), { recursive: true });\n await fs.appendFile(this.file, JSON.stringify(evt) + '\\n', 'utf8');\n }\n\n /** Read all events in seq order (i.e. file order). */\n async *readAll(): AsyncIterable<MetadataEvent> {\n if (!existsSync(this.file)) return;\n const rl = readline.createInterface({\n input: createReadStream(this.file, { encoding: 'utf8' }),\n crlfDelay: Infinity,\n });\n try {\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n yield JSON.parse(line) as MetadataEvent;\n } catch {\n // Skip corrupt line.\n }\n }\n } finally {\n rl.close();\n }\n }\n\n /** Return the highest seq number in the log, or 0 if empty. */\n async highestSeq(): Promise<number> {\n let max = 0;\n for await (const evt of this.readAll()) {\n if (typeof evt.seq === 'number' && evt.seq > max) max = evt.seq;\n }\n return max;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Mutex / event-broker primitives used by FileSystemRepository.\n *\n * `KeyedMutex` serializes operations on the same key (refKey). The\n * broker re-uses the same manual-AsyncIterator pattern as\n * InMemoryRepository so that consumer `return()` reliably unblocks.\n */\n\nimport type { MetadataEvent, WatchFilter } from '@objectstack/metadata-core';\n\nexport class KeyedMutex {\n private readonly tails = new Map<string, Promise<unknown>>();\n\n async run<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const prev = this.tails.get(key) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n // Save the swallowed-error tail so successive runs don't reject on\n // an unrelated prior failure.\n const swallowed = next.catch(() => undefined);\n this.tails.set(key, swallowed);\n try {\n return await next;\n } finally {\n // Best-effort cleanup: drop the entry if nothing newer was queued.\n if (this.tails.get(key) === swallowed) {\n this.tails.delete(key);\n }\n }\n }\n}\n\nexport interface BrokerSubscriber {\n filter: WatchFilter;\n closed: boolean;\n push(evt: MetadataEvent): void;\n}\n\nexport interface EventBroker {\n subscribe(sub: BrokerSubscriber): void;\n unsubscribe(sub: BrokerSubscriber): void;\n publish(evt: MetadataEvent): void;\n}\n\nexport function createBroker(matches: (evt: MetadataEvent, filter: WatchFilter) => boolean): EventBroker {\n const subs = new Set<BrokerSubscriber>();\n return {\n subscribe: (s) => { subs.add(s); },\n unsubscribe: (s) => { subs.delete(s); },\n publish: (evt) => {\n for (const s of subs) {\n if (s.closed) continue;\n if (!matches(evt, s.filter)) continue;\n s.push(evt);\n }\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Manual `AsyncIterator` factory for `repo.watch()`. Mirrors the\n * pattern used in `@objectstack/metadata-core`'s `InMemoryRepository`:\n * async generators do NOT run `finally` when paused on an unresolved\n * `await`, so we cannot use them to implement `watch()`.\n */\n\nimport type { MetadataEvent, WatchFilter } from '@objectstack/metadata-core';\nimport { type EventBroker, type BrokerSubscriber } from './sync.js';\n\nexport interface CreateWatchIteratorArgs {\n filter: WatchFilter;\n since: number | undefined;\n replay: MetadataEvent[];\n broker: EventBroker;\n /** Returns true if `evt.ref` matches `filter`. */\n matches: (evt: MetadataEvent, filter: WatchFilter) => boolean;\n branchKeyOf: (evt: MetadataEvent) => string;\n}\n\nexport function createWatchIterable(\n args: CreateWatchIteratorArgs,\n): AsyncIterable<MetadataEvent> {\n const queue: MetadataEvent[] = [];\n let waiter: ((evt: IteratorResult<MetadataEvent>) => void) | null = null;\n let closed = false;\n const delivered = new Set<string>();\n const evtKey = (e: MetadataEvent) => `${args.branchKeyOf(e)}#${e.seq}`;\n\n const subscriber: BrokerSubscriber = {\n filter: args.filter,\n closed: false,\n push: (evt) => {\n if (subscriber.closed) return;\n const k = evtKey(evt);\n if (delivered.has(k)) return;\n if (waiter) {\n delivered.add(k);\n const w = waiter;\n waiter = null;\n w({ value: clone(evt), done: false });\n } else {\n queue.push(evt);\n }\n },\n };\n args.broker.subscribe(subscriber);\n\n const replay = [...args.replay].sort((a, b) => a.seq - b.seq);\n let replayIdx = 0;\n\n const drain = (): IteratorResult<MetadataEvent> | null => {\n while (replayIdx < replay.length) {\n const evt = replay[replayIdx++]!;\n if (typeof args.since === 'number' && evt.seq <= args.since) continue;\n const k = evtKey(evt);\n if (delivered.has(k)) continue;\n delivered.add(k);\n return { value: clone(evt), done: false };\n }\n while (queue.length > 0) {\n const evt = queue.shift()!;\n const k = evtKey(evt);\n if (delivered.has(k)) continue;\n delivered.add(k);\n return { value: clone(evt), done: false };\n }\n return null;\n };\n\n const close = (): IteratorResult<MetadataEvent> => {\n if (!closed) {\n closed = true;\n subscriber.closed = true;\n args.broker.unsubscribe(subscriber);\n if (waiter) {\n const w = waiter;\n waiter = null;\n w({ value: undefined, done: true });\n }\n }\n return { value: undefined, done: true };\n };\n\n const iterator: AsyncIterator<MetadataEvent> = {\n next: () => {\n if (closed) return Promise.resolve({ value: undefined, done: true });\n const immediate = drain();\n if (immediate) return Promise.resolve(immediate);\n return new Promise<IteratorResult<MetadataEvent>>((resolve) => {\n waiter = resolve;\n });\n },\n return: () => Promise.resolve(close()),\n throw: (err) => {\n close();\n return Promise.reject(err);\n },\n };\n return { [Symbol.asyncIterator]: () => iterator };\n}\n\nfunction clone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n"],"mappings":"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","names":["import_promises","import_node_fs","import_node_path","path","import_node_path","fs","path","readline","path","fs","chokidar"]}
package/dist/index.d.cts CHANGED
@@ -25,10 +25,18 @@ declare class FileSystemRepository implements MetadataRepository {
25
25
  private readonly heads;
26
26
  /** Next seq counter, hydrated from the log on `start()`. */
27
27
  private nextSeq;
28
- /** Paths we wrote ourselves; suppress the resulting chokidar event. */
29
- private readonly selfWrites;
30
28
  private watcher;
31
29
  private started;
30
+ /** Pending reconciliation sweep (#9339). Chained, never overlapping. */
31
+ private resyncTimer;
32
+ /** False before the watcher is armed and from `close()` onwards. */
33
+ private resyncEnabled;
34
+ /**
35
+ * Sweep read faults already reported, keyed `CODE @ path`, so a standing
36
+ * fault is announced once rather than every 2s (AGENTS.md: say it once, at
37
+ * the first degradation). An entry is cleared when that path reads again.
38
+ */
39
+ private readonly resyncFaults;
32
40
  constructor(opts: FileSystemRepositoryOptions);
33
41
  /**
34
42
  * Attach the repository. **Creates nothing on disk** (#7000).
@@ -110,6 +118,170 @@ declare class FileSystemRepository implements MetadataRepository {
110
118
  */
111
119
  private trackWrittenPath;
112
120
  private startWatcher;
121
+ /**
122
+ * Publish the `delete` face of an externally-observed removal.
123
+ *
124
+ * Extracted from `handleFsChange` unchanged so the reconciliation sweep
125
+ * (#9339) can reuse it **verbatim** rather than growing a second copy of the
126
+ * event shape. The one-line invariant: the caller already holds the per-key
127
+ * mutex, and `!currentHead` is the content-keyed suppression that makes our
128
+ * own `delete()` a no-op here.
129
+ */
130
+ private publishExternalDelete;
131
+ private startResync;
132
+ private stopResync;
133
+ /**
134
+ * Schedule the next sweep — chained, never `setInterval` (#9339).
135
+ *
136
+ * A chained timeout cannot stack: the next sweep is armed only once the
137
+ * previous one has finished, so a saturated runner degrades to *fewer*
138
+ * sweeps instead of a growing backlog of overlapping tree walks. The timer
139
+ * is `unref`ed because a backstop must never be the reason a process stays
140
+ * alive.
141
+ */
142
+ private scheduleResync;
143
+ /**
144
+ * Announce a sweep read that could not run — the non-silence half of #8895's
145
+ * "discriminate or propagate".
146
+ *
147
+ * ## Why `error` and not `warn`
148
+ *
149
+ * AGENTS.md decides the level with one question: *after the degradation, does
150
+ * the system still look "normal" from the outside while something it claims
151
+ * is persisted has not actually landed?* Here it does. Nothing throws, the
152
+ * watcher stays armed, `getWatched()` stays populated, `start()` succeeded —
153
+ * and the repository's index quietly stops tracking what is on disk. That is
154
+ * the rule's second limb verbatim ("persisted state and runtime state
155
+ * disagree"), not the functional-degradation limb: no capability is visibly
156
+ * smaller, so nobody finds out by using the missing thing.
157
+ *
158
+ * The counter-argument — *this is only a backstop, the watcher is still the
159
+ * fast path* — is why the level is arguable, and it does not survive the
160
+ * failing errno. The sharp case is fd exhaustion: EMFILE/ENFILE break this
161
+ * `readdir` and chokidar's `fs.watchFile` polling **at the same time and for
162
+ * the same reason**, so the fast path is not an independent fallback under
163
+ * precisely the load that produces this fault. A backstop that is silently
164
+ * absent whenever it is most needed is a durability-shaped degradation.
165
+ *
166
+ * ⚠️ AGENTS.md also warns against over-applying `error`, and the discipline
167
+ * that answers it is the ledger, not a quieter level: an `error` owes the
168
+ * consequence and the fix, said **once** at the first degradation rather than
169
+ * once per failed read. A sweep runs every 2s forever, so an unlatched
170
+ * `console.error` here would be the mirror-image failure the same rule names.
171
+ *
172
+ * ⛔ It deliberately does NOT throw. This runs on a background timer; taking
173
+ * a process down on a transient EACCES would be worse than the bug. The bar
174
+ * met here is non-silence, not propagation.
175
+ *
176
+ * The channel is `console.error` because this class has no logger: nothing is
177
+ * injected through `FileSystemRepositoryOptions`, and widening that public
178
+ * surface to carry one is out of scope for this fix.
179
+ */
180
+ private reportResyncFault;
181
+ /** Re-arm reporting for a path that reads again, so a recurrence is heard. */
182
+ private clearResyncFault;
183
+ /**
184
+ * Content-keyed reconciliation sweep — the backstop that makes external-edit
185
+ * detection a guarantee rather than a single chance (#9339, #7282).
186
+ *
187
+ * ## Why the watcher alone cannot be the guarantee
188
+ *
189
+ * An external write to `<root>/<type>/<name>.json` reaches a subscriber only
190
+ * if chokidar notices it, and under `usePolling` it gets **exactly one**
191
+ * opportunity to do so: the write advances the type directory's mtime once,
192
+ * and chokidar re-reads a directory only when its stat *strictly advances*,
193
+ * so every later poll compares an unchanged stat and can never rediscover
194
+ * the file. Measured on #9339 with a fault-injection harness: with the one
195
+ * read suppressed, fifteen further poll ticks never find the new file, and a
196
+ * 20s deadline and a 200s deadline buy the same single attempt. That is the
197
+ * structural reason behind #7282's empirical finding that the event is
198
+ * "never delivered, not slow", and why widening the deadline (#7208) and
199
+ * lowering `interval` were both spent before they were tried.
200
+ *
201
+ * At least six independent one-shot gates sit on that single attempt,
202
+ * spanning three layers — the kernel timestamp (the directory mtime does not
203
+ * strictly advance), chokidar's readdir throttle and readdir snapshot, and
204
+ * chokidar's emit gates (`_throttle('add')`, a stale `_pendingWrites` entry,
205
+ * the `awaitWriteFinish` ENOENT early return). Each one produces a
206
+ * byte-identical observable: no event, ever, for that path.
207
+ *
208
+ * ## Why this shape, and not a narrower one
209
+ *
210
+ * ⚠️ The six are indistinguishable at the point of failure, so **any fix
211
+ * that has to name which gate fired is a fix for one member of a family** —
212
+ * which is exactly how #7282 was closed and exactly why it reopened. This
213
+ * sweep never asks. It compares what is on disk against `heads`, the index
214
+ * that already defines what this repository believes it holds, and publishes
215
+ * the divergence through the same `handleFsChange` the watcher feeds. It is
216
+ * therefore robust across all six *by construction*, and equally across a
217
+ * seventh nobody has found: the only property it relies on is that the bytes
218
+ * on disk stopped matching the index.
219
+ *
220
+ * `put()` is unaffected and keeps its direct registration (`trackWrittenPath`
221
+ * calls `watcher.add` and bypasses the whole chain, which is why the `put()`
222
+ * half of this family was already closed by #7336 and the external-write half
223
+ * was not).
224
+ *
225
+ * ## Cost, and why it is bounded
226
+ *
227
+ * One pass over `<root>/<type>/*.json` per sweep — the same walk `start()`
228
+ * already performs once — with no retry loop inside it and no work at all
229
+ * when nothing diverged. Sweeps are chained, so they cannot overlap; the
230
+ * timer is `unref`ed and dies with `close()`; and it is armed only alongside
231
+ * the watcher, so a `disableWatch` repository pays nothing.
232
+ *
233
+ * Discovery is by content, never by stat: a stat pre-filter would reintroduce
234
+ * a time key of exactly the kind this replaces.
235
+ */
236
+ private resync;
237
+ /**
238
+ * Translate a watcher event into a `MetadataEvent`, or drop it.
239
+ *
240
+ * ## Self-writes are suppressed by content identity, never by a clock (#7335)
241
+ *
242
+ * This used to open with `if (this.selfWrites.has(absPath)) return;` — a
243
+ * `Set` that `put()`/`delete()` added the path to and a `setTimeout(…, 200)`
244
+ * cleared. That check discarded **every** event for a recently-written path
245
+ * without ever looking at what the watcher had actually observed, which is
246
+ * the whole defect: with `usePolling`, chokidar compares state once per
247
+ * `interval`, so our write and an external edit landing between two ticks
248
+ * are delivered as **one** event carrying the *external* content. Dropping
249
+ * it on a wall clock destroyed the only notification that edit would ever
250
+ * produce.
251
+ *
252
+ * Measured on `origin/main` @ `69fde55`, 40 iterations, poll phase
253
+ * randomised so the delivery lag samples `[0, interval)` uniformly:
254
+ *
255
+ * delivery lag < 200ms → 7 runs → external edit SWALLOWED, every time
256
+ * delivery lag > 200ms → 33 runs → external edit delivered, every time
257
+ *
258
+ * A perfect split on the wall-clock boundary, and the reason earlier
259
+ * instrumentation saw 0/360: a *fixed* pre-edit sleep phase-locks the poll,
260
+ * pinning the lag (measured: 519–585ms across 25 runs) safely outside the
261
+ * window. Nothing about the window was rare — it was unsampled.
262
+ *
263
+ * What remains is the check that was already doing the real work one step
264
+ * down, and it needs no timer because it compares the content the watcher
265
+ * **read** against the index:
266
+ *
267
+ * - `add`/`change` — `currentHead === hash` drops the event when the bytes
268
+ * on disk are the bytes we last published. `put()` sets that head in the
269
+ * same continuation as its `rename`, and `awaitWriteFinish` holds the
270
+ * event for a further `stabilityThreshold`, so it is never late.
271
+ * - `unlink` — `!currentHead` drops the event when the index already
272
+ * agrees the item is gone. `delete()` retires the head *before* it
273
+ * unlinks, precisely because this face gets no `awaitWriteFinish` delay.
274
+ *
275
+ * Both faces are pinned together in `test/self-write-suppression.test.ts`.
276
+ *
277
+ * Note the deliberate limit: identity is judged on what round-trips through
278
+ * the file, so a spec whose in-memory form does not (a `Date`, which
279
+ * canonicalises to `{}` in memory but to an ISO string once written and
280
+ * re-read) is republished as an external `update`. That predates this change
281
+ * and is independent of it — such a spec already fails `put().version ===
282
+ * get().hash`, and the 200ms window never covered it either, expiring some
283
+ * 360ms before the event it would have had to catch.
284
+ */
113
285
  private handleFsChange;
114
286
  }
115
287