@objectstack/metadata-fs 17.0.0-rc.4 → 17.0.0-rc.6
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/CHANGELOG.md +85 -0
- package/README.md +16 -1
- package/dist/index.cjs +89 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +89 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @objectstack/metadata-fs
|
|
2
2
|
|
|
3
|
+
## 17.0.0-rc.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a1b66ef: `FileSystemRepository` no longer creates its root directory when it is attached — only when it first writes.
|
|
8
|
+
|
|
9
|
+
`start()` used to `mkdir` both `<root>` and `<root>/.objectstack/.log` unconditionally, so merely attaching a repository was a write. Because `MetadataPlugin` attaches one at `<project>/.objectstack/metadata` during every boot, a command that never writes metadata still brought a directory skeleton into existence. The loudest case is `os migrate plan`, a declared dry run: on a project that had never been started it left
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.objectstack/metadata/.objectstack/.log
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
behind, which also destroyed the one signal — does `.objectstack/` exist? — by which the next command can tell a fresh project from a started one. This is the filesystem half of the same property the database half already covers: a dry run leaves nothing behind.
|
|
16
|
+
|
|
17
|
+
Attaching and reading a repository whose root does not exist is now explicitly supported and answers as an empty repository (`get`, `getByHash`, `list`, `history`, `watch`). The root, the type directories and the JSONL change log all appear on the first `put` / `delete`, and nothing about the boot's read-only character changes: no metadata is written that was not written before.
|
|
18
|
+
|
|
19
|
+
One behavioural note for direct users of the package: when the root is absent at `start()`, the chokidar watcher is armed by the first write instead, because chokidar cannot watch a path that does not yet exist. A root brought into existence by a third party while the process runs — with this repository never writing — is therefore not picked up until the next `start()`.
|
|
20
|
+
|
|
21
|
+
- ab07b53: fix(metadata-fs): register every written path with the watcher, so an item created while chokidar is still scanning is not invisible forever (#7282)
|
|
22
|
+
|
|
23
|
+
`FileSystemRepository`'s watcher could go **permanently blind to a single
|
|
24
|
+
item** — external edits to that file produced no `MetadataEvent` for the whole
|
|
25
|
+
life of the process, and nothing recovered short of a restart. The window is a
|
|
26
|
+
race between chokidar's asynchronous initial scan and the repository's own
|
|
27
|
+
first write, and both `start()` (which arms the watcher, after which the caller
|
|
28
|
+
may `put()` on the next tick) and `ensureRoot()` (which arms it in the middle
|
|
29
|
+
of the very first write, #7000) can open it.
|
|
30
|
+
|
|
31
|
+
Measured on chokidar 5 with this repository's options (`usePolling`,
|
|
32
|
+
`interval: 1000`):
|
|
33
|
+
|
|
34
|
+
1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic `rename` in
|
|
35
|
+
`writeJsonAtomic` has not landed yet;
|
|
36
|
+
2. the rename lands, changing the directory's mtime;
|
|
37
|
+
3. chokidar calls `watchFile()` on that directory and libuv takes its polling
|
|
38
|
+
baseline stat, which already reflects step 2.
|
|
39
|
+
|
|
40
|
+
The directory's stat then never changes again, so no poll ever fires for it,
|
|
41
|
+
the directory is never re-read, the item file is never added to the watched
|
|
42
|
+
set, and no per-file watcher is created. `getWatched()` reports the type
|
|
43
|
+
directory as `[]` while the file sits in it, and neither `add` nor `change` is
|
|
44
|
+
ever emitted for that path.
|
|
45
|
+
|
|
46
|
+
The fix does not widen any timer. The only writer that can be inside that
|
|
47
|
+
window is the repository itself, so `put()` now tells the watcher explicitly
|
|
48
|
+
about the path it created instead of depending on a directory scan that may
|
|
49
|
+
never notice it. Registration is idempotent and emits nothing.
|
|
50
|
+
|
|
51
|
+
User-visible effect: `MetadataManager.subscribe()` (and every consumer of
|
|
52
|
+
`repo.watch()`) now reliably sees out-of-process edits — a hand edit, or a
|
|
53
|
+
`git checkout` bringing metadata JSON in — to items written earlier in the same
|
|
54
|
+
process. This was also the cause of four merge-queue ejections across three
|
|
55
|
+
PRs; the two time-based mitigations tried before it (a 20s/25s event deadline
|
|
56
|
+
and a wider pre-edit sleep) could not have worked, because the event was never
|
|
57
|
+
delivered rather than late.
|
|
58
|
+
|
|
59
|
+
- 684ab22: fix(metadata-fs): the `FileSystemRepository` watcher now sees external edits in the production layout
|
|
60
|
+
|
|
61
|
+
`MetadataPlugin` attaches the repository at `<project>/.objectstack/metadata`, and the
|
|
62
|
+
watcher's `ignored` matcher was a bare dotfile regex. chokidar applies that matcher to the
|
|
63
|
+
watched root path itself, not only to entries found underneath it, so the `.objectstack`
|
|
64
|
+
segment of the root matched and the entire watch was inert — `getWatched()` returned `{}`
|
|
65
|
+
and no event ever fired. Hand edits, a `git checkout` that brings metadata JSON in, and any
|
|
66
|
+
other out-of-process writer under `.objectstack/metadata/` were invisible until the next
|
|
67
|
+
`start()`, even though `MetadataManager.setRepository()` is wired to those events and uses
|
|
68
|
+
them to invalidate the registry and the `list()` cache.
|
|
69
|
+
|
|
70
|
+
The matcher is now evaluated against the path _relative_ to the watch root, so dot segments
|
|
71
|
+
belonging to the root itself are never considered while dotfiles under the root — including
|
|
72
|
+
the repository's own `.objectstack/` bookkeeping subtree — stay ignored as before.
|
|
73
|
+
|
|
74
|
+
- Updated dependencies [121852d]
|
|
75
|
+
- Updated dependencies [5e247fd]
|
|
76
|
+
- Updated dependencies [1a53a02]
|
|
77
|
+
- Updated dependencies [a954634]
|
|
78
|
+
- Updated dependencies [3d4c545]
|
|
79
|
+
- Updated dependencies [bb7cb41]
|
|
80
|
+
- @objectstack/metadata-core@17.0.0-rc.6
|
|
81
|
+
|
|
82
|
+
## 17.0.0-rc.5
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- @objectstack/metadata-core@17.0.0-rc.5
|
|
87
|
+
|
|
3
88
|
## 17.0.0-rc.4
|
|
4
89
|
|
|
5
90
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ const repo = new FileSystemRepository({
|
|
|
35
35
|
root: './metadata',
|
|
36
36
|
org: 'system',
|
|
37
37
|
});
|
|
38
|
-
await repo.start(); // scan + open watcher
|
|
38
|
+
await repo.start(); // scan + open watcher — creates nothing on disk
|
|
39
39
|
|
|
40
40
|
const view = await repo.get({
|
|
41
41
|
org: 'system',
|
|
@@ -47,4 +47,19 @@ for await (const evt of repo.watch({})) {
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Root creation is a write, not an attach
|
|
51
|
+
|
|
52
|
+
`start()` never creates `<root>`. Attaching a repository whose root does not
|
|
53
|
+
exist is legal: reads answer as if the repository were empty, and the root —
|
|
54
|
+
together with `<root>/.objectstack/.log/` — appears on the **first write**
|
|
55
|
+
(`put` / `delete`). This is what keeps a read-only boot, such as the dry run
|
|
56
|
+
`os migrate plan` performs on a project that has never been started, from
|
|
57
|
+
leaving a directory skeleton behind (#7000, the filesystem half of #6743).
|
|
58
|
+
|
|
59
|
+
One consequence worth knowing: when the root is absent at `start()`, the
|
|
60
|
+
chokidar watcher is armed by the first write instead, because chokidar cannot
|
|
61
|
+
watch a path that does not exist yet. A root brought into existence by a third
|
|
62
|
+
party while the process runs, without this repository ever writing, is
|
|
63
|
+
therefore not picked up until the next `start()`.
|
|
64
|
+
|
|
50
65
|
See ADR-0008 (incl. §0 amendment) and the `metadata-branch-removal` changeset.
|
package/dist/index.cjs
CHANGED
|
@@ -255,15 +255,42 @@ var FileSystemRepository = class {
|
|
|
255
255
|
this.log = new JsonlLog(logFile(this.layout));
|
|
256
256
|
}
|
|
257
257
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Attach the repository. **Creates nothing on disk** (#7000).
|
|
260
|
+
*
|
|
261
|
+
* Attaching is not a write. `start()` used to `mkdir` both the root and
|
|
262
|
+
* `<root>/.objectstack/.log` unconditionally, which meant every read-only
|
|
263
|
+
* boot that merely attaches a repository left a skeleton behind — most
|
|
264
|
+
* visibly `os migrate plan`, a declared dry run, on a project that has
|
|
265
|
+
* never been started. That is the same property #6743 ruled on for
|
|
266
|
+
* `.objectstack/data/`: a dry run leaves nothing behind, and the existence
|
|
267
|
+
* of `.objectstack/` has to stay a usable "this project has been started"
|
|
268
|
+
* signal.
|
|
269
|
+
*
|
|
270
|
+
* Every read path below already treats a missing root as an empty
|
|
271
|
+
* repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on
|
|
272
|
+
* `existsSync`, `get` guards on `existsSync`), so the root is materialized
|
|
273
|
+
* by `ensureRoot()` on the first write instead.
|
|
274
|
+
*/
|
|
258
275
|
async start() {
|
|
259
276
|
if (this.started) return;
|
|
260
277
|
this.started = true;
|
|
261
|
-
await import_promises2.default.mkdir(this.layout.root, { recursive: true });
|
|
262
|
-
await import_promises2.default.mkdir(logDir(this.layout), { recursive: true });
|
|
263
278
|
await this.scanHeads();
|
|
264
279
|
const highest = await this.log.highestSeq();
|
|
265
280
|
this.nextSeq = highest + 1;
|
|
266
|
-
if (!this.disableWatch) this.startWatcher();
|
|
281
|
+
if (!this.disableWatch && (0, import_node_fs2.existsSync)(this.layout.root)) this.startWatcher();
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Bring the repository root into existence. Called by every write path
|
|
285
|
+
* immediately before it touches the disk — `start()` deliberately does not
|
|
286
|
+
* create it (#7000), so this is the single seam where the root appears.
|
|
287
|
+
*
|
|
288
|
+
* It is also where a watcher that `start()` could not arm (missing root)
|
|
289
|
+
* gets armed, so "external edits are detected" survives the change.
|
|
290
|
+
*/
|
|
291
|
+
async ensureRoot() {
|
|
292
|
+
await import_promises2.default.mkdir(this.layout.root, { recursive: true });
|
|
293
|
+
if (this.started && !this.disableWatch && !this.watcher) this.startWatcher();
|
|
267
294
|
}
|
|
268
295
|
async close() {
|
|
269
296
|
if (this.watcher) {
|
|
@@ -381,6 +408,7 @@ var FileSystemRepository = class {
|
|
|
381
408
|
const seq = this.nextSeq++;
|
|
382
409
|
const ts = this.now().toISOString();
|
|
383
410
|
const file = itemPath(this.layout, ref.type, ref.name);
|
|
411
|
+
await this.ensureRoot();
|
|
384
412
|
await import_promises2.default.mkdir(typeDir(this.layout, ref.type), { recursive: true });
|
|
385
413
|
this.selfWrites.add(file);
|
|
386
414
|
try {
|
|
@@ -388,6 +416,7 @@ var FileSystemRepository = class {
|
|
|
388
416
|
} finally {
|
|
389
417
|
setTimeout(() => this.selfWrites.delete(file), 200);
|
|
390
418
|
}
|
|
419
|
+
this.trackWrittenPath(file);
|
|
391
420
|
this.heads.set(key, hash);
|
|
392
421
|
const evt = {
|
|
393
422
|
seq,
|
|
@@ -427,6 +456,7 @@ var FileSystemRepository = class {
|
|
|
427
456
|
throw new import_metadata_core.ConflictError(ref, opts.parentVersion, currentHead);
|
|
428
457
|
}
|
|
429
458
|
const file = itemPath(this.layout, ref.type, ref.name);
|
|
459
|
+
await this.ensureRoot();
|
|
430
460
|
this.selfWrites.add(file);
|
|
431
461
|
try {
|
|
432
462
|
if ((0, import_node_fs2.existsSync)(file)) await import_promises2.default.unlink(file);
|
|
@@ -502,10 +532,58 @@ var FileSystemRepository = class {
|
|
|
502
532
|
}
|
|
503
533
|
return last;
|
|
504
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Register a path this repository just wrote with the watcher (#7282).
|
|
537
|
+
*
|
|
538
|
+
* chokidar's initial scan is asynchronous, and every write path here can be
|
|
539
|
+
* running **while it is still walking the tree** — `start()` arms the watcher
|
|
540
|
+
* and the caller may `put()` on the next tick, and `ensureRoot()` arms it in
|
|
541
|
+
* the middle of the very first write. With `usePolling` that combination has
|
|
542
|
+
* a permanently-blinding interleaving, measured on chokidar 5 with this
|
|
543
|
+
* repository's own options:
|
|
544
|
+
*
|
|
545
|
+
* 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic
|
|
546
|
+
* `rename` in `writeJsonAtomic` has not landed yet.
|
|
547
|
+
* 2. the rename lands; the directory's mtime changes.
|
|
548
|
+
* 3. chokidar calls `watchFile()` on that directory, and libuv takes its
|
|
549
|
+
* polling baseline stat — which already reflects step 2.
|
|
550
|
+
*
|
|
551
|
+
* From then on the directory's stat never changes again, so no poll ever
|
|
552
|
+
* fires for it, `_handleRead` never re-runs, the item file is never added to
|
|
553
|
+
* the watched set, and no per-file watcher is ever created. chokidar emits
|
|
554
|
+
* neither `add` nor `change` for that path **for the life of the process** —
|
|
555
|
+
* `getWatched()` reports the type directory as `[]` forever while the file
|
|
556
|
+
* sits in it. That is the whole of #7282: the four merge-queue ejections all
|
|
557
|
+
* waited out their deadlines (20s, then 25541ms against 25s) on an event that
|
|
558
|
+
* was never going to be delivered, which is why widening the deadline and
|
|
559
|
+
* widening the pre-edit sleep both changed nothing, and why lowering
|
|
560
|
+
* `interval` would change nothing either — a shorter poll re-compares against
|
|
561
|
+
* the same unchanged directory stat.
|
|
562
|
+
*
|
|
563
|
+
* The window is exactly "files that exist at baseline time but were absent
|
|
564
|
+
* from the snapshot read a moment earlier", and the only writer that can be
|
|
565
|
+
* inside it is us. So we close it at the source: tell the watcher explicitly
|
|
566
|
+
* about every path we create, instead of hoping its scan happened to see it.
|
|
567
|
+
*
|
|
568
|
+
* `add()` is idempotent here — `_handleFile` returns early when the parent
|
|
569
|
+
* directory already tracks the basename — and it emits nothing, because
|
|
570
|
+
* chokidar treats an explicit `add()` as an initial add and `ignoreInitial`
|
|
571
|
+
* is set. Its effect is the one we need: `_watchWithNodeFs` registers the
|
|
572
|
+
* basename with the parent directory (without which chokidar drops `change`
|
|
573
|
+
* events for the file) and starts the per-file poll.
|
|
574
|
+
*/
|
|
575
|
+
trackWrittenPath(file) {
|
|
576
|
+
const w = this.watcher;
|
|
577
|
+
if (!w || w.closed) return;
|
|
578
|
+
w.add(file);
|
|
579
|
+
}
|
|
505
580
|
startWatcher() {
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
//
|
|
581
|
+
const root = this.layout.root;
|
|
582
|
+
const w = import_chokidar.default.watch(root, {
|
|
583
|
+
// Skip dotfiles under the root — including the repository's own
|
|
584
|
+
// `.objectstack/` bookkeeping subtree — matched on the path RELATIVE
|
|
585
|
+
// to the watch root (#7150). See `isIgnoredWatchPath`.
|
|
586
|
+
ignored: (p) => isIgnoredWatchPath(root, p),
|
|
509
587
|
ignoreInitial: true,
|
|
510
588
|
depth: 2,
|
|
511
589
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
|
|
@@ -573,6 +651,11 @@ var FileSystemRepository = class {
|
|
|
573
651
|
});
|
|
574
652
|
}
|
|
575
653
|
};
|
|
654
|
+
function isIgnoredWatchPath(root, absPath) {
|
|
655
|
+
const rel = import_node_path3.default.relative(root, absPath);
|
|
656
|
+
if (rel === "" || rel.startsWith("..")) return false;
|
|
657
|
+
return rel.split(/[\\/]/).some((segment) => segment.startsWith("."));
|
|
658
|
+
}
|
|
576
659
|
async function readJson(file) {
|
|
577
660
|
try {
|
|
578
661
|
const text = await import_promises2.default.readFile(file, "utf8");
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 */\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 logDir,\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 async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n await fs.mkdir(this.layout.root, { recursive: true });\n await fs.mkdir(logDir(this.layout), { recursive: true });\n\n // 1) Scan body files to build the head index.\n await this.scanHeads();\n\n // 2) Hydrate nextSeq from the existing log.\n const highest = await this.log.highestSeq();\n this.nextSeq = highest + 1;\n\n // 3) Start the watcher (unless disabled).\n if (!this.disableWatch) 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 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 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 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 private startWatcher(): void {\n const w = chokidar.watch(this.layout.root, {\n ignored: [/(^|[\\\\/])\\../], // skip dotfiles incl. .objectstack\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\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\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuBA,IAAAA,mBAAe;AACf,IAAAC,kBAA2B;AAC3B,IAAAC,oBAAiB;AAEjB,sBAAqB;AACrB,2BAiBO;;;ACpCP,uBAAiB;AAQV,SAAS,SAAS,QAAkB,MAAoB,MAAsB;AACnF,SAAO,iBAAAC,QAAK,KAAK,OAAO,MAAM,MAAM,GAAG,IAAI,OAAO;AACpD;AAEO,SAAS,QAAQ,QAAkB,MAA4B;AACpE,SAAO,iBAAAA,QAAK,KAAK,OAAO,MAAM,IAAI;AACpC;AAEO,SAAS,OAAO,QAA0B;AAC/C,SAAO,iBAAAA,QAAK,KAAK,OAAO,MAAM,gBAAgB,MAAM;AACtD;AAEO,SAAS,QAAQ,QAA0B;AAGhD,SAAO,iBAAAA,QAAK,KAAK,OAAO,MAAM,GAAG,YAAY;AAC/C;AAGO,SAAS,cACd,QACA,SACuC;AACvC,QAAM,MAAM,iBAAAA,QAAK,SAAS,OAAO,MAAM,OAAO;AAC9C,MAAI,IAAI,WAAW,IAAI,KAAK,IAAI,WAAW,cAAc,EAAG,QAAO;AACnE,QAAM,WAAW,IAAI,MAAM,iBAAAA,QAAK,GAAG;AACnC,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,OAAO,SAAS,CAAC;AACvB,QAAM,OAAO,SAAS,CAAC;AACvB,MAAI,CAAC,KAAK,SAAS,OAAO,EAAG,QAAO;AACpC,QAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,SAAO,EAAE,MAAM,KAAK;AACtB;;;AClCA,sBAAe;AACf,IAAAC,oBAAiB;AACjB,2BAAqB;AACrB,qBAA6C;AAGtC,IAAM,WAAN,MAAe;AAAA,EACpB,YAA6B,MAAc;AAAd;AAAA,EAAe;AAAA,EAE5C,MAAM,OAAO,KAAmC;AAC9C,UAAM,gBAAAC,QAAG,MAAM,kBAAAC,QAAK,QAAQ,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,UAAM,gBAAAD,QAAG,WAAW,KAAK,MAAM,KAAK,UAAU,GAAG,IAAI,MAAM,MAAM;AAAA,EACnE;AAAA;AAAA,EAGA,OAAO,UAAwC;AAC7C,QAAI,KAAC,2BAAW,KAAK,IAAI,EAAG;AAC5B,UAAM,KAAK,qBAAAE,QAAS,gBAAgB;AAAA,MAClC,WAAO,iCAAiB,KAAK,MAAM,EAAE,UAAU,OAAO,CAAC;AAAA,MACvD,WAAW;AAAA,IACb,CAAC;AACD,QAAI;AACF,uBAAiB,QAAQ,IAAI;AAC3B,YAAI,CAAC,KAAK,KAAK,EAAG;AAClB,YAAI;AACF,gBAAM,KAAK,MAAM,IAAI;AAAA,QACvB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA8B;AAClC,QAAI,MAAM;AACV,qBAAiB,OAAO,KAAK,QAAQ,GAAG;AACtC,UAAI,OAAO,IAAI,QAAQ,YAAY,IAAI,MAAM,IAAK,OAAM,IAAI;AAAA,IAC9D;AACA,WAAO;AAAA,EACT;AACF;;;AC9CO,IAAM,aAAN,MAAiB;AAAA,EAAjB;AACL,SAAiB,QAAQ,oBAAI,IAA8B;AAAA;AAAA,EAE3D,MAAM,IAAO,KAAa,IAAkC;AAC1D,UAAM,OAAO,KAAK,MAAM,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACpD,UAAM,OAAO,KAAK,KAAK,IAAI,EAAE;AAG7B,UAAM,YAAY,KAAK,MAAM,MAAM,MAAS;AAC5C,SAAK,MAAM,IAAI,KAAK,SAAS;AAC7B,QAAI;AACF,aAAO,MAAM;AAAA,IACf,UAAE;AAEA,UAAI,KAAK,MAAM,IAAI,GAAG,MAAM,WAAW;AACrC,aAAK,MAAM,OAAO,GAAG;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;AAcO,SAAS,aAAa,SAA4E;AACvG,QAAM,OAAO,oBAAI,IAAsB;AACvC,SAAO;AAAA,IACL,WAAW,CAAC,MAAM;AAAE,WAAK,IAAI,CAAC;AAAA,IAAG;AAAA,IACjC,aAAa,CAAC,MAAM;AAAE,WAAK,OAAO,CAAC;AAAA,IAAG;AAAA,IACtC,SAAS,CAAC,QAAQ;AAChB,iBAAW,KAAK,MAAM;AACpB,YAAI,EAAE,OAAQ;AACd,YAAI,CAAC,QAAQ,KAAK,EAAE,MAAM,EAAG;AAC7B,UAAE,KAAK,GAAG;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;;;ACpCO,SAAS,oBACd,MAC8B;AAC9B,QAAM,QAAyB,CAAC;AAChC,MAAI,SAAgE;AACpE,MAAI,SAAS;AACb,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,SAAS,CAAC,MAAqB,GAAG,KAAK,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG;AAEpE,QAAM,aAA+B;AAAA,IACnC,QAAQ,KAAK;AAAA,IACb,QAAQ;AAAA,IACR,MAAM,CAAC,QAAQ;AACb,UAAI,WAAW,OAAQ;AACvB,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,UAAI,QAAQ;AACV,kBAAU,IAAI,CAAC;AACf,cAAM,IAAI;AACV,iBAAS;AACT,UAAE,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC;AAAA,MACtC,OAAO;AACL,cAAM,KAAK,GAAG;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACA,OAAK,OAAO,UAAU,UAAU;AAEhC,QAAM,SAAS,CAAC,GAAG,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC5D,MAAI,YAAY;AAEhB,QAAM,QAAQ,MAA4C;AACxD,WAAO,YAAY,OAAO,QAAQ;AAChC,YAAM,MAAM,OAAO,WAAW;AAC9B,UAAI,OAAO,KAAK,UAAU,YAAY,IAAI,OAAO,KAAK,MAAO;AAC7D,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,gBAAU,IAAI,CAAC;AACf,aAAO,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM;AAAA,IAC1C;AACA,WAAO,MAAM,SAAS,GAAG;AACvB,YAAM,MAAM,MAAM,MAAM;AACxB,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,gBAAU,IAAI,CAAC;AACf,aAAO,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAqC;AACjD,QAAI,CAAC,QAAQ;AACX,eAAS;AACT,iBAAW,SAAS;AACpB,WAAK,OAAO,YAAY,UAAU;AAClC,UAAI,QAAQ;AACV,cAAM,IAAI;AACV,iBAAS;AACT,UAAE,EAAE,OAAO,QAAW,MAAM,KAAK,CAAC;AAAA,MACpC;AAAA,IACF;AACA,WAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,EACxC;AAEA,QAAM,WAAyC;AAAA,IAC7C,MAAM,MAAM;AACV,UAAI,OAAQ,QAAO,QAAQ,QAAQ,EAAE,OAAO,QAAW,MAAM,KAAK,CAAC;AACnE,YAAM,YAAY,MAAM;AACxB,UAAI,UAAW,QAAO,QAAQ,QAAQ,SAAS;AAC/C,aAAO,IAAI,QAAuC,CAAC,YAAY;AAC7D,iBAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,MAAM,QAAQ,QAAQ,MAAM,CAAC;AAAA,IACrC,OAAO,CAAC,QAAQ;AACd,YAAM;AACN,aAAO,QAAQ,OAAO,GAAG;AAAA,IAC3B;AAAA,EACF;AACA,SAAO,EAAE,CAAC,OAAO,aAAa,GAAG,MAAM,SAAS;AAClD;AAEA,SAAS,MAAS,OAAa;AAC7B,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;AJpCA,IAAM,iBAAiB,CACrB,KACA,WACY;AACZ,MAAI,OAAO,OAAO,OAAO,QAAQ,IAAI,IAAK,QAAO;AACjD,MAAI,OAAO,QAAQ,OAAO,SAAS,IAAI,KAAM,QAAO;AACpD,MAAI,OAAO,QAAQ,OAAO,SAAS,IAAI,KAAM,QAAO;AACpD,SAAO;AACT;AAEA,IAAM,aAAa,CAAC,KAAoB,WAAiC,eAAe,IAAI,KAAK,MAAM;AAEhG,IAAM,uBAAN,MAAyD;AAAA,EAmB9D,YAAY,MAAmC;AAZ/C,SAAiB,QAAQ,IAAI,WAAW;AACxC,SAAiB,SAAsB,aAAa,UAAU;AAG9D;AAAA,SAAiB,QAAQ,oBAAI,IAAoB;AAEjD;AAAA,SAAQ,UAAU;AAElB;AAAA,SAAiB,aAAa,oBAAI,IAAY;AAC9C,SAAQ,UAA4B;AACpC,SAAQ,UAAU;AAGhB,SAAK,MAAM,KAAK;AAChB,SAAK,UAAU,KAAK,WAAW;AAC/B,SAAK,eAAe,KAAK,gBAAgB;AACzC,SAAK,MAAM,KAAK,QAAQ,MAAM,oBAAI,KAAK;AACvC,SAAK,SAAS,EAAE,MAAM,kBAAAC,QAAK,QAAQ,KAAK,IAAI,EAAE;AAC9C,SAAK,MAAM,IAAI,SAAS,QAAQ,KAAK,MAAM,CAAC;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAM,QAAuB;AAC3B,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAGf,UAAM,KAAK,UAAU;AAGrB,UAAM,UAAU,MAAM,KAAK,IAAI,WAAW;AAC1C,SAAK,UAAU,UAAU;AASzB,QAAI,CAAC,KAAK,oBAAgB,4BAAW,KAAK,OAAO,IAAI,EAAG,MAAK,aAAa;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,aAA4B;AACxC,UAAM,iBAAAC,QAAG,MAAM,KAAK,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;AACpD,QAAI,KAAK,WAAW,CAAC,KAAK,gBAAgB,CAAC,KAAK,QAAS,MAAK,aAAa;AAAA,EAC7E;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,MAAM;AACzB,WAAK,UAAU;AAAA,IACjB;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAIA,MAAM,IAAI,KAA4C;AACpD,SAAK,YAAY,GAAG;AACpB,UAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AACrD,QAAI,KAAC,4BAAW,IAAI,EAAG,QAAO;AAC9B,UAAM,OAAO,MAAM,SAAS,IAAI;AAChC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,WAAO,+BAAS,IAAI;AAC1B,QAAI,IAAI,WAAW,IAAI,YAAY,KAAM,QAAO;AAEhD,UAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,WAAO;AAAA,MACL,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,MAClC;AAAA,MACA;AAAA,MACA,YAAY,MAAM,cAAc;AAAA,MAChC,YAAY,MAAM,SAAS,KAAK;AAAA,MAChC,YAAY,MAAM,OAAM,oBAAI,KAAK,CAAC,GAAE,YAAY;AAAA,MAChD,SAAS,MAAM;AAAA,MACf,KAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAAc,MAA4C;AAIxE,UAAM,OAAO,MAAM,KAAK,IAAI,GAAG;AAC/B,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAM,QAAO;AACxC,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,KAAK,QAAuD;AACjE,UAAM,QAAQ,OAAO,SAAS;AAC9B,QAAI,UAAU;AACd,eAAW,CAAC,KAAK,IAAI,KAAK,KAAK,OAAO;AACpC,YAAM,MAAM,YAAY,GAAG;AAC3B,UAAI,CAAC,IAAK;AACV,UAAI,CAAC,eAAe,KAAK,MAAM,EAAG;AAClC,UAAI,OAAO,gBAAgB,CAAC,IAAI,KAAK,SAAS,OAAO,YAAY,EAAG;AACpE,YAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,YAAM,SAA6B;AAAA,QACjC,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY,MAAM,cAAc;AAAA,QAChC,YAAY,MAAM,SAAS,KAAK;AAAA,QAChC,YAAY,MAAM,OAAM,oBAAI,KAAK,CAAC,GAAE,YAAY;AAAA,QAChD,SAAS,MAAM;AAAA,QACf,KAAK,MAAM,OAAO;AAAA,MACpB;AACA,YAAM;AACN,UAAI,EAAE,WAAW,MAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,OAAO,QAAQ,KAAc,OAAuB,CAAC,GAAiC;AACpF,SAAK,YAAY,GAAG;AACpB,UAAM,QAAQ,KAAK,YAAY;AAC/B,UAAM,QAAQ,KAAK,SAAS;AAC5B,QAAI,UAAU;AACd,qBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,UAAI,IAAI,OAAO,MAAO;AACtB,UAAI,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI,IAAI,SAAS,IAAI,KAAM;AAC5D,UAAI,IAAI,IAAI,QAAQ,IAAI,IAAK;AAC7B,YAAM;AACN,UAAI,EAAE,WAAW,MAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAM,QAAqB,OAA8C;AAEvE,UAAM,SAA0B,CAAC;AACjC,UAAM,WAAW,YAAY;AAC3B,uBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,YAAI,WAAW,KAAK,MAAM,EAAG,QAAO,KAAK,GAAG;AAAA,MAC9C;AAAA,IACF,GAAG;AAGH,WAAO,iBAAiB,QAAQ;AAAA,MAAK,MACnC,oBAAoB;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,KAAK;AAAA,QACb,SAAS;AAAA,QACT,aAAa,CAAC,MAAM,EAAE,IAAI;AAAA,MAC5B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,IAAI,KAAc,MAAe,MAAsC;AACrE,SAAK,YAAY,GAAG;AACpB,WAAO,KAAK,MAAM,QAAI,6BAAO,GAAG,GAAG,YAAY;AAC7C,YAAM,UAAM,6BAAO,GAAG;AACtB,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,WAAK,KAAK,iBAAiB,UAAU,aAAa;AAChD,cAAM,IAAI,mCAAc,KAAK,KAAK,iBAAiB,MAAM,WAAW;AAAA,MACtE;AACA,YAAM,WAAO,+BAAS,IAAI;AAC1B,UAAI,gBAAgB,MAAM;AAExB,cAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,eAAO;AAAA,UACL,SAAS;AAAA,UACT,KAAK,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,YACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,YAClC,MAAM;AAAA,YACN;AAAA,YACA,YAAY,MAAM,cAAc;AAAA,YAChC,YAAY,MAAM,SAAS,KAAK;AAAA,YAChC,YAAY,MAAM,MAAM,KAAK,IAAI,EAAE,YAAY;AAAA,YAC/C,SAAS,MAAM;AAAA,YACf,KAAK,MAAM,OAAO;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,MAAM,KAAK;AACjB,YAAM,KAAK,KAAK,IAAI,EAAE,YAAY;AAClC,YAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AAErD,YAAM,KAAK,WAAW;AACtB,YAAM,iBAAAA,QAAG,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAClE,WAAK,WAAW,IAAI,IAAI;AACxB,UAAI;AACF,cAAM,gBAAgB,MAAM,IAAI;AAAA,MAClC,UAAE;AAGA,mBAAW,MAAM,KAAK,WAAW,OAAO,IAAI,GAAG,GAAG;AAAA,MACpD;AAGA,WAAK,iBAAiB,IAAI;AAC1B,WAAK,MAAM,IAAI,KAAK,IAAI;AAExB,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI,cAAc,WAAW;AAAA,QAC7B,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,QACd;AAAA,QACA,QAAQ,KAAK,UAAU;AAAA,MACzB;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AAEvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT;AAAA,QACA,MAAM;AAAA,UACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,UAClC,MAAM;AAAA,UACN;AAAA,UACA,YAAY;AAAA,UACZ,YAAY,KAAK;AAAA,UACjB,YAAY;AAAA,UACZ,SAAS,KAAK;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,KAAc,MAA4C;AAC/D,SAAK,YAAY,GAAG;AACpB,WAAO,KAAK,MAAM,QAAI,6BAAO,GAAG,GAAG,YAAY;AAC7C,YAAM,UAAM,6BAAO,GAAG;AACtB,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,UAAI,gBAAgB,KAAK,eAAe;AACtC,cAAM,IAAI,mCAAc,KAAK,KAAK,eAAe,WAAW;AAAA,MAC9D;AACA,YAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AAErD,YAAM,KAAK,WAAW;AACtB,WAAK,WAAW,IAAI,IAAI;AACxB,UAAI;AACF,gBAAI,4BAAW,IAAI,EAAG,OAAM,iBAAAA,QAAG,OAAO,IAAI;AAAA,MAC5C,UAAE;AACA,mBAAW,MAAM,KAAK,WAAW,OAAO,IAAI,GAAG,GAAG;AAAA,MACpD;AACA,WAAK,MAAM,OAAO,GAAG;AACrB,YAAM,MAAM,KAAK;AACjB,YAAM,KAAK,KAAK,IAAI,EAAE,YAAY;AAClC,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI;AAAA,QACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,QACd;AAAA,QACA,QAAQ,KAAK,UAAU;AAAA,MACzB;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AACvB,aAAO,EAAE,IAAI;AAAA,IACf,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,YAAY,KAAoB;AACtC,QAAI,IAAI,QAAQ,KAAK,KAAK;AACxB,YAAM,IAAI;AAAA,QACR,qDAAqD,KAAK,GAAG,aAAa,IAAI,GAAG;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAA2B;AACvC,SAAK,MAAM,MAAM;AAEjB,QAAI,UAAsC,CAAC;AAC3C,QAAI;AACF,gBAAU,MAAM,iBAAAA,QAAG,QAAQ,KAAK,OAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IACtE,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,UAAI,MAAM,KAAK,WAAW,GAAG,EAAG;AAChC,YAAM,OAAO,MAAM;AACnB,YAAM,MAAM,kBAAAD,QAAK,KAAK,KAAK,OAAO,MAAM,IAAI;AAC5C,UAAI,QAAkB,CAAC;AACvB,UAAI;AACF,gBAAQ,MAAM,iBAAAC,QAAG,QAAQ,GAAG;AAAA,MAC9B,QAAQ;AACN;AAAA,MACF;AACA,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAC7B,cAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,cAAM,MAAe;AAAA,UACnB,KAAK,KAAK;AAAA,UACV;AAAA,UACA;AAAA,QACF;AACA,cAAM,OAAO,MAAM,SAAS,kBAAAD,QAAK,KAAK,KAAK,IAAI,CAAC;AAChD,YAAI,CAAC,KAAM;AACX,aAAK,MAAM,QAAI,6BAAO,GAAG,OAAG,+BAAS,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,gBACZ,KACA,MAC+B;AAC/B,QAAI,OAA6B;AACjC,qBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,UAAI,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI,IAAI,SAAS,IAAI,KAAM;AAC5D,UAAI,IAAI,IAAI,QAAQ,IAAI,IAAK;AAC7B,UAAI,IAAI,SAAS,KAAM,QAAO;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0CQ,iBAAiB,MAAoB;AAC3C,UAAM,IAAI,KAAK;AAEf,QAAI,CAAC,KAAK,EAAE,OAAQ;AACpB,MAAE,IAAI,IAAI;AAAA,EACZ;AAAA,EAEQ,eAAqB;AAC3B,UAAM,OAAO,KAAK,OAAO;AACzB,UAAM,IAAI,gBAAAE,QAAS,MAAM,MAAM;AAAA;AAAA;AAAA;AAAA,MAI7B,SAAS,CAAC,MAAc,mBAAmB,MAAM,CAAC;AAAA,MAClD,eAAe;AAAA,MACf,OAAO;AAAA,MACP,kBAAkB,EAAE,oBAAoB,IAAI,cAAc,GAAG;AAAA;AAAA;AAAA;AAAA,MAI7D,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,gBAAgB;AAAA,IAClB,CAAC;AACD,MAAE,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,KAAK,CAAC;AACrD,MAAE,GAAG,UAAU,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,QAAQ,CAAC;AAC3D,MAAE,GAAG,UAAU,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,QAAQ,CAAC;AAC3D,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,eAAe,SAAiB,MAAkD;AAC9F,QAAI,KAAK,WAAW,IAAI,OAAO,EAAG;AAClC,UAAM,SAAS,cAAc,KAAK,QAAQ,OAAO;AACjD,QAAI,CAAC,OAAQ;AACb,UAAM,MAAe;AAAA,MACnB,KAAK,KAAK;AAAA,MACV,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,IACf;AACA,UAAM,UAAM,6BAAO,GAAG;AACtB,UAAM,KAAK,MAAM,IAAI,KAAK,YAAY;AACpC,UAAI,SAAS,UAAU;AACrB,cAAMC,eAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,YAAI,CAACA,aAAa;AAClB,aAAK,MAAM,OAAO,GAAG;AACrB,cAAMC,OAAM,KAAK;AACjB,cAAMC,OAAqB;AAAA,UACzB,KAAAD;AAAA,UACA,IAAI;AAAA,UACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,UAClC,MAAM;AAAA,UACN,YAAYD;AAAA,UACZ,OAAO,KAAK;AAAA,UACZ,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,UAC3B,QAAQ;AAAA,QACV;AACA,cAAM,KAAK,IAAI,OAAOE,IAAG;AACzB,aAAK,OAAO,QAAQA,IAAG;AACvB;AAAA,MACF;AACA,YAAM,OAAO,MAAM,SAAS,OAAO;AACnC,UAAI,CAAC,KAAM;AACX,YAAM,WAAO,+BAAS,IAAI;AAC1B,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,UAAI,gBAAgB,KAAM;AAC1B,WAAK,MAAM,IAAI,KAAK,IAAI;AACxB,YAAM,MAAM,KAAK;AACjB,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI,cAAc,WAAW;AAAA,QAC7B,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,QAC3B,QAAQ;AAAA,MACV;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AAAA,IACzB,CAAC;AAAA,EACH;AACF;AAkCA,SAAS,mBAAmB,MAAc,SAA0B;AAClE,QAAM,MAAM,kBAAAL,QAAK,SAAS,MAAM,OAAO;AAEvC,MAAI,QAAQ,MAAM,IAAI,WAAW,IAAI,EAAG,QAAO;AAC/C,SAAO,IAAI,MAAM,OAAO,EAAE,KAAK,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AACrE;AAEA,eAAe,SAAS,MAAuC;AAC7D,MAAI;AACF,UAAM,OAAO,MAAM,iBAAAC,QAAG,SAAS,MAAM,MAAM;AAC3C,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,gBAAgB,MAAc,MAA8B;AACzE,QAAM,MAAM,GAAG,IAAI,IAAI,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;AAChD,QAAM,iBAAAA,QAAG,UAAU,KAAK,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,MAAM,MAAM;AACpE,QAAM,iBAAAA,QAAG,OAAO,KAAK,IAAI;AAC3B;AAEA,SAAS,YAAY,KAA6B;AAChD,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,KAAK,MAAM,CAAC;AAAA,IACZ,MAAM,MAAM,CAAC;AAAA,IACb,MAAM,MAAM,CAAC;AAAA,EACf;AACF;AAMA,SAAS,iBAAoB,SAAsD;AACjF,SAAO;AAAA,IACL,CAAC,OAAO,aAAa,IAAI;AACvB,UAAI,QAAiC;AACrC,aAAO;AAAA,QACL,MAAM,OAAO;AACX,cAAI,CAAC,OAAO;AACV,kBAAM,WAAW,MAAM;AACvB,oBAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,UACzC;AACA,iBAAO,MAAM,KAAK;AAAA,QACpB;AAAA,QACA,MAAM,OAAO,OAAiB;AAC5B,cAAI,CAAC,OAAO;AACV,kBAAM,WAAW,MAAM;AACvB,oBAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,UACzC;AACA,cAAI,MAAM,OAAQ,QAAO,MAAM,OAAO,KAAK;AAC3C,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["import_promises","import_node_fs","import_node_path","path","import_node_path","fs","path","readline","path","fs","chokidar","currentHead","seq","evt"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -30,7 +30,33 @@ declare class FileSystemRepository implements MetadataRepository {
|
|
|
30
30
|
private watcher;
|
|
31
31
|
private started;
|
|
32
32
|
constructor(opts: FileSystemRepositoryOptions);
|
|
33
|
+
/**
|
|
34
|
+
* Attach the repository. **Creates nothing on disk** (#7000).
|
|
35
|
+
*
|
|
36
|
+
* Attaching is not a write. `start()` used to `mkdir` both the root and
|
|
37
|
+
* `<root>/.objectstack/.log` unconditionally, which meant every read-only
|
|
38
|
+
* boot that merely attaches a repository left a skeleton behind — most
|
|
39
|
+
* visibly `os migrate plan`, a declared dry run, on a project that has
|
|
40
|
+
* never been started. That is the same property #6743 ruled on for
|
|
41
|
+
* `.objectstack/data/`: a dry run leaves nothing behind, and the existence
|
|
42
|
+
* of `.objectstack/` has to stay a usable "this project has been started"
|
|
43
|
+
* signal.
|
|
44
|
+
*
|
|
45
|
+
* Every read path below already treats a missing root as an empty
|
|
46
|
+
* repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on
|
|
47
|
+
* `existsSync`, `get` guards on `existsSync`), so the root is materialized
|
|
48
|
+
* by `ensureRoot()` on the first write instead.
|
|
49
|
+
*/
|
|
33
50
|
start(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Bring the repository root into existence. Called by every write path
|
|
53
|
+
* immediately before it touches the disk — `start()` deliberately does not
|
|
54
|
+
* create it (#7000), so this is the single seam where the root appears.
|
|
55
|
+
*
|
|
56
|
+
* It is also where a watcher that `start()` could not arm (missing root)
|
|
57
|
+
* gets armed, so "external edits are detected" survives the change.
|
|
58
|
+
*/
|
|
59
|
+
private ensureRoot;
|
|
34
60
|
close(): Promise<void>;
|
|
35
61
|
get(ref: MetaRef): Promise<MetadataItem | null>;
|
|
36
62
|
getByHash(ref: MetaRef, hash: string): Promise<MetadataItem | null>;
|
|
@@ -42,6 +68,47 @@ declare class FileSystemRepository implements MetadataRepository {
|
|
|
42
68
|
private assertScope;
|
|
43
69
|
private scanHeads;
|
|
44
70
|
private findMetaForHash;
|
|
71
|
+
/**
|
|
72
|
+
* Register a path this repository just wrote with the watcher (#7282).
|
|
73
|
+
*
|
|
74
|
+
* chokidar's initial scan is asynchronous, and every write path here can be
|
|
75
|
+
* running **while it is still walking the tree** — `start()` arms the watcher
|
|
76
|
+
* and the caller may `put()` on the next tick, and `ensureRoot()` arms it in
|
|
77
|
+
* the middle of the very first write. With `usePolling` that combination has
|
|
78
|
+
* a permanently-blinding interleaving, measured on chokidar 5 with this
|
|
79
|
+
* repository's own options:
|
|
80
|
+
*
|
|
81
|
+
* 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic
|
|
82
|
+
* `rename` in `writeJsonAtomic` has not landed yet.
|
|
83
|
+
* 2. the rename lands; the directory's mtime changes.
|
|
84
|
+
* 3. chokidar calls `watchFile()` on that directory, and libuv takes its
|
|
85
|
+
* polling baseline stat — which already reflects step 2.
|
|
86
|
+
*
|
|
87
|
+
* From then on the directory's stat never changes again, so no poll ever
|
|
88
|
+
* fires for it, `_handleRead` never re-runs, the item file is never added to
|
|
89
|
+
* the watched set, and no per-file watcher is ever created. chokidar emits
|
|
90
|
+
* neither `add` nor `change` for that path **for the life of the process** —
|
|
91
|
+
* `getWatched()` reports the type directory as `[]` forever while the file
|
|
92
|
+
* sits in it. That is the whole of #7282: the four merge-queue ejections all
|
|
93
|
+
* waited out their deadlines (20s, then 25541ms against 25s) on an event that
|
|
94
|
+
* was never going to be delivered, which is why widening the deadline and
|
|
95
|
+
* widening the pre-edit sleep both changed nothing, and why lowering
|
|
96
|
+
* `interval` would change nothing either — a shorter poll re-compares against
|
|
97
|
+
* the same unchanged directory stat.
|
|
98
|
+
*
|
|
99
|
+
* The window is exactly "files that exist at baseline time but were absent
|
|
100
|
+
* from the snapshot read a moment earlier", and the only writer that can be
|
|
101
|
+
* inside it is us. So we close it at the source: tell the watcher explicitly
|
|
102
|
+
* about every path we create, instead of hoping its scan happened to see it.
|
|
103
|
+
*
|
|
104
|
+
* `add()` is idempotent here — `_handleFile` returns early when the parent
|
|
105
|
+
* directory already tracks the basename — and it emits nothing, because
|
|
106
|
+
* chokidar treats an explicit `add()` as an initial add and `ignoreInitial`
|
|
107
|
+
* is set. Its effect is the one we need: `_watchWithNodeFs` registers the
|
|
108
|
+
* basename with the parent directory (without which chokidar drops `change`
|
|
109
|
+
* events for the file) and starts the per-file poll.
|
|
110
|
+
*/
|
|
111
|
+
private trackWrittenPath;
|
|
45
112
|
private startWatcher;
|
|
46
113
|
private handleFsChange;
|
|
47
114
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,33 @@ declare class FileSystemRepository implements MetadataRepository {
|
|
|
30
30
|
private watcher;
|
|
31
31
|
private started;
|
|
32
32
|
constructor(opts: FileSystemRepositoryOptions);
|
|
33
|
+
/**
|
|
34
|
+
* Attach the repository. **Creates nothing on disk** (#7000).
|
|
35
|
+
*
|
|
36
|
+
* Attaching is not a write. `start()` used to `mkdir` both the root and
|
|
37
|
+
* `<root>/.objectstack/.log` unconditionally, which meant every read-only
|
|
38
|
+
* boot that merely attaches a repository left a skeleton behind — most
|
|
39
|
+
* visibly `os migrate plan`, a declared dry run, on a project that has
|
|
40
|
+
* never been started. That is the same property #6743 ruled on for
|
|
41
|
+
* `.objectstack/data/`: a dry run leaves nothing behind, and the existence
|
|
42
|
+
* of `.objectstack/` has to stay a usable "this project has been started"
|
|
43
|
+
* signal.
|
|
44
|
+
*
|
|
45
|
+
* Every read path below already treats a missing root as an empty
|
|
46
|
+
* repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on
|
|
47
|
+
* `existsSync`, `get` guards on `existsSync`), so the root is materialized
|
|
48
|
+
* by `ensureRoot()` on the first write instead.
|
|
49
|
+
*/
|
|
33
50
|
start(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Bring the repository root into existence. Called by every write path
|
|
53
|
+
* immediately before it touches the disk — `start()` deliberately does not
|
|
54
|
+
* create it (#7000), so this is the single seam where the root appears.
|
|
55
|
+
*
|
|
56
|
+
* It is also where a watcher that `start()` could not arm (missing root)
|
|
57
|
+
* gets armed, so "external edits are detected" survives the change.
|
|
58
|
+
*/
|
|
59
|
+
private ensureRoot;
|
|
34
60
|
close(): Promise<void>;
|
|
35
61
|
get(ref: MetaRef): Promise<MetadataItem | null>;
|
|
36
62
|
getByHash(ref: MetaRef, hash: string): Promise<MetadataItem | null>;
|
|
@@ -42,6 +68,47 @@ declare class FileSystemRepository implements MetadataRepository {
|
|
|
42
68
|
private assertScope;
|
|
43
69
|
private scanHeads;
|
|
44
70
|
private findMetaForHash;
|
|
71
|
+
/**
|
|
72
|
+
* Register a path this repository just wrote with the watcher (#7282).
|
|
73
|
+
*
|
|
74
|
+
* chokidar's initial scan is asynchronous, and every write path here can be
|
|
75
|
+
* running **while it is still walking the tree** — `start()` arms the watcher
|
|
76
|
+
* and the caller may `put()` on the next tick, and `ensureRoot()` arms it in
|
|
77
|
+
* the middle of the very first write. With `usePolling` that combination has
|
|
78
|
+
* a permanently-blinding interleaving, measured on chokidar 5 with this
|
|
79
|
+
* repository's own options:
|
|
80
|
+
*
|
|
81
|
+
* 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic
|
|
82
|
+
* `rename` in `writeJsonAtomic` has not landed yet.
|
|
83
|
+
* 2. the rename lands; the directory's mtime changes.
|
|
84
|
+
* 3. chokidar calls `watchFile()` on that directory, and libuv takes its
|
|
85
|
+
* polling baseline stat — which already reflects step 2.
|
|
86
|
+
*
|
|
87
|
+
* From then on the directory's stat never changes again, so no poll ever
|
|
88
|
+
* fires for it, `_handleRead` never re-runs, the item file is never added to
|
|
89
|
+
* the watched set, and no per-file watcher is ever created. chokidar emits
|
|
90
|
+
* neither `add` nor `change` for that path **for the life of the process** —
|
|
91
|
+
* `getWatched()` reports the type directory as `[]` forever while the file
|
|
92
|
+
* sits in it. That is the whole of #7282: the four merge-queue ejections all
|
|
93
|
+
* waited out their deadlines (20s, then 25541ms against 25s) on an event that
|
|
94
|
+
* was never going to be delivered, which is why widening the deadline and
|
|
95
|
+
* widening the pre-edit sleep both changed nothing, and why lowering
|
|
96
|
+
* `interval` would change nothing either — a shorter poll re-compares against
|
|
97
|
+
* the same unchanged directory stat.
|
|
98
|
+
*
|
|
99
|
+
* The window is exactly "files that exist at baseline time but were absent
|
|
100
|
+
* from the snapshot read a moment earlier", and the only writer that can be
|
|
101
|
+
* inside it is us. So we close it at the source: tell the watcher explicitly
|
|
102
|
+
* about every path we create, instead of hoping its scan happened to see it.
|
|
103
|
+
*
|
|
104
|
+
* `add()` is idempotent here — `_handleFile` returns early when the parent
|
|
105
|
+
* directory already tracks the basename — and it emits nothing, because
|
|
106
|
+
* chokidar treats an explicit `add()` as an initial add and `ignoreInitial`
|
|
107
|
+
* is set. Its effect is the one we need: `_watchWithNodeFs` registers the
|
|
108
|
+
* basename with the parent directory (without which chokidar drops `change`
|
|
109
|
+
* events for the file) and starts the per-file poll.
|
|
110
|
+
*/
|
|
111
|
+
private trackWrittenPath;
|
|
45
112
|
private startWatcher;
|
|
46
113
|
private handleFsChange;
|
|
47
114
|
}
|
package/dist/index.js
CHANGED
|
@@ -222,15 +222,42 @@ var FileSystemRepository = class {
|
|
|
222
222
|
this.log = new JsonlLog(logFile(this.layout));
|
|
223
223
|
}
|
|
224
224
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* Attach the repository. **Creates nothing on disk** (#7000).
|
|
227
|
+
*
|
|
228
|
+
* Attaching is not a write. `start()` used to `mkdir` both the root and
|
|
229
|
+
* `<root>/.objectstack/.log` unconditionally, which meant every read-only
|
|
230
|
+
* boot that merely attaches a repository left a skeleton behind — most
|
|
231
|
+
* visibly `os migrate plan`, a declared dry run, on a project that has
|
|
232
|
+
* never been started. That is the same property #6743 ruled on for
|
|
233
|
+
* `.objectstack/data/`: a dry run leaves nothing behind, and the existence
|
|
234
|
+
* of `.objectstack/` has to stay a usable "this project has been started"
|
|
235
|
+
* signal.
|
|
236
|
+
*
|
|
237
|
+
* Every read path below already treats a missing root as an empty
|
|
238
|
+
* repository (`scanHeads` swallows ENOENT, `JsonlLog` guards on
|
|
239
|
+
* `existsSync`, `get` guards on `existsSync`), so the root is materialized
|
|
240
|
+
* by `ensureRoot()` on the first write instead.
|
|
241
|
+
*/
|
|
225
242
|
async start() {
|
|
226
243
|
if (this.started) return;
|
|
227
244
|
this.started = true;
|
|
228
|
-
await fs2.mkdir(this.layout.root, { recursive: true });
|
|
229
|
-
await fs2.mkdir(logDir(this.layout), { recursive: true });
|
|
230
245
|
await this.scanHeads();
|
|
231
246
|
const highest = await this.log.highestSeq();
|
|
232
247
|
this.nextSeq = highest + 1;
|
|
233
|
-
if (!this.disableWatch) this.startWatcher();
|
|
248
|
+
if (!this.disableWatch && existsSync2(this.layout.root)) this.startWatcher();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Bring the repository root into existence. Called by every write path
|
|
252
|
+
* immediately before it touches the disk — `start()` deliberately does not
|
|
253
|
+
* create it (#7000), so this is the single seam where the root appears.
|
|
254
|
+
*
|
|
255
|
+
* It is also where a watcher that `start()` could not arm (missing root)
|
|
256
|
+
* gets armed, so "external edits are detected" survives the change.
|
|
257
|
+
*/
|
|
258
|
+
async ensureRoot() {
|
|
259
|
+
await fs2.mkdir(this.layout.root, { recursive: true });
|
|
260
|
+
if (this.started && !this.disableWatch && !this.watcher) this.startWatcher();
|
|
234
261
|
}
|
|
235
262
|
async close() {
|
|
236
263
|
if (this.watcher) {
|
|
@@ -348,6 +375,7 @@ var FileSystemRepository = class {
|
|
|
348
375
|
const seq = this.nextSeq++;
|
|
349
376
|
const ts = this.now().toISOString();
|
|
350
377
|
const file = itemPath(this.layout, ref.type, ref.name);
|
|
378
|
+
await this.ensureRoot();
|
|
351
379
|
await fs2.mkdir(typeDir(this.layout, ref.type), { recursive: true });
|
|
352
380
|
this.selfWrites.add(file);
|
|
353
381
|
try {
|
|
@@ -355,6 +383,7 @@ var FileSystemRepository = class {
|
|
|
355
383
|
} finally {
|
|
356
384
|
setTimeout(() => this.selfWrites.delete(file), 200);
|
|
357
385
|
}
|
|
386
|
+
this.trackWrittenPath(file);
|
|
358
387
|
this.heads.set(key, hash);
|
|
359
388
|
const evt = {
|
|
360
389
|
seq,
|
|
@@ -394,6 +423,7 @@ var FileSystemRepository = class {
|
|
|
394
423
|
throw new ConflictError(ref, opts.parentVersion, currentHead);
|
|
395
424
|
}
|
|
396
425
|
const file = itemPath(this.layout, ref.type, ref.name);
|
|
426
|
+
await this.ensureRoot();
|
|
397
427
|
this.selfWrites.add(file);
|
|
398
428
|
try {
|
|
399
429
|
if (existsSync2(file)) await fs2.unlink(file);
|
|
@@ -469,10 +499,58 @@ var FileSystemRepository = class {
|
|
|
469
499
|
}
|
|
470
500
|
return last;
|
|
471
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Register a path this repository just wrote with the watcher (#7282).
|
|
504
|
+
*
|
|
505
|
+
* chokidar's initial scan is asynchronous, and every write path here can be
|
|
506
|
+
* running **while it is still walking the tree** — `start()` arms the watcher
|
|
507
|
+
* and the caller may `put()` on the next tick, and `ensureRoot()` arms it in
|
|
508
|
+
* the middle of the very first write. With `usePolling` that combination has
|
|
509
|
+
* a permanently-blinding interleaving, measured on chokidar 5 with this
|
|
510
|
+
* repository's own options:
|
|
511
|
+
*
|
|
512
|
+
* 1. chokidar reads `<root>/<type>/` and finds it EMPTY — the atomic
|
|
513
|
+
* `rename` in `writeJsonAtomic` has not landed yet.
|
|
514
|
+
* 2. the rename lands; the directory's mtime changes.
|
|
515
|
+
* 3. chokidar calls `watchFile()` on that directory, and libuv takes its
|
|
516
|
+
* polling baseline stat — which already reflects step 2.
|
|
517
|
+
*
|
|
518
|
+
* From then on the directory's stat never changes again, so no poll ever
|
|
519
|
+
* fires for it, `_handleRead` never re-runs, the item file is never added to
|
|
520
|
+
* the watched set, and no per-file watcher is ever created. chokidar emits
|
|
521
|
+
* neither `add` nor `change` for that path **for the life of the process** —
|
|
522
|
+
* `getWatched()` reports the type directory as `[]` forever while the file
|
|
523
|
+
* sits in it. That is the whole of #7282: the four merge-queue ejections all
|
|
524
|
+
* waited out their deadlines (20s, then 25541ms against 25s) on an event that
|
|
525
|
+
* was never going to be delivered, which is why widening the deadline and
|
|
526
|
+
* widening the pre-edit sleep both changed nothing, and why lowering
|
|
527
|
+
* `interval` would change nothing either — a shorter poll re-compares against
|
|
528
|
+
* the same unchanged directory stat.
|
|
529
|
+
*
|
|
530
|
+
* The window is exactly "files that exist at baseline time but were absent
|
|
531
|
+
* from the snapshot read a moment earlier", and the only writer that can be
|
|
532
|
+
* inside it is us. So we close it at the source: tell the watcher explicitly
|
|
533
|
+
* about every path we create, instead of hoping its scan happened to see it.
|
|
534
|
+
*
|
|
535
|
+
* `add()` is idempotent here — `_handleFile` returns early when the parent
|
|
536
|
+
* directory already tracks the basename — and it emits nothing, because
|
|
537
|
+
* chokidar treats an explicit `add()` as an initial add and `ignoreInitial`
|
|
538
|
+
* is set. Its effect is the one we need: `_watchWithNodeFs` registers the
|
|
539
|
+
* basename with the parent directory (without which chokidar drops `change`
|
|
540
|
+
* events for the file) and starts the per-file poll.
|
|
541
|
+
*/
|
|
542
|
+
trackWrittenPath(file) {
|
|
543
|
+
const w = this.watcher;
|
|
544
|
+
if (!w || w.closed) return;
|
|
545
|
+
w.add(file);
|
|
546
|
+
}
|
|
472
547
|
startWatcher() {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
//
|
|
548
|
+
const root = this.layout.root;
|
|
549
|
+
const w = chokidar.watch(root, {
|
|
550
|
+
// Skip dotfiles under the root — including the repository's own
|
|
551
|
+
// `.objectstack/` bookkeeping subtree — matched on the path RELATIVE
|
|
552
|
+
// to the watch root (#7150). See `isIgnoredWatchPath`.
|
|
553
|
+
ignored: (p) => isIgnoredWatchPath(root, p),
|
|
476
554
|
ignoreInitial: true,
|
|
477
555
|
depth: 2,
|
|
478
556
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
|
|
@@ -540,6 +618,11 @@ var FileSystemRepository = class {
|
|
|
540
618
|
});
|
|
541
619
|
}
|
|
542
620
|
};
|
|
621
|
+
function isIgnoredWatchPath(root, absPath) {
|
|
622
|
+
const rel = path3.relative(root, absPath);
|
|
623
|
+
if (rel === "" || rel.startsWith("..")) return false;
|
|
624
|
+
return rel.split(/[\\/]/).some((segment) => segment.startsWith("."));
|
|
625
|
+
}
|
|
543
626
|
async function readJson(file) {
|
|
544
627
|
try {
|
|
545
628
|
const text = await fs2.readFile(file, "utf8");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../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\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 */\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 logDir,\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 async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n await fs.mkdir(this.layout.root, { recursive: true });\n await fs.mkdir(logDir(this.layout), { recursive: true });\n\n // 1) Scan body files to build the head index.\n await this.scanHeads();\n\n // 2) Hydrate nextSeq from the existing log.\n const highest = await this.log.highestSeq();\n this.nextSeq = highest + 1;\n\n // 3) Start the watcher (unless disabled).\n if (!this.disableWatch) 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 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 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 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 private startWatcher(): void {\n const w = chokidar.watch(this.layout.root, {\n ignored: [/(^|[\\\\/])\\../], // skip dotfiles incl. .objectstack\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\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":["fs","existsSync","path","path","path","fs","existsSync","currentHead","seq","evt"]}
|
|
1
|
+
{"version":3,"sources":["../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\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":";AAuBA,OAAOA,SAAQ;AACf,SAAS,cAAAC,mBAAkB;AAC3B,OAAOC,WAAU;AAEjB,OAAO,cAAc;AACrB;AAAA,EAcE;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACpCP,OAAO,UAAU;AAQV,SAAS,SAAS,QAAkB,MAAoB,MAAsB;AACnF,SAAO,KAAK,KAAK,OAAO,MAAM,MAAM,GAAG,IAAI,OAAO;AACpD;AAEO,SAAS,QAAQ,QAAkB,MAA4B;AACpE,SAAO,KAAK,KAAK,OAAO,MAAM,IAAI;AACpC;AAEO,SAAS,OAAO,QAA0B;AAC/C,SAAO,KAAK,KAAK,OAAO,MAAM,gBAAgB,MAAM;AACtD;AAEO,SAAS,QAAQ,QAA0B;AAGhD,SAAO,KAAK,KAAK,OAAO,MAAM,GAAG,YAAY;AAC/C;AAGO,SAAS,cACd,QACA,SACuC;AACvC,QAAM,MAAM,KAAK,SAAS,OAAO,MAAM,OAAO;AAC9C,MAAI,IAAI,WAAW,IAAI,KAAK,IAAI,WAAW,cAAc,EAAG,QAAO;AACnE,QAAM,WAAW,IAAI,MAAM,KAAK,GAAG;AACnC,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,QAAM,OAAO,SAAS,CAAC;AACvB,QAAM,OAAO,SAAS,CAAC;AACvB,MAAI,CAAC,KAAK,SAAS,OAAO,EAAG,QAAO;AACpC,QAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,SAAO,EAAE,MAAM,KAAK;AACtB;;;AClCA,OAAO,QAAQ;AACf,OAAOC,WAAU;AACjB,OAAO,cAAc;AACrB,SAAS,kBAAkB,kBAAkB;AAGtC,IAAM,WAAN,MAAe;AAAA,EACpB,YAA6B,MAAc;AAAd;AAAA,EAAe;AAAA,EAE5C,MAAM,OAAO,KAAmC;AAC9C,UAAM,GAAG,MAAMA,MAAK,QAAQ,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,UAAM,GAAG,WAAW,KAAK,MAAM,KAAK,UAAU,GAAG,IAAI,MAAM,MAAM;AAAA,EACnE;AAAA;AAAA,EAGA,OAAO,UAAwC;AAC7C,QAAI,CAAC,WAAW,KAAK,IAAI,EAAG;AAC5B,UAAM,KAAK,SAAS,gBAAgB;AAAA,MAClC,OAAO,iBAAiB,KAAK,MAAM,EAAE,UAAU,OAAO,CAAC;AAAA,MACvD,WAAW;AAAA,IACb,CAAC;AACD,QAAI;AACF,uBAAiB,QAAQ,IAAI;AAC3B,YAAI,CAAC,KAAK,KAAK,EAAG;AAClB,YAAI;AACF,gBAAM,KAAK,MAAM,IAAI;AAAA,QACvB,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,UAAE;AACA,SAAG,MAAM;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAA8B;AAClC,QAAI,MAAM;AACV,qBAAiB,OAAO,KAAK,QAAQ,GAAG;AACtC,UAAI,OAAO,IAAI,QAAQ,YAAY,IAAI,MAAM,IAAK,OAAM,IAAI;AAAA,IAC9D;AACA,WAAO;AAAA,EACT;AACF;;;AC9CO,IAAM,aAAN,MAAiB;AAAA,EAAjB;AACL,SAAiB,QAAQ,oBAAI,IAA8B;AAAA;AAAA,EAE3D,MAAM,IAAO,KAAa,IAAkC;AAC1D,UAAM,OAAO,KAAK,MAAM,IAAI,GAAG,KAAK,QAAQ,QAAQ;AACpD,UAAM,OAAO,KAAK,KAAK,IAAI,EAAE;AAG7B,UAAM,YAAY,KAAK,MAAM,MAAM,MAAS;AAC5C,SAAK,MAAM,IAAI,KAAK,SAAS;AAC7B,QAAI;AACF,aAAO,MAAM;AAAA,IACf,UAAE;AAEA,UAAI,KAAK,MAAM,IAAI,GAAG,MAAM,WAAW;AACrC,aAAK,MAAM,OAAO,GAAG;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;AAcO,SAAS,aAAa,SAA4E;AACvG,QAAM,OAAO,oBAAI,IAAsB;AACvC,SAAO;AAAA,IACL,WAAW,CAAC,MAAM;AAAE,WAAK,IAAI,CAAC;AAAA,IAAG;AAAA,IACjC,aAAa,CAAC,MAAM;AAAE,WAAK,OAAO,CAAC;AAAA,IAAG;AAAA,IACtC,SAAS,CAAC,QAAQ;AAChB,iBAAW,KAAK,MAAM;AACpB,YAAI,EAAE,OAAQ;AACd,YAAI,CAAC,QAAQ,KAAK,EAAE,MAAM,EAAG;AAC7B,UAAE,KAAK,GAAG;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;;;ACpCO,SAAS,oBACd,MAC8B;AAC9B,QAAM,QAAyB,CAAC;AAChC,MAAI,SAAgE;AACpE,MAAI,SAAS;AACb,QAAM,YAAY,oBAAI,IAAY;AAClC,QAAM,SAAS,CAAC,MAAqB,GAAG,KAAK,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG;AAEpE,QAAM,aAA+B;AAAA,IACnC,QAAQ,KAAK;AAAA,IACb,QAAQ;AAAA,IACR,MAAM,CAAC,QAAQ;AACb,UAAI,WAAW,OAAQ;AACvB,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,UAAI,QAAQ;AACV,kBAAU,IAAI,CAAC;AACf,cAAM,IAAI;AACV,iBAAS;AACT,UAAE,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC;AAAA,MACtC,OAAO;AACL,cAAM,KAAK,GAAG;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AACA,OAAK,OAAO,UAAU,UAAU;AAEhC,QAAM,SAAS,CAAC,GAAG,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG;AAC5D,MAAI,YAAY;AAEhB,QAAM,QAAQ,MAA4C;AACxD,WAAO,YAAY,OAAO,QAAQ;AAChC,YAAM,MAAM,OAAO,WAAW;AAC9B,UAAI,OAAO,KAAK,UAAU,YAAY,IAAI,OAAO,KAAK,MAAO;AAC7D,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,gBAAU,IAAI,CAAC;AACf,aAAO,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM;AAAA,IAC1C;AACA,WAAO,MAAM,SAAS,GAAG;AACvB,YAAM,MAAM,MAAM,MAAM;AACxB,YAAM,IAAI,OAAO,GAAG;AACpB,UAAI,UAAU,IAAI,CAAC,EAAG;AACtB,gBAAU,IAAI,CAAC;AACf,aAAO,EAAE,OAAO,MAAM,GAAG,GAAG,MAAM,MAAM;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAqC;AACjD,QAAI,CAAC,QAAQ;AACX,eAAS;AACT,iBAAW,SAAS;AACpB,WAAK,OAAO,YAAY,UAAU;AAClC,UAAI,QAAQ;AACV,cAAM,IAAI;AACV,iBAAS;AACT,UAAE,EAAE,OAAO,QAAW,MAAM,KAAK,CAAC;AAAA,MACpC;AAAA,IACF;AACA,WAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,EACxC;AAEA,QAAM,WAAyC;AAAA,IAC7C,MAAM,MAAM;AACV,UAAI,OAAQ,QAAO,QAAQ,QAAQ,EAAE,OAAO,QAAW,MAAM,KAAK,CAAC;AACnE,YAAM,YAAY,MAAM;AACxB,UAAI,UAAW,QAAO,QAAQ,QAAQ,SAAS;AAC/C,aAAO,IAAI,QAAuC,CAAC,YAAY;AAC7D,iBAAS;AAAA,MACX,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,MAAM,QAAQ,QAAQ,MAAM,CAAC;AAAA,IACrC,OAAO,CAAC,QAAQ;AACd,YAAM;AACN,aAAO,QAAQ,OAAO,GAAG;AAAA,IAC3B;AAAA,EACF;AACA,SAAO,EAAE,CAAC,OAAO,aAAa,GAAG,MAAM,SAAS;AAClD;AAEA,SAAS,MAAS,OAAa;AAC7B,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;;;AJpCA,IAAM,iBAAiB,CACrB,KACA,WACY;AACZ,MAAI,OAAO,OAAO,OAAO,QAAQ,IAAI,IAAK,QAAO;AACjD,MAAI,OAAO,QAAQ,OAAO,SAAS,IAAI,KAAM,QAAO;AACpD,MAAI,OAAO,QAAQ,OAAO,SAAS,IAAI,KAAM,QAAO;AACpD,SAAO;AACT;AAEA,IAAM,aAAa,CAAC,KAAoB,WAAiC,eAAe,IAAI,KAAK,MAAM;AAEhG,IAAM,uBAAN,MAAyD;AAAA,EAmB9D,YAAY,MAAmC;AAZ/C,SAAiB,QAAQ,IAAI,WAAW;AACxC,SAAiB,SAAsB,aAAa,UAAU;AAG9D;AAAA,SAAiB,QAAQ,oBAAI,IAAoB;AAEjD;AAAA,SAAQ,UAAU;AAElB;AAAA,SAAiB,aAAa,oBAAI,IAAY;AAC9C,SAAQ,UAA4B;AACpC,SAAQ,UAAU;AAGhB,SAAK,MAAM,KAAK;AAChB,SAAK,UAAU,KAAK,WAAW;AAC/B,SAAK,eAAe,KAAK,gBAAgB;AACzC,SAAK,MAAM,KAAK,QAAQ,MAAM,oBAAI,KAAK;AACvC,SAAK,SAAS,EAAE,MAAMC,MAAK,QAAQ,KAAK,IAAI,EAAE;AAC9C,SAAK,MAAM,IAAI,SAAS,QAAQ,KAAK,MAAM,CAAC;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,MAAM,QAAuB;AAC3B,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAGf,UAAM,KAAK,UAAU;AAGrB,UAAM,UAAU,MAAM,KAAK,IAAI,WAAW;AAC1C,SAAK,UAAU,UAAU;AASzB,QAAI,CAAC,KAAK,gBAAgBC,YAAW,KAAK,OAAO,IAAI,EAAG,MAAK,aAAa;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,aAA4B;AACxC,UAAMC,IAAG,MAAM,KAAK,OAAO,MAAM,EAAE,WAAW,KAAK,CAAC;AACpD,QAAI,KAAK,WAAW,CAAC,KAAK,gBAAgB,CAAC,KAAK,QAAS,MAAK,aAAa;AAAA,EAC7E;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,MAAM;AACzB,WAAK,UAAU;AAAA,IACjB;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAIA,MAAM,IAAI,KAA4C;AACpD,SAAK,YAAY,GAAG;AACpB,UAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AACrD,QAAI,CAACD,YAAW,IAAI,EAAG,QAAO;AAC9B,UAAM,OAAO,MAAM,SAAS,IAAI;AAChC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,OAAO,SAAS,IAAI;AAC1B,QAAI,IAAI,WAAW,IAAI,YAAY,KAAM,QAAO;AAEhD,UAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,WAAO;AAAA,MACL,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,MAClC;AAAA,MACA;AAAA,MACA,YAAY,MAAM,cAAc;AAAA,MAChC,YAAY,MAAM,SAAS,KAAK;AAAA,MAChC,YAAY,MAAM,OAAM,oBAAI,KAAK,CAAC,GAAE,YAAY;AAAA,MAChD,SAAS,MAAM;AAAA,MACf,KAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAAc,MAA4C;AAIxE,UAAM,OAAO,MAAM,KAAK,IAAI,GAAG;AAC/B,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAM,QAAO;AACxC,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,KAAK,QAAuD;AACjE,UAAM,QAAQ,OAAO,SAAS;AAC9B,QAAI,UAAU;AACd,eAAW,CAAC,KAAK,IAAI,KAAK,KAAK,OAAO;AACpC,YAAM,MAAM,YAAY,GAAG;AAC3B,UAAI,CAAC,IAAK;AACV,UAAI,CAAC,eAAe,KAAK,MAAM,EAAG;AAClC,UAAI,OAAO,gBAAgB,CAAC,IAAI,KAAK,SAAS,OAAO,YAAY,EAAG;AACpE,YAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,YAAM,SAA6B;AAAA,QACjC,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY,MAAM,cAAc;AAAA,QAChC,YAAY,MAAM,SAAS,KAAK;AAAA,QAChC,YAAY,MAAM,OAAM,oBAAI,KAAK,CAAC,GAAE,YAAY;AAAA,QAChD,SAAS,MAAM;AAAA,QACf,KAAK,MAAM,OAAO;AAAA,MACpB;AACA,YAAM;AACN,UAAI,EAAE,WAAW,MAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,OAAO,QAAQ,KAAc,OAAuB,CAAC,GAAiC;AACpF,SAAK,YAAY,GAAG;AACpB,UAAM,QAAQ,KAAK,YAAY;AAC/B,UAAM,QAAQ,KAAK,SAAS;AAC5B,QAAI,UAAU;AACd,qBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,UAAI,IAAI,OAAO,MAAO;AACtB,UAAI,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI,IAAI,SAAS,IAAI,KAAM;AAC5D,UAAI,IAAI,IAAI,QAAQ,IAAI,IAAK;AAC7B,YAAM;AACN,UAAI,EAAE,WAAW,MAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAM,QAAqB,OAA8C;AAEvE,UAAM,SAA0B,CAAC;AACjC,UAAM,WAAW,YAAY;AAC3B,uBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,YAAI,WAAW,KAAK,MAAM,EAAG,QAAO,KAAK,GAAG;AAAA,MAC9C;AAAA,IACF,GAAG;AAGH,WAAO,iBAAiB,QAAQ;AAAA,MAAK,MACnC,oBAAoB;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,KAAK;AAAA,QACb,SAAS;AAAA,QACT,aAAa,CAAC,MAAM,EAAE,IAAI;AAAA,MAC5B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA,EAIA,IAAI,KAAc,MAAe,MAAsC;AACrE,SAAK,YAAY,GAAG;AACpB,WAAO,KAAK,MAAM,IAAI,OAAO,GAAG,GAAG,YAAY;AAC7C,YAAM,MAAM,OAAO,GAAG;AACtB,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,WAAK,KAAK,iBAAiB,UAAU,aAAa;AAChD,cAAM,IAAI,cAAc,KAAK,KAAK,iBAAiB,MAAM,WAAW;AAAA,MACtE;AACA,YAAM,OAAO,SAAS,IAAI;AAC1B,UAAI,gBAAgB,MAAM;AAExB,cAAM,OAAO,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACjD,eAAO;AAAA,UACL,SAAS;AAAA,UACT,KAAK,MAAM,OAAO;AAAA,UAClB,MAAM;AAAA,YACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,YAClC,MAAM;AAAA,YACN;AAAA,YACA,YAAY,MAAM,cAAc;AAAA,YAChC,YAAY,MAAM,SAAS,KAAK;AAAA,YAChC,YAAY,MAAM,MAAM,KAAK,IAAI,EAAE,YAAY;AAAA,YAC/C,SAAS,MAAM;AAAA,YACf,KAAK,MAAM,OAAO;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAEA,YAAM,MAAM,KAAK;AACjB,YAAM,KAAK,KAAK,IAAI,EAAE,YAAY;AAClC,YAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AAErD,YAAM,KAAK,WAAW;AACtB,YAAMC,IAAG,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAClE,WAAK,WAAW,IAAI,IAAI;AACxB,UAAI;AACF,cAAM,gBAAgB,MAAM,IAAI;AAAA,MAClC,UAAE;AAGA,mBAAW,MAAM,KAAK,WAAW,OAAO,IAAI,GAAG,GAAG;AAAA,MACpD;AAGA,WAAK,iBAAiB,IAAI;AAC1B,WAAK,MAAM,IAAI,KAAK,IAAI;AAExB,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI,cAAc,WAAW;AAAA,QAC7B,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,QACd;AAAA,QACA,QAAQ,KAAK,UAAU;AAAA,MACzB;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AAEvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT;AAAA,QACA,MAAM;AAAA,UACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,UAClC,MAAM;AAAA,UACN;AAAA,UACA,YAAY;AAAA,UACZ,YAAY,KAAK;AAAA,UACjB,YAAY;AAAA,UACZ,SAAS,KAAK;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,OAAO,KAAc,MAA4C;AAC/D,SAAK,YAAY,GAAG;AACpB,WAAO,KAAK,MAAM,IAAI,OAAO,GAAG,GAAG,YAAY;AAC7C,YAAM,MAAM,OAAO,GAAG;AACtB,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,UAAI,gBAAgB,KAAK,eAAe;AACtC,cAAM,IAAI,cAAc,KAAK,KAAK,eAAe,WAAW;AAAA,MAC9D;AACA,YAAM,OAAO,SAAS,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI;AAErD,YAAM,KAAK,WAAW;AACtB,WAAK,WAAW,IAAI,IAAI;AACxB,UAAI;AACF,YAAID,YAAW,IAAI,EAAG,OAAMC,IAAG,OAAO,IAAI;AAAA,MAC5C,UAAE;AACA,mBAAW,MAAM,KAAK,WAAW,OAAO,IAAI,GAAG,GAAG;AAAA,MACpD;AACA,WAAK,MAAM,OAAO,GAAG;AACrB,YAAM,MAAM,KAAK;AACjB,YAAM,KAAK,KAAK,IAAI,EAAE,YAAY;AAClC,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI;AAAA,QACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,QACd;AAAA,QACA,QAAQ,KAAK,UAAU;AAAA,MACzB;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AACvB,aAAO,EAAE,IAAI;AAAA,IACf,CAAC;AAAA,EACH;AAAA;AAAA,EAIQ,YAAY,KAAoB;AACtC,QAAI,IAAI,QAAQ,KAAK,KAAK;AACxB,YAAM,IAAI;AAAA,QACR,qDAAqD,KAAK,GAAG,aAAa,IAAI,GAAG;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAA2B;AACvC,SAAK,MAAM,MAAM;AAEjB,QAAI,UAAsC,CAAC;AAC3C,QAAI;AACF,gBAAU,MAAMA,IAAG,QAAQ,KAAK,OAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IACtE,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,UAAI,MAAM,KAAK,WAAW,GAAG,EAAG;AAChC,YAAM,OAAO,MAAM;AACnB,YAAM,MAAMF,MAAK,KAAK,KAAK,OAAO,MAAM,IAAI;AAC5C,UAAI,QAAkB,CAAC;AACvB,UAAI;AACF,gBAAQ,MAAME,IAAG,QAAQ,GAAG;AAAA,MAC9B,QAAQ;AACN;AAAA,MACF;AACA,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAC7B,cAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,cAAM,MAAe;AAAA,UACnB,KAAK,KAAK;AAAA,UACV;AAAA,UACA;AAAA,QACF;AACA,cAAM,OAAO,MAAM,SAASF,MAAK,KAAK,KAAK,IAAI,CAAC;AAChD,YAAI,CAAC,KAAM;AACX,aAAK,MAAM,IAAI,OAAO,GAAG,GAAG,SAAS,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,gBACZ,KACA,MAC+B;AAC/B,QAAI,OAA6B;AACjC,qBAAiB,OAAO,KAAK,IAAI,QAAQ,GAAG;AAC1C,UAAI,IAAI,IAAI,SAAS,IAAI,QAAQ,IAAI,IAAI,SAAS,IAAI,KAAM;AAC5D,UAAI,IAAI,IAAI,QAAQ,IAAI,IAAK;AAC7B,UAAI,IAAI,SAAS,KAAM,QAAO;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0CQ,iBAAiB,MAAoB;AAC3C,UAAM,IAAI,KAAK;AAEf,QAAI,CAAC,KAAK,EAAE,OAAQ;AACpB,MAAE,IAAI,IAAI;AAAA,EACZ;AAAA,EAEQ,eAAqB;AAC3B,UAAM,OAAO,KAAK,OAAO;AACzB,UAAM,IAAI,SAAS,MAAM,MAAM;AAAA;AAAA;AAAA;AAAA,MAI7B,SAAS,CAAC,MAAc,mBAAmB,MAAM,CAAC;AAAA,MAClD,eAAe;AAAA,MACf,OAAO;AAAA,MACP,kBAAkB,EAAE,oBAAoB,IAAI,cAAc,GAAG;AAAA;AAAA;AAAA;AAAA,MAI7D,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,gBAAgB;AAAA,IAClB,CAAC;AACD,MAAE,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,KAAK,CAAC;AACrD,MAAE,GAAG,UAAU,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,QAAQ,CAAC;AAC3D,MAAE,GAAG,UAAU,CAAC,MAAM,KAAK,KAAK,eAAe,GAAG,QAAQ,CAAC;AAC3D,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAc,eAAe,SAAiB,MAAkD;AAC9F,QAAI,KAAK,WAAW,IAAI,OAAO,EAAG;AAClC,UAAM,SAAS,cAAc,KAAK,QAAQ,OAAO;AACjD,QAAI,CAAC,OAAQ;AACb,UAAM,MAAe;AAAA,MACnB,KAAK,KAAK;AAAA,MACV,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,IACf;AACA,UAAM,MAAM,OAAO,GAAG;AACtB,UAAM,KAAK,MAAM,IAAI,KAAK,YAAY;AACpC,UAAI,SAAS,UAAU;AACrB,cAAMG,eAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,YAAI,CAACA,aAAa;AAClB,aAAK,MAAM,OAAO,GAAG;AACrB,cAAMC,OAAM,KAAK;AACjB,cAAMC,OAAqB;AAAA,UACzB,KAAAD;AAAA,UACA,IAAI;AAAA,UACJ,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,UAClC,MAAM;AAAA,UACN,YAAYD;AAAA,UACZ,OAAO,KAAK;AAAA,UACZ,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,UAC3B,QAAQ;AAAA,QACV;AACA,cAAM,KAAK,IAAI,OAAOE,IAAG;AACzB,aAAK,OAAO,QAAQA,IAAG;AACvB;AAAA,MACF;AACA,YAAM,OAAO,MAAM,SAAS,OAAO;AACnC,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,SAAS,IAAI;AAC1B,YAAM,cAAc,KAAK,MAAM,IAAI,GAAG,KAAK;AAC3C,UAAI,gBAAgB,KAAM;AAC1B,WAAK,MAAM,IAAI,KAAK,IAAI;AACxB,YAAM,MAAM,KAAK;AACjB,YAAM,MAAqB;AAAA,QACzB;AAAA,QACA,IAAI,cAAc,WAAW;AAAA,QAC7B,KAAK,EAAE,GAAG,KAAK,SAAS,OAAU;AAAA,QAClC;AAAA,QACA,YAAY;AAAA,QACZ,OAAO,KAAK;AAAA,QACZ,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,QAC3B,QAAQ;AAAA,MACV;AACA,YAAM,KAAK,IAAI,OAAO,GAAG;AACzB,WAAK,OAAO,QAAQ,GAAG;AAAA,IACzB,CAAC;AAAA,EACH;AACF;AAkCA,SAAS,mBAAmB,MAAc,SAA0B;AAClE,QAAM,MAAML,MAAK,SAAS,MAAM,OAAO;AAEvC,MAAI,QAAQ,MAAM,IAAI,WAAW,IAAI,EAAG,QAAO;AAC/C,SAAO,IAAI,MAAM,OAAO,EAAE,KAAK,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AACrE;AAEA,eAAe,SAAS,MAAuC;AAC7D,MAAI;AACF,UAAM,OAAO,MAAME,IAAG,SAAS,MAAM,MAAM;AAC3C,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,gBAAgB,MAAc,MAA8B;AACzE,QAAM,MAAM,GAAG,IAAI,IAAI,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;AAChD,QAAMA,IAAG,UAAU,KAAK,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,MAAM,MAAM;AACpE,QAAMA,IAAG,OAAO,KAAK,IAAI;AAC3B;AAEA,SAAS,YAAY,KAA6B;AAChD,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,IACL,KAAK,MAAM,CAAC;AAAA,IACZ,MAAM,MAAM,CAAC;AAAA,IACb,MAAM,MAAM,CAAC;AAAA,EACf;AACF;AAMA,SAAS,iBAAoB,SAAsD;AACjF,SAAO;AAAA,IACL,CAAC,OAAO,aAAa,IAAI;AACvB,UAAI,QAAiC;AACrC,aAAO;AAAA,QACL,MAAM,OAAO;AACX,cAAI,CAAC,OAAO;AACV,kBAAM,WAAW,MAAM;AACvB,oBAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,UACzC;AACA,iBAAO,MAAM,KAAK;AAAA,QACpB;AAAA,QACA,MAAM,OAAO,OAAiB;AAC5B,cAAI,CAAC,OAAO;AACV,kBAAM,WAAW,MAAM;AACvB,oBAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,UACzC;AACA,cAAI,MAAM,OAAQ,QAAO,MAAM,OAAO,KAAK;AAC3C,iBAAO,EAAE,OAAO,QAAW,MAAM,KAAK;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["fs","existsSync","path","path","path","existsSync","fs","currentHead","seq","evt"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/metadata-fs",
|
|
3
|
-
"version": "17.0.0-rc.
|
|
3
|
+
"version": "17.0.0-rc.6",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"chokidar": "^5.0.0",
|
|
29
|
-
"@objectstack/metadata-core": "17.0.0-rc.
|
|
29
|
+
"@objectstack/metadata-core": "17.0.0-rc.6"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.1.2",
|