mrplex 0.0.1
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/LICENSE +21 -0
- package/README.md +284 -0
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +13 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.js +148 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +1518 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/kernel-client.d.ts +97 -0
- package/dist/client/kernel-client.js +14 -0
- package/dist/client/kernel-client.js.map +1 -0
- package/dist/client/local.d.ts +26 -0
- package/dist/client/local.js +103 -0
- package/dist/client/local.js.map +1 -0
- package/dist/client/remote-mcp.d.ts +35 -0
- package/dist/client/remote-mcp.js +174 -0
- package/dist/client/remote-mcp.js.map +1 -0
- package/dist/embed/backfill.d.ts +24 -0
- package/dist/embed/backfill.js +33 -0
- package/dist/embed/backfill.js.map +1 -0
- package/dist/embed/chunker.d.ts +31 -0
- package/dist/embed/chunker.js +121 -0
- package/dist/embed/chunker.js.map +1 -0
- package/dist/embed/cmd-hook.d.ts +21 -0
- package/dist/embed/cmd-hook.js +100 -0
- package/dist/embed/cmd-hook.js.map +1 -0
- package/dist/embed/config.d.ts +61 -0
- package/dist/embed/config.js +109 -0
- package/dist/embed/config.js.map +1 -0
- package/dist/embed/hook.d.ts +38 -0
- package/dist/embed/hook.js +56 -0
- package/dist/embed/hook.js.map +1 -0
- package/dist/embed/http-hook.d.ts +13 -0
- package/dist/embed/http-hook.js +39 -0
- package/dist/embed/http-hook.js.map +1 -0
- package/dist/embed/worker.d.ts +62 -0
- package/dist/embed/worker.js +228 -0
- package/dist/embed/worker.js.map +1 -0
- package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
- package/dist/fixtures/hwe/characters/cent.md +11 -0
- package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
- package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
- package/dist/fixtures/hwe/entities/demiurge.md +11 -0
- package/dist/fixtures/hwe/entities/monad.md +11 -0
- package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
- package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
- package/dist/fixtures/hwe/readme.md +24 -0
- package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
- package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
- package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
- package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
- package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
- package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
- package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
- package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
- package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
- package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
- package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
- package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
- package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
- package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
- package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
- package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
- package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
- package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
- package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
- package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
- package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
- package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
- package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
- package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
- package/dist/fixtures/starship/moc/crew.md +19 -0
- package/dist/fixtures/starship/moc/encounters.md +14 -0
- package/dist/fixtures/starship/moc/missions.md +15 -0
- package/dist/fixtures/starship/readme.md +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/auth/glob.d.ts +46 -0
- package/dist/kernel/auth/glob.js +112 -0
- package/dist/kernel/auth/glob.js.map +1 -0
- package/dist/kernel/auth/scope.d.ts +53 -0
- package/dist/kernel/auth/scope.js +83 -0
- package/dist/kernel/auth/scope.js.map +1 -0
- package/dist/kernel/casefold.d.ts +37 -0
- package/dist/kernel/casefold.js +44 -0
- package/dist/kernel/casefold.js.map +1 -0
- package/dist/kernel/constants.d.ts +21 -0
- package/dist/kernel/constants.js +22 -0
- package/dist/kernel/constants.js.map +1 -0
- package/dist/kernel/context.d.ts +70 -0
- package/dist/kernel/context.js +114 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/deletion.d.ts +50 -0
- package/dist/kernel/deletion.js +83 -0
- package/dist/kernel/deletion.js.map +1 -0
- package/dist/kernel/diff.d.ts +36 -0
- package/dist/kernel/diff.js +61 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +37 -0
- package/dist/kernel/errors.js +55 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/frontmatter-input.d.ts +32 -0
- package/dist/kernel/frontmatter-input.js +85 -0
- package/dist/kernel/frontmatter-input.js.map +1 -0
- package/dist/kernel/graph.d.ts +34 -0
- package/dist/kernel/graph.js +559 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/history.d.ts +74 -0
- package/dist/kernel/history.js +143 -0
- package/dist/kernel/history.js.map +1 -0
- package/dist/kernel/kernel.d.ts +162 -0
- package/dist/kernel/kernel.js +675 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/kernel/path-config.d.ts +101 -0
- package/dist/kernel/path-config.js +171 -0
- package/dist/kernel/path-config.js.map +1 -0
- package/dist/kernel/query/ast.d.ts +24 -0
- package/dist/kernel/query/ast.js +52 -0
- package/dist/kernel/query/ast.js.map +1 -0
- package/dist/kernel/query/cel-parse.d.ts +37 -0
- package/dist/kernel/query/cel-parse.js +88 -0
- package/dist/kernel/query/cel-parse.js.map +1 -0
- package/dist/kernel/query/degrees.d.ts +31 -0
- package/dist/kernel/query/degrees.js +135 -0
- package/dist/kernel/query/degrees.js.map +1 -0
- package/dist/kernel/query/graph-ast.d.ts +51 -0
- package/dist/kernel/query/graph-ast.js +139 -0
- package/dist/kernel/query/graph-ast.js.map +1 -0
- package/dist/kernel/query/query.d.ts +56 -0
- package/dist/kernel/query/query.js +276 -0
- package/dist/kernel/query/query.js.map +1 -0
- package/dist/kernel/validation.d.ts +64 -0
- package/dist/kernel/validation.js +183 -0
- package/dist/kernel/validation.js.map +1 -0
- package/dist/kernel/version-id.d.ts +10 -0
- package/dist/kernel/version-id.js +21 -0
- package/dist/kernel/version-id.js.map +1 -0
- package/dist/kernel/wire.d.ts +148 -0
- package/dist/kernel/wire.js +6 -0
- package/dist/kernel/wire.js.map +1 -0
- package/dist/links/backfill.d.ts +28 -0
- package/dist/links/backfill.js +33 -0
- package/dist/links/backfill.js.map +1 -0
- package/dist/links/extract.d.ts +29 -0
- package/dist/links/extract.js +233 -0
- package/dist/links/extract.js.map +1 -0
- package/dist/links/link-config.d.ts +70 -0
- package/dist/links/link-config.js +107 -0
- package/dist/links/link-config.js.map +1 -0
- package/dist/links/maintain.d.ts +37 -0
- package/dist/links/maintain.js +91 -0
- package/dist/links/maintain.js.map +1 -0
- package/dist/links/repair.d.ts +46 -0
- package/dist/links/repair.js +96 -0
- package/dist/links/repair.js.map +1 -0
- package/dist/links/resolve.d.ts +41 -0
- package/dist/links/resolve.js +121 -0
- package/dist/links/resolve.js.map +1 -0
- package/dist/links/stale.d.ts +32 -0
- package/dist/links/stale.js +73 -0
- package/dist/links/stale.js.map +1 -0
- package/dist/markdown/content-hash.d.ts +34 -0
- package/dist/markdown/content-hash.js +46 -0
- package/dist/markdown/content-hash.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +69 -0
- package/dist/markdown/frontmatter.js +147 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/hash-backfill.d.ts +22 -0
- package/dist/markdown/hash-backfill.js +43 -0
- package/dist/markdown/hash-backfill.js.map +1 -0
- package/dist/mcp/query-syntax.d.ts +20 -0
- package/dist/mcp/query-syntax.js +194 -0
- package/dist/mcp/query-syntax.js.map +1 -0
- package/dist/mcp/render.d.ts +31 -0
- package/dist/mcp/render.js +110 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +60 -0
- package/dist/mcp/server.js +170 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +93 -0
- package/dist/mcp/tools.js +1218 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rest/conditional.d.ts +37 -0
- package/dist/rest/conditional.js +58 -0
- package/dist/rest/conditional.js.map +1 -0
- package/dist/rest/negotiate.d.ts +33 -0
- package/dist/rest/negotiate.js +54 -0
- package/dist/rest/negotiate.js.map +1 -0
- package/dist/rest/routes.d.ts +37 -0
- package/dist/rest/routes.js +744 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/seed/fixture-seed.d.ts +35 -0
- package/dist/seed/fixture-seed.js +75 -0
- package/dist/seed/fixture-seed.js.map +1 -0
- package/dist/seed/starship-config.d.ts +3 -0
- package/dist/seed/starship-config.js +5 -0
- package/dist/seed/starship-config.js.map +1 -0
- package/dist/server/headers.d.ts +36 -0
- package/dist/server/headers.js +30 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/http-error.d.ts +53 -0
- package/dist/server/http-error.js +104 -0
- package/dist/server/http-error.js.map +1 -0
- package/dist/server/serve.d.ts +56 -0
- package/dist/server/serve.js +135 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/shell/audit.d.ts +32 -0
- package/dist/shell/audit.js +28 -0
- package/dist/shell/audit.js.map +1 -0
- package/dist/shell/guard.d.ts +50 -0
- package/dist/shell/guard.js +176 -0
- package/dist/shell/guard.js.map +1 -0
- package/dist/shell/keys.d.ts +36 -0
- package/dist/shell/keys.js +64 -0
- package/dist/shell/keys.js.map +1 -0
- package/dist/shell/login.d.ts +56 -0
- package/dist/shell/login.js +130 -0
- package/dist/shell/login.js.map +1 -0
- package/dist/shell/oidc.d.ts +62 -0
- package/dist/shell/oidc.js +102 -0
- package/dist/shell/oidc.js.map +1 -0
- package/dist/shell/policy.d.ts +100 -0
- package/dist/shell/policy.js +232 -0
- package/dist/shell/policy.js.map +1 -0
- package/dist/shell/proxy-policy.d.ts +34 -0
- package/dist/shell/proxy-policy.js +120 -0
- package/dist/shell/proxy-policy.js.map +1 -0
- package/dist/shell/proxy.d.ts +47 -0
- package/dist/shell/proxy.js +253 -0
- package/dist/shell/proxy.js.map +1 -0
- package/dist/shell/serve.d.ts +86 -0
- package/dist/shell/serve.js +0 -0
- package/dist/shell/serve.js.map +1 -0
- package/dist/shell/stdio.d.ts +55 -0
- package/dist/shell/stdio.js +63 -0
- package/dist/shell/stdio.js.map +1 -0
- package/dist/storage/registry.d.ts +20 -0
- package/dist/storage/registry.js +38 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/search-plan.d.ts +93 -0
- package/dist/storage/search-plan.js +15 -0
- package/dist/storage/search-plan.js.map +1 -0
- package/dist/storage/types.d.ts +448 -0
- package/dist/storage/types.js +14 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/versions-since.d.ts +72 -0
- package/dist/storage/versions-since.js +92 -0
- package/dist/storage/versions-since.js.map +1 -0
- package/dist/storage-postgres/adapter.d.ts +17 -0
- package/dist/storage-postgres/adapter.js +782 -0
- package/dist/storage-postgres/adapter.js.map +1 -0
- package/dist/storage-postgres/compile-postgres.d.ts +22 -0
- package/dist/storage-postgres/compile-postgres.js +830 -0
- package/dist/storage-postgres/compile-postgres.js.map +1 -0
- package/dist/storage-postgres/errors.d.ts +34 -0
- package/dist/storage-postgres/errors.js +50 -0
- package/dist/storage-postgres/errors.js.map +1 -0
- package/dist/storage-postgres/migrations/0001_init.sql +113 -0
- package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-postgres/migrations/index.d.ts +22 -0
- package/dist/storage-postgres/migrations/index.js +70 -0
- package/dist/storage-postgres/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/adapter.d.ts +2 -0
- package/dist/storage-sqlite/adapter.js +688 -0
- package/dist/storage-sqlite/adapter.js.map +1 -0
- package/dist/storage-sqlite/compile-filter.d.ts +41 -0
- package/dist/storage-sqlite/compile-filter.js +881 -0
- package/dist/storage-sqlite/compile-filter.js.map +1 -0
- package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
- package/dist/storage-sqlite/compile-sqlite.js +176 -0
- package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
- package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
- package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-sqlite/migrations/index.d.ts +6 -0
- package/dist/storage-sqlite/migrations/index.js +41 -0
- package/dist/storage-sqlite/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/vec.d.ts +40 -0
- package/dist/storage-sqlite/vec.js +62 -0
- package/dist/storage-sqlite/vec.js.map +1 -0
- package/dist/sync/cursor.d.ts +36 -0
- package/dist/sync/cursor.js +66 -0
- package/dist/sync/cursor.js.map +1 -0
- package/dist/sync/daemon.d.ts +40 -0
- package/dist/sync/daemon.js +202 -0
- package/dist/sync/daemon.js.map +1 -0
- package/dist/sync/feed.d.ts +38 -0
- package/dist/sync/feed.js +125 -0
- package/dist/sync/feed.js.map +1 -0
- package/dist/sync/fs-store.d.ts +8 -0
- package/dist/sync/fs-store.js +76 -0
- package/dist/sync/fs-store.js.map +1 -0
- package/dist/sync/intrinsics.d.ts +52 -0
- package/dist/sync/intrinsics.js +76 -0
- package/dist/sync/intrinsics.js.map +1 -0
- package/dist/sync/paths.d.ts +38 -0
- package/dist/sync/paths.js +56 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/push.d.ts +48 -0
- package/dist/sync/push.js +328 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/reconcile.d.ts +54 -0
- package/dist/sync/reconcile.js +252 -0
- package/dist/sync/reconcile.js.map +1 -0
- package/dist/sync/run.d.ts +27 -0
- package/dist/sync/run.js +46 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one persistent client state: a single cursor (sync/history plan §4.2).
|
|
3
|
+
* `<root>/.mrplex/sync.json` holds `{ server | database, repo,
|
|
4
|
+
* last_synced_version_id }`, meaning "every change at or before this position
|
|
5
|
+
* in the log is reflected on disk." The source is recorded as exactly one of
|
|
6
|
+
* `server` (remote sync) or `database` (local sync) — they are mutually
|
|
7
|
+
* exclusive, mirroring the CLI's --server/--database gate. In-vault so it
|
|
8
|
+
* travels with the vault it describes. There is no per-file manifest or
|
|
9
|
+
* tombstone journal.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { CURSOR_FILE } from "./paths.js";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the cursor's source fields (§4.2). The source is exactly one of
|
|
16
|
+
* `server` (remote sync) or `database` (local sync); a set `server` always
|
|
17
|
+
* wins, otherwise a set `database` is recorded. When neither is supplied on
|
|
18
|
+
* this run, carry forward whatever the existing marker held so an incidental
|
|
19
|
+
* re-run (e.g. via a different code path) doesn't strip provenance.
|
|
20
|
+
*/
|
|
21
|
+
export function sourceFields(server, database, existing) {
|
|
22
|
+
if (server !== undefined)
|
|
23
|
+
return { server };
|
|
24
|
+
if (database !== undefined)
|
|
25
|
+
return { database };
|
|
26
|
+
if (existing?.server !== undefined)
|
|
27
|
+
return { server: existing.server };
|
|
28
|
+
if (existing?.database !== undefined)
|
|
29
|
+
return { database: existing.database };
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read the cursor file, or null when absent (marker absent → full index
|
|
34
|
+
* reconciliation, §4.9). A malformed file throws — the caller must not silently
|
|
35
|
+
* treat corruption as "start fresh."
|
|
36
|
+
*/
|
|
37
|
+
export async function readCursor(root) {
|
|
38
|
+
const path = join(root, CURSOR_FILE);
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = await readFile(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === "ENOENT")
|
|
45
|
+
return null;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
const parsed = JSON.parse(text);
|
|
49
|
+
if (typeof parsed.last_synced_version_id !== "string") {
|
|
50
|
+
throw new Error(`${path}: missing last_synced_version_id`);
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Write the cursor atomically (temp + rename) so a crash mid-write can't leave
|
|
56
|
+
* a torn file. The cursor advances only after a batch's filesystem effects
|
|
57
|
+
* complete (§4.3), so this is called at safe points.
|
|
58
|
+
*/
|
|
59
|
+
export async function writeCursor(root, cursor) {
|
|
60
|
+
const path = join(root, CURSOR_FILE);
|
|
61
|
+
await mkdir(dirname(path), { recursive: true });
|
|
62
|
+
const tmp = `${path}.tmp`;
|
|
63
|
+
await writeFile(tmp, `${JSON.stringify(cursor, null, 2)}\n`, "utf8");
|
|
64
|
+
await rename(tmp, path);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/sync/cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AASzC;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA0B,EAC1B,QAA4B,EAC5B,QAA2B;IAE3B,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC5C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAChD,IAAI,QAAQ,EAAE,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACvE,IAAI,QAAQ,EAAE,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAC7E,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACrC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;IAC9C,IAAI,OAAO,MAAM,CAAC,sBAAsB,KAAK,QAAQ,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,kCAAkC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,MAAkB;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACrC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,MAAM,SAAS,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrE,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-way sync daemon (sync/history plan §4.3–4.8, M8). Startup
|
|
3
|
+
* reconciliation, then two steady-state loops:
|
|
4
|
+
*
|
|
5
|
+
* • remote → local: poll history.since(cursor) every --interval, apply refs
|
|
6
|
+
* (feed.ts), advance the cursor after fs effects.
|
|
7
|
+
* • local → remote: a chokidar watcher coalesces events into a debounce
|
|
8
|
+
* *burst* (all paths that settled together). The burst runs present files
|
|
9
|
+
* first so unlink+add of a rename is a move, not a delete (§4.7); each
|
|
10
|
+
* path is still stated, not trusted by event type (§4.6).
|
|
11
|
+
*
|
|
12
|
+
* chokidar is confined to this module (§4.4). Echo suppression falls out of
|
|
13
|
+
* self-description (§4.5): our own pushes come back on the feed but the local
|
|
14
|
+
* file already embeds that version+hash, and our own writes trip the watcher
|
|
15
|
+
* but the hash gate no-ops them.
|
|
16
|
+
*/
|
|
17
|
+
import type { KernelClient } from "../client/kernel-client.js";
|
|
18
|
+
export type DaemonOptions = {
|
|
19
|
+
root: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
server?: string;
|
|
22
|
+
database?: string;
|
|
23
|
+
include?: string[];
|
|
24
|
+
exclude?: string[];
|
|
25
|
+
intervalMs?: number;
|
|
26
|
+
debounceMs?: number;
|
|
27
|
+
settleMs?: number;
|
|
28
|
+
log?: (msg: string) => void;
|
|
29
|
+
};
|
|
30
|
+
export type Daemon = {
|
|
31
|
+
/** Resolves once the initial reconciliation + first feed drain complete. */
|
|
32
|
+
ready: Promise<void>;
|
|
33
|
+
/** Stop the watcher and poll loop; resolves when fully torn down. */
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Start the daemon. Returns handles rather than blocking, so the CLI (or a
|
|
38
|
+
* test) controls its lifetime. The poll loop and watcher run until `stop()`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function startDaemon(client: KernelClient, opts: DaemonOptions): Daemon;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two-way sync daemon (sync/history plan §4.3–4.8, M8). Startup
|
|
3
|
+
* reconciliation, then two steady-state loops:
|
|
4
|
+
*
|
|
5
|
+
* • remote → local: poll history.since(cursor) every --interval, apply refs
|
|
6
|
+
* (feed.ts), advance the cursor after fs effects.
|
|
7
|
+
* • local → remote: a chokidar watcher coalesces events into a debounce
|
|
8
|
+
* *burst* (all paths that settled together). The burst runs present files
|
|
9
|
+
* first so unlink+add of a rename is a move, not a delete (§4.7); each
|
|
10
|
+
* path is still stated, not trusted by event type (§4.6).
|
|
11
|
+
*
|
|
12
|
+
* chokidar is confined to this module (§4.4). Echo suppression falls out of
|
|
13
|
+
* self-description (§4.5): our own pushes come back on the feed but the local
|
|
14
|
+
* file already embeds that version+hash, and our own writes trip the watcher
|
|
15
|
+
* but the hash gate no-ops them.
|
|
16
|
+
*/
|
|
17
|
+
import { watch } from "chokidar";
|
|
18
|
+
import { readCursor, sourceFields, writeCursor } from "./cursor.js";
|
|
19
|
+
import { applyFeed } from "./feed.js";
|
|
20
|
+
import { createFsStore } from "./fs-store.js";
|
|
21
|
+
import { isIgnored, readFileIntrinsics } from "./intrinsics.js";
|
|
22
|
+
import { SYNC_DIR, makeScopeFilter, toDocPath } from "./paths.js";
|
|
23
|
+
import { pushBurst, pushPath } from "./push.js";
|
|
24
|
+
import { reconcileOnce } from "./reconcile.js";
|
|
25
|
+
/**
|
|
26
|
+
* Start the daemon. Returns handles rather than blocking, so the CLI (or a
|
|
27
|
+
* test) controls its lifetime. The poll loop and watcher run until `stop()`.
|
|
28
|
+
*/
|
|
29
|
+
export function startDaemon(client, opts) {
|
|
30
|
+
const log = opts.log ?? (() => { });
|
|
31
|
+
const store = createFsStore(opts.root);
|
|
32
|
+
const scope = makeScopeFilter({ include: opts.include, exclude: opts.exclude });
|
|
33
|
+
const map = new Map();
|
|
34
|
+
const intervalMs = opts.intervalMs ?? 5000;
|
|
35
|
+
const debounceMs = opts.debounceMs ?? 5000;
|
|
36
|
+
let stopped = false;
|
|
37
|
+
let watcher;
|
|
38
|
+
const pending = new Set();
|
|
39
|
+
let burstTimer;
|
|
40
|
+
let cursor = "";
|
|
41
|
+
let existingCursor = null;
|
|
42
|
+
// Serialize all remote-touching work so a poll and a push never interleave a
|
|
43
|
+
// read-modify-write on the same path.
|
|
44
|
+
let chain = Promise.resolve();
|
|
45
|
+
const serialize = (fn) => {
|
|
46
|
+
chain = chain.then(fn, fn);
|
|
47
|
+
return chain;
|
|
48
|
+
};
|
|
49
|
+
async function persistCursor() {
|
|
50
|
+
await writeCursor(opts.root, {
|
|
51
|
+
...sourceFields(opts.server, opts.database, existingCursor),
|
|
52
|
+
repo: opts.repo,
|
|
53
|
+
last_synced_version_id: cursor,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The marker-present offline reconcile (§4.9): a purely-local walk that
|
|
58
|
+
* pushes offline edits and creations without enumerating the remote. Each
|
|
59
|
+
* file runs the ordinary push pass — clean files no-op via the hash gate,
|
|
60
|
+
* dirty ones push, files lacking provenance resolve the remote path first.
|
|
61
|
+
* Offline deletions and moves reconcile lazily (they are not witnessed), per
|
|
62
|
+
* the plan's stated consequence; a full pass (--once) is the lever for those.
|
|
63
|
+
*/
|
|
64
|
+
async function localDirtyWalk() {
|
|
65
|
+
for (const docPath of await store.list()) {
|
|
66
|
+
if (!scope.matches(docPath))
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
await pushPath(docPath, { client, store, repo: opts.repo, map, log });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
log(`push error\t${docPath}\t${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function pollFeedOnce() {
|
|
77
|
+
const { cursor: next } = await applyFeed(client, store, scope, {
|
|
78
|
+
repo: opts.repo,
|
|
79
|
+
since: cursor,
|
|
80
|
+
log,
|
|
81
|
+
map,
|
|
82
|
+
});
|
|
83
|
+
if (next !== cursor) {
|
|
84
|
+
cursor = next;
|
|
85
|
+
await persistCursor();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function schedulePush(docPath) {
|
|
89
|
+
if (!scope.matches(docPath))
|
|
90
|
+
return;
|
|
91
|
+
pending.add(docPath);
|
|
92
|
+
if (burstTimer)
|
|
93
|
+
clearTimeout(burstTimer);
|
|
94
|
+
burstTimer = setTimeout(() => {
|
|
95
|
+
burstTimer = undefined;
|
|
96
|
+
const batch = [...pending];
|
|
97
|
+
pending.clear();
|
|
98
|
+
void serialize(async () => {
|
|
99
|
+
if (stopped)
|
|
100
|
+
return;
|
|
101
|
+
await pushBurst(batch, { client, store, repo: opts.repo, map, log });
|
|
102
|
+
});
|
|
103
|
+
}, debounceMs);
|
|
104
|
+
}
|
|
105
|
+
const ready = (async () => {
|
|
106
|
+
// 1. Startup is deterministic on the cursor marker (§4.9, §7).
|
|
107
|
+
const existing = await readCursor(opts.root);
|
|
108
|
+
existingCursor = existing;
|
|
109
|
+
// Seed the map from local provenance first, so the dirty walk's witnessed
|
|
110
|
+
// deletes (and the feed's) know each file's prev_version_id.
|
|
111
|
+
await seedMapFromLocal(store, scope, map);
|
|
112
|
+
if (existing === null) {
|
|
113
|
+
// Marker absent → full index reconciliation, then resume the feed from R.
|
|
114
|
+
const report = await reconcileOnce(client, store, scope, { repo: opts.repo, log });
|
|
115
|
+
// reconcileOnce may have materialized/pushed files; re-seed so the map
|
|
116
|
+
// reflects the post-reconcile provenance.
|
|
117
|
+
await seedMapFromLocal(store, scope, map);
|
|
118
|
+
cursor = report.through_version;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Marker present → skip the index scan. Run a purely-local dirty walk
|
|
122
|
+
// (pushes offline edits + creations; no remote enumeration), then resume
|
|
123
|
+
// history.since(cursor). The feed is gap-free from any cursor age (§3.2),
|
|
124
|
+
// so an old marker is merely more replay, never a correctness question.
|
|
125
|
+
// Offline deletions/moves reconcile lazily until a full pass (§4.9).
|
|
126
|
+
await localDirtyWalk();
|
|
127
|
+
cursor = existing.last_synced_version_id;
|
|
128
|
+
}
|
|
129
|
+
await persistCursor();
|
|
130
|
+
// 2. Drain the feed from the cursor (advances + re-persists if it moves).
|
|
131
|
+
await pollFeedOnce();
|
|
132
|
+
// 3. Start the watcher (local → remote).
|
|
133
|
+
watcher = watch(opts.root, {
|
|
134
|
+
ignoreInitial: true,
|
|
135
|
+
// Prune the sync state dir; the scope filter still guards everything else.
|
|
136
|
+
ignored: (p) => p.includes(`/${SYNC_DIR}/`) || p.endsWith(`/${SYNC_DIR}`),
|
|
137
|
+
...(opts.settleMs
|
|
138
|
+
? { awaitWriteFinish: { stabilityThreshold: opts.settleMs, pollInterval: 100 } }
|
|
139
|
+
: {}),
|
|
140
|
+
});
|
|
141
|
+
const onEvent = (abs) => {
|
|
142
|
+
const docPath = toDocPath(opts.root, abs);
|
|
143
|
+
if (docPath !== null)
|
|
144
|
+
schedulePush(docPath);
|
|
145
|
+
};
|
|
146
|
+
watcher.on("add", onEvent).on("change", onEvent).on("unlink", onEvent);
|
|
147
|
+
// Wait for the initial scan to finish so events aren't missed right after
|
|
148
|
+
// ready resolves (chokidar only watches reliably once it emits `ready`).
|
|
149
|
+
const w = watcher;
|
|
150
|
+
await new Promise((resolve) => w.once("ready", () => resolve()));
|
|
151
|
+
// 4. Start the remote → local poll loop.
|
|
152
|
+
void pollLoop();
|
|
153
|
+
})();
|
|
154
|
+
async function pollLoop() {
|
|
155
|
+
while (!stopped) {
|
|
156
|
+
await sleep(intervalMs);
|
|
157
|
+
if (stopped)
|
|
158
|
+
break;
|
|
159
|
+
await serialize(async () => {
|
|
160
|
+
if (stopped)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
await pollFeedOnce();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
log(`poll error\t${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
ready,
|
|
173
|
+
async stop() {
|
|
174
|
+
stopped = true;
|
|
175
|
+
if (burstTimer)
|
|
176
|
+
clearTimeout(burstTimer);
|
|
177
|
+
burstTimer = undefined;
|
|
178
|
+
pending.clear();
|
|
179
|
+
if (watcher)
|
|
180
|
+
await watcher.close();
|
|
181
|
+
await chain.catch(() => { });
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Seed the in-memory map from the local files' embedded provenance (§4.2). */
|
|
186
|
+
async function seedMapFromLocal(store, scope, map) {
|
|
187
|
+
for (const path of await store.list()) {
|
|
188
|
+
if (!scope.matches(path))
|
|
189
|
+
continue;
|
|
190
|
+
const text = await store.read(path);
|
|
191
|
+
if (text === null)
|
|
192
|
+
continue;
|
|
193
|
+
const intr = readFileIntrinsics(text);
|
|
194
|
+
if (isIgnored(intr) || !intr.version || !intr.content_hash)
|
|
195
|
+
continue;
|
|
196
|
+
map.set(path, { version_id: intr.version, content_hash: intr.content_hash });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function sleep(ms) {
|
|
200
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.js","sourceRoot":"","sources":["../../src/sync/daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote → local: consume the change feed (sync/history plan §4.3). Drains
|
|
3
|
+
* `history.since` from a cursor and applies each ref to the local store, then
|
|
4
|
+
* returns the final cursor and a count. Shared by `--once` (drain to the tip
|
|
5
|
+
* after reconciliation) and the daemon's poll loop (M8).
|
|
6
|
+
*
|
|
7
|
+
* Every operation is idempotent (§4.3): same bytes → same file; removing an
|
|
8
|
+
* absent file is a no-op; the dirty check prevents a replay from clobbering an
|
|
9
|
+
* edit made in a crash window. So a crash between apply and cursor-advance
|
|
10
|
+
* simply replays the batch harmlessly.
|
|
11
|
+
*/
|
|
12
|
+
import type { KernelClient } from "../client/kernel-client.js";
|
|
13
|
+
import type { ScopeFilter } from "./paths.js";
|
|
14
|
+
import type { RemoteMap } from "./push.js";
|
|
15
|
+
import type { FileStore } from "./reconcile.js";
|
|
16
|
+
export type ApplyFeedOptions = {
|
|
17
|
+
repo: string;
|
|
18
|
+
since: string;
|
|
19
|
+
log?: (msg: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Optional daemon working-map (§4.2 tier 3). When supplied, feed application
|
|
22
|
+
* keeps it current — set on materialize, delete on remove — so a later
|
|
23
|
+
* witnessed unlink knows the file's prev_version_id.
|
|
24
|
+
*/
|
|
25
|
+
map?: RemoteMap;
|
|
26
|
+
};
|
|
27
|
+
export type ApplyFeedResult = {
|
|
28
|
+
/** Final resume cursor after draining the currently-safe feed. */
|
|
29
|
+
cursor: string;
|
|
30
|
+
/** Number of refs whose filesystem effect was applied. */
|
|
31
|
+
applied: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Drain the safe feed from `since` to the live tip, applying each in-scope ref.
|
|
35
|
+
* Returns the final cursor to persist. Stops when a poll returns no new refs
|
|
36
|
+
* (caught up, or stalled at a hot gap — the caller decides whether to re-poll).
|
|
37
|
+
*/
|
|
38
|
+
export declare function applyFeed(client: KernelClient, store: FileStore, scope: ScopeFilter, opts: ApplyFeedOptions): Promise<ApplyFeedResult>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote → local: consume the change feed (sync/history plan §4.3). Drains
|
|
3
|
+
* `history.since` from a cursor and applies each ref to the local store, then
|
|
4
|
+
* returns the final cursor and a count. Shared by `--once` (drain to the tip
|
|
5
|
+
* after reconciliation) and the daemon's poll loop (M8).
|
|
6
|
+
*
|
|
7
|
+
* Every operation is idempotent (§4.3): same bytes → same file; removing an
|
|
8
|
+
* absent file is a no-op; the dirty check prevents a replay from clobbering an
|
|
9
|
+
* edit made in a crash window. So a crash between apply and cursor-advance
|
|
10
|
+
* simply replays the batch harmlessly.
|
|
11
|
+
*/
|
|
12
|
+
import { withVersionSuffix } from "../kernel/deletion.js";
|
|
13
|
+
import { decodeVersionId } from "../kernel/version-id.js";
|
|
14
|
+
import { isDirty, isIgnored, readFileIntrinsics, renderIgnoredSibling, renderMaterialized, } from "./intrinsics.js";
|
|
15
|
+
/**
|
|
16
|
+
* Drain the safe feed from `since` to the live tip, applying each in-scope ref.
|
|
17
|
+
* Returns the final cursor to persist. Stops when a poll returns no new refs
|
|
18
|
+
* (caught up, or stalled at a hot gap — the caller decides whether to re-poll).
|
|
19
|
+
*/
|
|
20
|
+
export async function applyFeed(client, store, scope, opts) {
|
|
21
|
+
const log = opts.log ?? (() => { });
|
|
22
|
+
let cursor = opts.since;
|
|
23
|
+
let applied = 0;
|
|
24
|
+
for (;;) {
|
|
25
|
+
const page = await client.history.since({ after_version: cursor, repo: opts.repo });
|
|
26
|
+
for (const ref of page.refs) {
|
|
27
|
+
if (await applyRef(client, store, scope, opts.repo, ref, log, opts.map))
|
|
28
|
+
applied++;
|
|
29
|
+
}
|
|
30
|
+
// No forward progress → caught up (or a hot gap). Stop draining.
|
|
31
|
+
if (page.next_since === cursor || page.refs.length === 0) {
|
|
32
|
+
cursor = page.next_since;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
cursor = page.next_since;
|
|
36
|
+
}
|
|
37
|
+
return { cursor, applied };
|
|
38
|
+
}
|
|
39
|
+
/** Apply one feed ref to the local store. Returns true if it changed a file. */
|
|
40
|
+
async function applyRef(client, store, scope, repo, ref, log, map) {
|
|
41
|
+
if (ref.op === "delete") {
|
|
42
|
+
// Remove the local file at prev_path IF clean; dirty ⇒ keep (resurrection
|
|
43
|
+
// happens on its next local-change cycle). §4.3 delete.
|
|
44
|
+
const target = ref.prev_path;
|
|
45
|
+
if (target === null || !scope.matches(target))
|
|
46
|
+
return false;
|
|
47
|
+
map?.delete(target);
|
|
48
|
+
const text = await store.read(target);
|
|
49
|
+
if (text === null)
|
|
50
|
+
return false;
|
|
51
|
+
const intr = readFileIntrinsics(text);
|
|
52
|
+
if (isIgnored(intr) || isDirty(intr))
|
|
53
|
+
return false; // preserve local work
|
|
54
|
+
await store.remove(target);
|
|
55
|
+
log(`feed delete\t${target}`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (ref.op === "move") {
|
|
59
|
+
const from = ref.prev_path;
|
|
60
|
+
if (from !== null && scope.matches(from)) {
|
|
61
|
+
map?.delete(from);
|
|
62
|
+
const text = await store.read(from);
|
|
63
|
+
if (text !== null) {
|
|
64
|
+
const intr = readFileIntrinsics(text);
|
|
65
|
+
if (!isIgnored(intr) && !isDirty(intr))
|
|
66
|
+
await store.remove(from);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Fall through to materialize at the destination path.
|
|
70
|
+
}
|
|
71
|
+
// create / update / move-destination: materialize at ref.path unless the
|
|
72
|
+
// local file already holds these bytes, is at-or-ahead of this ref, is
|
|
73
|
+
// dirty against a *newer* remote, or is $sync: ignore.
|
|
74
|
+
if (!scope.matches(ref.path))
|
|
75
|
+
return false;
|
|
76
|
+
const existing = await store.read(ref.path);
|
|
77
|
+
if (existing !== null) {
|
|
78
|
+
const intr = readFileIntrinsics(existing);
|
|
79
|
+
if (isIgnored(intr))
|
|
80
|
+
return false;
|
|
81
|
+
if (intr.computed_hash === ref.content_hash) {
|
|
82
|
+
// Bytes already present (our own push echoing back, or a vault copy).
|
|
83
|
+
map?.set(ref.path, { version_id: ref.version_id, content_hash: ref.content_hash });
|
|
84
|
+
// Repair provenance only if the embedded version lags.
|
|
85
|
+
if (intr.version === ref.version_id)
|
|
86
|
+
return false;
|
|
87
|
+
if (versionAtOrAhead(intr.version, ref.version_id))
|
|
88
|
+
return false;
|
|
89
|
+
const v = await client.docs.get_version(repo, ref.version_id);
|
|
90
|
+
await store.write(ref.path, renderMaterialized(v), { preserveMtime: true });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// Local is at or ahead of this ref (echo of our push, or a replay of an
|
|
94
|
+
// older version). Never clobber and never park a sibling of something we
|
|
95
|
+
// already have — including when the user has typed more since (dirty).
|
|
96
|
+
if (versionAtOrAhead(intr.version, ref.version_id))
|
|
97
|
+
return false;
|
|
98
|
+
if (isDirty(intr)) {
|
|
99
|
+
// A local edit collides with a *newer* incoming version → conflict, not
|
|
100
|
+
// overwrite. Park the remote as an ignored sibling; keep local bytes.
|
|
101
|
+
const v = await client.docs.get_version(repo, ref.version_id);
|
|
102
|
+
await store.write(withVersionSuffix(ref.path, ref.version_id), renderIgnoredSibling(v));
|
|
103
|
+
log(`feed conflict\t${ref.path}`);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const v = await client.docs.get_version(repo, ref.version_id);
|
|
108
|
+
await store.write(ref.path, renderMaterialized(v));
|
|
109
|
+
map?.set(ref.path, { version_id: ref.version_id, content_hash: ref.content_hash });
|
|
110
|
+
log(`feed ${ref.op}\t${ref.path}`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/** True when the local embedded version is this ref or a later one. */
|
|
114
|
+
function versionAtOrAhead(localVersion, refVersion) {
|
|
115
|
+
if (!localVersion)
|
|
116
|
+
return false;
|
|
117
|
+
if (localVersion === refVersion)
|
|
118
|
+
return true;
|
|
119
|
+
const local = decodeVersionId(localVersion);
|
|
120
|
+
const ref = decodeVersionId(refVersion);
|
|
121
|
+
if (local === null || ref === null)
|
|
122
|
+
return false;
|
|
123
|
+
return local > ref;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=feed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed.js","sourceRoot":"","sources":["../../src/sync/feed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,EACL,OAAO,EACP,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAwBzB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAoB,EACpB,KAAgB,EAChB,KAAkB,EAClB,IAAsB;IAEtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnC,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC;IACxB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACpF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,MAAM,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC;gBAAE,OAAO,EAAE,CAAC;QACrF,CAAC;QACD,iEAAiE;QACjE,IAAI,IAAI,CAAC,UAAU,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzD,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;YACzB,MAAM;QACR,CAAC;QACD,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED,gFAAgF;AAChF,KAAK,UAAU,QAAQ,CACrB,MAAoB,EACpB,KAAgB,EAChB,KAAkB,EAClB,IAAY,EACZ,GAAe,EACf,GAA0B,EAC1B,GAAe;IAEf,IAAI,GAAG,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;QACxB,0EAA0E;QAC1E,wDAAwD;QACxD,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC;QAC7B,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;QAC5D,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACpB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAChC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,sBAAsB;QAC1E,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3B,GAAG,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,GAAG,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC;QAC3B,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBACtC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAAE,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACnE,CAAC;QACH,CAAC;QACD,uDAAuD;IACzD,CAAC;IAED,yEAAyE;IACzE,uEAAuE;IACvE,uDAAuD;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAClC,IAAI,IAAI,CAAC,aAAa,KAAK,GAAG,CAAC,YAAY,EAAE,CAAC;YAC5C,sEAAsE;YACtE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;YACnF,uDAAuD;YACvD,IAAI,IAAI,CAAC,OAAO,KAAK,GAAG,CAAC,UAAU;gBAAE,OAAO,KAAK,CAAC;YAClD,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC;gBAAE,OAAO,KAAK,CAAC;YACjE,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;YAC9D,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,wEAAwE;QACxE,yEAAyE;QACzE,uEAAuE;QACvE,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC;YAAE,OAAO,KAAK,CAAC;QACjE,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAClB,wEAAwE;YACxE,sEAAsE;YACtE,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;YAC9D,MAAM,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;YACxF,GAAG,CAAC,kBAAkB,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;IACnF,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uEAAuE;AACvE,SAAS,gBAAgB,CAAC,YAAgC,EAAE,UAAkB;IAC5E,IAAI,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAChC,IAAI,YAAY,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACjD,OAAO,KAAK,GAAG,GAAG,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real filesystem `FileStore` (sync/history plan §4). Maps repo-relative
|
|
3
|
+
* POSIX doc paths to files under `<root>`, walking the vault to enumerate the
|
|
4
|
+
* live set. The `.mrplex/` state dir is skipped by the scope filter, but we
|
|
5
|
+
* also prune it (and dotdirs like `.obsidian/`) during the walk for speed.
|
|
6
|
+
*/
|
|
7
|
+
import type { FileStore } from "./reconcile.js";
|
|
8
|
+
export declare function createFsStore(root: string): FileStore;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real filesystem `FileStore` (sync/history plan §4). Maps repo-relative
|
|
3
|
+
* POSIX doc paths to files under `<root>`, walking the vault to enumerate the
|
|
4
|
+
* live set. The `.mrplex/` state dir is skipped by the scope filter, but we
|
|
5
|
+
* also prune it (and dotdirs like `.obsidian/`) during the walk for speed.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, readFile, readdir, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import { toDocPath, toLocalPath } from "./paths.js";
|
|
10
|
+
export function createFsStore(root) {
|
|
11
|
+
return {
|
|
12
|
+
async list() {
|
|
13
|
+
const out = [];
|
|
14
|
+
await walk(root, root, out);
|
|
15
|
+
return out;
|
|
16
|
+
},
|
|
17
|
+
async read(docPath) {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(toLocalPath(root, docPath), "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err.code === "ENOENT")
|
|
23
|
+
return null;
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async write(docPath, text, opts) {
|
|
28
|
+
const abs = toLocalPath(root, docPath);
|
|
29
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
30
|
+
let mtime;
|
|
31
|
+
if (opts?.preserveMtime) {
|
|
32
|
+
try {
|
|
33
|
+
mtime = (await stat(abs)).mtime;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err.code !== "ENOENT")
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await writeFile(abs, text, "utf8");
|
|
41
|
+
if (mtime)
|
|
42
|
+
await utimes(abs, mtime, mtime);
|
|
43
|
+
},
|
|
44
|
+
async remove(docPath) {
|
|
45
|
+
await rm(toLocalPath(root, docPath), { force: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function walk(root, dir, out) {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
return;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
for (const ent of entries) {
|
|
60
|
+
if (ent.isDirectory()) {
|
|
61
|
+
// Prune any dot-prefixed dir (the `.mrplex/` state dir, app dotdirs like
|
|
62
|
+
// `.obsidian/`). This is exactly what makeScopeFilter's hasDotSegment
|
|
63
|
+
// excludes, so the walk and the scope filter agree in both directions
|
|
64
|
+
// (§4.1); pruning here just avoids the descent.
|
|
65
|
+
if (ent.name.startsWith("."))
|
|
66
|
+
continue;
|
|
67
|
+
await walk(root, `${dir}/${ent.name}`, out);
|
|
68
|
+
}
|
|
69
|
+
else if (ent.isFile()) {
|
|
70
|
+
const docPath = toDocPath(root, `${dir}/${ent.name}`);
|
|
71
|
+
if (docPath !== null)
|
|
72
|
+
out.push(docPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=fs-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-store.js","sourceRoot":"","sources":["../../src/sync/fs-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and writing the self-describing frontmatter that makes sync work
|
|
3
|
+
* (sync/history plan §2.4, §4.2). A materialized file embeds `$version` and
|
|
4
|
+
* `$content_hash`; a user may also add the `$sync: ignore` client directive.
|
|
5
|
+
* Per-file sync state lives *in the file* — this module is the single place
|
|
6
|
+
* that parses it out and renders it back.
|
|
7
|
+
*/
|
|
8
|
+
import type { Version } from "../kernel/wire.js";
|
|
9
|
+
/** The `$sync` directive value we honor (§4.2). Only `ignore` is defined. */
|
|
10
|
+
export declare const SYNC_IGNORE = "ignore";
|
|
11
|
+
export type FileIntrinsics = {
|
|
12
|
+
/** Embedded `$version` (ancestry), or undefined if absent. */
|
|
13
|
+
version?: string;
|
|
14
|
+
/** Embedded `$content_hash` (clean-state fingerprint), or undefined. */
|
|
15
|
+
content_hash?: string;
|
|
16
|
+
/** Embedded `$sync` client directive, or undefined. */
|
|
17
|
+
sync?: string;
|
|
18
|
+
/** The canonical content hash computed from the file's user bytes. */
|
|
19
|
+
computed_hash: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Parse a file's embedded intrinsics and compute its canonical content hash.
|
|
23
|
+
* The computed hash excludes all `$*` lines, so adding/removing intrinsics or
|
|
24
|
+
* the `$sync` directive never changes it (§4.2).
|
|
25
|
+
*/
|
|
26
|
+
export declare function readFileIntrinsics(text: string): FileIntrinsics;
|
|
27
|
+
/** True when the file opts out of sync via `$sync: ignore` (§4.2). */
|
|
28
|
+
export declare function isIgnored(intr: FileIntrinsics): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* A file is "dirty" when its user bytes differ from the version its embedded
|
|
31
|
+
* `$version` names — i.e. the computed hash ≠ the embedded `$content_hash`
|
|
32
|
+
* (§4.4 hash gate). A file with no embedded hash is treated as dirty (we
|
|
33
|
+
* cannot prove it clean).
|
|
34
|
+
*/
|
|
35
|
+
export declare function isDirty(intr: FileIntrinsics): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Render a version as a self-describing file: strip any stale `$*` lines from
|
|
38
|
+
* the stored frontmatter, then append `$version` and `$content_hash` in fixed
|
|
39
|
+
* order (§2.4). Robust whether or not the transport already injected them.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderMaterialized(v: Version): string;
|
|
42
|
+
/**
|
|
43
|
+
* Render a version as a conflict sibling (§4.8): the normal materialization
|
|
44
|
+
* plus `$sync: ignore`, which is the *only* thing keeping it out of sync.
|
|
45
|
+
*/
|
|
46
|
+
export declare function renderIgnoredSibling(v: Version): string;
|
|
47
|
+
/**
|
|
48
|
+
* Stamp `$version` / `$content_hash` onto an existing file without replacing
|
|
49
|
+
* its user bytes. Used when the editor saved again between our kernel write
|
|
50
|
+
* and the ack rewrite — we must not revert their typing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stampProvenance(text: string, versionId: string, contentHash: string): string;
|