cursedbelt 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/core/file-tree/fileTreeModel.d.ts +18 -0
  2. package/dist/core/file-tree/fileTreeModel.d.ts.map +1 -1
  3. package/dist/core/file-tree/fileTreeModel.js +24 -0
  4. package/dist/core/file-tree/fileTreeModel.js.map +1 -1
  5. package/dist/react/components/TagsInput.d.ts +8 -1
  6. package/dist/react/components/TagsInput.d.ts.map +1 -1
  7. package/dist/react/components/TagsInput.js +10 -4
  8. package/dist/react/components/TagsInput.js.map +1 -1
  9. package/dist/react/file-tree/FileTree.d.ts +14 -2
  10. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  11. package/dist/react/file-tree/FileTree.js +3 -9
  12. package/dist/react/file-tree/FileTree.js.map +1 -1
  13. package/dist/react/file-tree/index.d.ts +1 -1
  14. package/dist/react/file-tree/index.d.ts.map +1 -1
  15. package/dist/react/file-tree/index.js +1 -1
  16. package/dist/react/file-tree/index.js.map +1 -1
  17. package/dist/server/sync/alarm.d.ts +35 -0
  18. package/dist/server/sync/alarm.d.ts.map +1 -0
  19. package/dist/server/sync/alarm.js +92 -0
  20. package/dist/server/sync/alarm.js.map +1 -0
  21. package/dist/server/sync/commands.d.ts +88 -0
  22. package/dist/server/sync/commands.d.ts.map +1 -0
  23. package/dist/server/sync/commands.js +242 -0
  24. package/dist/server/sync/commands.js.map +1 -0
  25. package/dist/server/sync/engine.d.ts +63 -0
  26. package/dist/server/sync/engine.d.ts.map +1 -0
  27. package/dist/server/sync/engine.js +185 -0
  28. package/dist/server/sync/engine.js.map +1 -0
  29. package/dist/server/sync/http.d.ts +107 -0
  30. package/dist/server/sync/http.d.ts.map +1 -0
  31. package/dist/server/sync/http.js +244 -0
  32. package/dist/server/sync/http.js.map +1 -0
  33. package/dist/server/sync/index.d.ts +42 -0
  34. package/dist/server/sync/index.d.ts.map +1 -0
  35. package/dist/server/sync/index.js +42 -0
  36. package/dist/server/sync/index.js.map +1 -0
  37. package/dist/server/sync/opLog.d.ts +62 -0
  38. package/dist/server/sync/opLog.d.ts.map +1 -0
  39. package/dist/server/sync/opLog.js +97 -0
  40. package/dist/server/sync/opLog.js.map +1 -0
  41. package/dist/server/sync/planner.d.ts +32 -0
  42. package/dist/server/sync/planner.d.ts.map +1 -0
  43. package/dist/server/sync/planner.js +31 -0
  44. package/dist/server/sync/planner.js.map +1 -0
  45. package/dist/server/sync/status.d.ts +64 -0
  46. package/dist/server/sync/status.d.ts.map +1 -0
  47. package/dist/server/sync/status.js +50 -0
  48. package/dist/server/sync/status.js.map +1 -0
  49. package/dist/server/sync/timer.d.ts +53 -0
  50. package/dist/server/sync/timer.d.ts.map +1 -0
  51. package/dist/server/sync/timer.js +171 -0
  52. package/dist/server/sync/timer.js.map +1 -0
  53. package/dist/server/sync/tokens.d.ts +26 -0
  54. package/dist/server/sync/tokens.d.ts.map +1 -0
  55. package/dist/server/sync/tokens.js +52 -0
  56. package/dist/server/sync/tokens.js.map +1 -0
  57. package/dist/server/sync/types.d.ts +102 -0
  58. package/dist/server/sync/types.d.ts.map +1 -0
  59. package/dist/server/sync/types.js +30 -0
  60. package/dist/server/sync/types.js.map +1 -0
  61. package/package.json +25 -15
  62. package/scripts/verify.ts +292 -0
  63. package/src/core/file-tree/fileTreeModel.spec.ts +23 -0
  64. package/src/core/file-tree/fileTreeModel.ts +33 -0
  65. package/src/demoFixture.spec.ts +11 -1
  66. package/src/react/components/TagsInput.spec.tsx +34 -0
  67. package/src/react/components/TagsInput.tsx +23 -1
  68. package/src/react/file-tree/FileTree.tsx +19 -11
  69. package/src/react/file-tree/index.ts +2 -0
  70. package/src/server/sync/alarm.spec.ts +149 -0
  71. package/src/server/sync/alarm.ts +137 -0
  72. package/src/server/sync/commands.spec.ts +145 -0
  73. package/src/server/sync/commands.ts +361 -0
  74. package/src/server/sync/engine.spec.ts +496 -0
  75. package/src/server/sync/engine.ts +255 -0
  76. package/src/server/sync/http.ts +316 -0
  77. package/src/server/sync/httpRemote.spec.ts +158 -0
  78. package/src/server/sync/index.ts +90 -0
  79. package/src/server/sync/opLog.spec.ts +110 -0
  80. package/src/server/sync/opLog.ts +189 -0
  81. package/src/server/sync/planner.spec.ts +53 -0
  82. package/src/server/sync/planner.ts +62 -0
  83. package/src/server/sync/status.spec.ts +92 -0
  84. package/src/server/sync/status.ts +108 -0
  85. package/src/server/sync/timer.spec.ts +150 -0
  86. package/src/server/sync/timer.ts +207 -0
  87. package/src/server/sync/tokens.spec.ts +38 -0
  88. package/src/server/sync/tokens.ts +94 -0
  89. package/src/server/sync/types.ts +108 -0
  90. package/src/verifyGraph.spec.ts +181 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The HTTP transport: the receiver sub-app the internet-facing instance mounts, and
3
+ * the fetch-backed {@link RemoteApi} the dialer binds to.
4
+ *
5
+ * Mount the receiver OUTSIDE any browser-session gate (a machine peer has no session;
6
+ * notes' 401 trap: a token route registered after the session gate answers 401 with a
7
+ * valid token) but behind the token gate built here. An unconfigured receiver simply
8
+ * does not exist — the orch-companion idiom: no token verifier, no routes, nothing to
9
+ * probe.
10
+ */
11
+ import { Hono } from "hono";
12
+ import type { OpLog } from "./opLog";
13
+ import type { ApplyOne, ApplyReport, RemoteApi } from "./types";
14
+ /**
15
+ * Trim a page to the byte budget, keeping it a PREFIX so `cursor` stays the seq
16
+ * of the last op actually returned. Anything else silently drops ops: the caller
17
+ * advances past rows it never received and no later pull will offer them again.
18
+ */
19
+ export declare function capPageBytes<T extends {
20
+ seq: number;
21
+ }>(ops: T[], maxBytes: number): {
22
+ ops: T[];
23
+ truncated: boolean;
24
+ };
25
+ export interface ReceiverHooks {
26
+ /** Every authenticated hit — the only evidence a non-dialing half has that it is
27
+ * in step (vault's `lastPeerContactAt` lesson). */
28
+ onPeerContact?: (peerId: string) => void;
29
+ /** The "Sync now was pressed HERE" stamp the dialer polls. */
30
+ readRequest?: () => number | null;
31
+ }
32
+ export interface ReceiverOptions {
33
+ log: OpLog;
34
+ opKinds: readonly string[];
35
+ /** Verify a presented bearer token for the sync scope. Return false to refuse.
36
+ * Bind this to a TokenStore (`(t) => tokens.verify(t, "sync") !== null`) or a
37
+ * constant-time static compare — the kit does not care which. */
38
+ verifyToken: (presented: string) => boolean;
39
+ /** The app's merge logic, when it is stateless across a batch (vault, station).
40
+ * Give this OR {@link beginBatch}, never both. */
41
+ applyOne?: ApplyOne;
42
+ /**
43
+ * The app's merge logic when the batch needs state of its own — called once per
44
+ * `POST /push`, so what it closes over belongs to that batch alone.
45
+ *
46
+ * 🔴 A single shared `ApplyOne` closing over a mutable accumulator is the bug this
47
+ * exists to make unrepresentable: two pushes in flight would pour into one counter,
48
+ * and the second response would carry the first's leftovers. notes needs it — its
49
+ * applier collects the attachment blobs a purge orphaned and the items whose search
50
+ * index must be rebuilt, neither of which can happen inside the op's transaction.
51
+ */
52
+ beginBatch?: () => ApplyOne;
53
+ /**
54
+ * Post-commit work, awaited BEFORE the response goes out — an index rebuild, a blob
55
+ * delete that is a network call. Anything that must not run inside the applier's
56
+ * transaction but must be done by the time the caller sees a 200, or the caller will
57
+ * advance its cursor past work that never happened.
58
+ */
59
+ afterBatch?: (report: ApplyReport) => void | Promise<void>;
60
+ hooks?: ReceiverHooks;
61
+ }
62
+ /** Build the receiver: GET /info, GET /pull, POST /push, GET /requested. */
63
+ export declare function createSyncReceiver(options: ReceiverOptions): Hono;
64
+ /**
65
+ * The fetch-backed remote the dialer binds to. `basePath` is where the peer
66
+ * mounted its receiver (e.g. `https://station.cursedalchemy.com/api/sync`).
67
+ *
68
+ * 🔴 **Redirects are REFUSED, never followed** (2026-08-21). `fetch` follows by
69
+ * default, and that turned a moved hostname into eight days of silence: the
70
+ * console's subdomain cutover made `station.cursedalchemy.com` a **301** to
71
+ * `station.cursedalchemy.com`, the dialer followed it, Cloudflare Access answered
72
+ * the un-credentialed redirect with its **login page — HTTP 200, text/html**,
73
+ * and `r.json()` threw `Failed to parse JSON`. The dialer logs that as
74
+ * `[sync] failed (ignored)` and retries forever, so:
75
+ *
76
+ * · every mirrored report on prod froze at the moment of the cutover, while
77
+ * each one still rendered a confident `capturedAt` age;
78
+ * · nothing was red anywhere — not a gate, not a smoke, not `/healthz`;
79
+ * · the error named JSON, which is three layers away from the cause.
80
+ *
81
+ * A 3xx here can only ever mean the peer address is wrong, so it is reported as
82
+ * exactly that, with the destination in the message. Following a redirect
83
+ * silently is how a sync survives its own misconfiguration.
84
+ *
85
+ * The same reasoning covers a 2xx that is not JSON: an Access login page, an
86
+ * nginx error page or a captive portal all arrive as `200 text/html`, and
87
+ * "Failed to parse JSON" is the least useful sentence available about any of
88
+ * them. {@link readJson} names the status, the content type and a snippet.
89
+ */
90
+ export declare function createHttpRemote(options: {
91
+ basePath: string;
92
+ token: string;
93
+ selfId: string;
94
+ fetchImpl?: typeof fetch;
95
+ timeoutMs?: number;
96
+ /**
97
+ * Extra headers for THIS peer's host — the Cloudflare Access service token,
98
+ * where the receiver sits behind an Access application. Host-scoped BY THE
99
+ * CALLER — never a blanket wrapper: a credential sent where it is not needed is a
100
+ * credential leaked. The retired generation had a `satellite-kit/cloudflare-access`
101
+ * helper that did the host-scoping; no caller in this app passes this today
102
+ * (`syncClient.ts` handles Access by refusing to follow its 302 instead), so a
103
+ * future one owns the scoping itself.
104
+ */
105
+ extraHeaders?: Record<string, string>;
106
+ }): RemoteApi;
107
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/server/sync/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAgB,QAAQ,EAAE,WAAW,EAAY,SAAS,EAAU,MAAM,SAAS,CAAC;AA2BhG;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACrD,GAAG,EAAE,CAAC,EAAE,EACR,QAAQ,EAAE,MAAM,GACd;IAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAOlC;AASD,MAAM,WAAW,aAAa;IAC7B;wDACoD;IACpD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC/B,GAAG,EAAE,KAAK,CAAC;IACX,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;sEAEkE;IAClE,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C;uDACmD;IACnD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,QAAQ,CAAC;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAmFjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC,GAAG,SAAS,CAqFZ"}
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The HTTP transport: the receiver sub-app the internet-facing instance mounts, and
3
+ * the fetch-backed {@link RemoteApi} the dialer binds to.
4
+ *
5
+ * Mount the receiver OUTSIDE any browser-session gate (a machine peer has no session;
6
+ * notes' 401 trap: a token route registered after the session gate answers 401 with a
7
+ * valid token) but behind the token gate built here. An unconfigured receiver simply
8
+ * does not exist — the orch-companion idiom: no token verifier, no routes, nothing to
9
+ * probe.
10
+ */
11
+ import { Hono } from "hono";
12
+ import { createSyncApplier } from "./engine";
13
+ const noStore = { "cache-control": "no-store" };
14
+ const SYNC_PAGE = 500;
15
+ /**
16
+ * Byte budget for ONE pull page — the mirror of `maxPushBytes` on the push side,
17
+ * and it was missing.
18
+ *
19
+ * 🔴 Measured against live prod on 2026-08-01: `GET /pull?after=0` against
20
+ * station's 709-op log served **37 MB at 309 ops** and then OOM-killed the unit
21
+ * (`memoryMax=300M` on a t3.micro). `SYNC_PAGE = 500` is a COUNT cap, and these
22
+ * ops are not small — a 130 KB overview snapshot, a 68 KB fleet CSV — so 500 of
23
+ * them is tens of megabytes serialized into one response buffer.
24
+ *
25
+ * What makes it worse than a slow request is that it does not converge: the unit
26
+ * dies, systemd restarts it, the dialer retries the identical oversized pull, and
27
+ * the pair never advances. Push already learned this exact lesson (a 413 wedges
28
+ * because the batch never shrinks); the receiving half kept only the count cap
29
+ * because a fully-caught-up peer never asks for enough rows to notice.
30
+ *
31
+ * A single op larger than the budget is still served alone — refusing it would
32
+ * wedge the log permanently on that op, which is worse than one big response.
33
+ */
34
+ const SYNC_PAGE_BYTES = 4_000_000;
35
+ /**
36
+ * Trim a page to the byte budget, keeping it a PREFIX so `cursor` stays the seq
37
+ * of the last op actually returned. Anything else silently drops ops: the caller
38
+ * advances past rows it never received and no later pull will offer them again.
39
+ */
40
+ export function capPageBytes(ops, maxBytes) {
41
+ let weight = 0;
42
+ for (let i = 0; i < ops.length; i++) {
43
+ weight += JSON.stringify(ops[i]).length + 1;
44
+ if (weight > maxBytes && i > 0)
45
+ return { ops: ops.slice(0, i), truncated: true };
46
+ }
47
+ return { ops, truncated: false };
48
+ }
49
+ /** Coerce and clamp the caller's peer id — a cursor key and a label, never trusted
50
+ * further than that. */
51
+ function peerIdOf(header, query) {
52
+ const raw = query ?? header ?? "";
53
+ return /^[0-9A-Za-z_-]{1,64}$/.test(raw) ? raw : "peer";
54
+ }
55
+ /** Build the receiver: GET /info, GET /pull, POST /push, GET /requested. */
56
+ export function createSyncReceiver(options) {
57
+ const { log, verifyToken } = options;
58
+ if ((options.applyOne === undefined) === (options.beginBatch === undefined)) {
59
+ // Loud at construction, because both mistakes are silent at runtime: neither
60
+ // given means every op vanishes as "ignored"; both given means one of the two
61
+ // is quietly never called.
62
+ throw new Error("[vault sync] receiver: give exactly one of `applyOne` or `beginBatch`");
63
+ }
64
+ const beginBatch = options.beginBatch ?? (() => options.applyOne);
65
+ const api = new Hono();
66
+ api.use("*", async (c, next) => {
67
+ const header = c.req.header("authorization") ?? "";
68
+ const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
69
+ if (presented.length === 0 || !verifyToken(presented)) {
70
+ return c.json({ error: "unauthorized" }, 401, noStore);
71
+ }
72
+ await next();
73
+ });
74
+ // AFTER the gate — an unauthenticated caller must never look like peer contact.
75
+ api.use("*", async (c, next) => {
76
+ options.hooks?.onPeerContact?.(peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer")));
77
+ await next();
78
+ });
79
+ api.get("/info", (c) => {
80
+ const body = {
81
+ instanceId: log.instanceId(),
82
+ head: log.head(),
83
+ opKinds: [...options.opKinds],
84
+ };
85
+ return c.json(body, 200, noStore);
86
+ });
87
+ api.get("/requested", (c) => c.json({ requestedAt: options.hooks?.readRequest?.() ?? null }, 200, noStore));
88
+ api.get("/pull", (c) => {
89
+ const afterRaw = c.req.query("after");
90
+ const after = afterRaw !== undefined && /^\d+$/.test(afterRaw) ? Number(afterRaw) : 0;
91
+ const excludeRaw = c.req.query("exclude") ?? "";
92
+ const exclude = /^[0-9A-Za-z_-]{1,64}$/.test(excludeRaw) ? excludeRaw : "";
93
+ const page = log.listAfter(after, exclude, SYNC_PAGE);
94
+ // Count cap first (cheap, bounds the serialize below), then the byte cap.
95
+ const capped = capPageBytes(page.ops, SYNC_PAGE_BYTES);
96
+ // The cursor must name the last op ACTUALLY served, or the peer skips the
97
+ // remainder forever. `head` is untouched — it is how the caller knows to
98
+ // come back for another page.
99
+ const cursor = capped.truncated
100
+ ? (capped.ops.at(-1)?.seq ?? after)
101
+ : page.cursor;
102
+ log.setCursor(peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer")), "served", cursor);
103
+ return c.json({ ops: capped.ops, cursor, head: page.head }, 200, noStore);
104
+ });
105
+ api.post("/push", async (c) => {
106
+ let body;
107
+ try {
108
+ body = await c.req.json();
109
+ }
110
+ catch {
111
+ return c.json({ error: "bad_request" }, 400, noStore);
112
+ }
113
+ if (body === null || typeof body !== "object")
114
+ return c.json({ error: "bad_request" }, 400, noStore);
115
+ const { ops, peerHas } = body;
116
+ if (!Array.isArray(ops))
117
+ return c.json({ error: "bad_request" }, 400, noStore);
118
+ const peerId = peerIdOf(c.req.header("x-sync-peer"), c.req.query("peer"));
119
+ // The client's DURABLE pull cursor for us is the crash-safe concurrency bound;
120
+ // fall back to our served cursor if an older caller omits it (vault's rule).
121
+ const bound = typeof peerHas === "number" && Number.isInteger(peerHas) && peerHas >= 0
122
+ ? peerHas
123
+ : log.cursor(peerId, "served");
124
+ const ctx = { peerId, peerReceivedUpTo: bound };
125
+ const report = createSyncApplier(log, beginBatch())(ops, ctx);
126
+ // Awaited, not fire-and-forget: the caller advances its push cursor on this
127
+ // response, so work still outstanding when it lands is work nobody will redo.
128
+ await options.afterBatch?.(report);
129
+ return c.json(report, report.error === null ? 200 : 409, noStore);
130
+ });
131
+ return api;
132
+ }
133
+ /**
134
+ * The fetch-backed remote the dialer binds to. `basePath` is where the peer
135
+ * mounted its receiver (e.g. `https://station.cursedalchemy.com/api/sync`).
136
+ *
137
+ * 🔴 **Redirects are REFUSED, never followed** (2026-08-21). `fetch` follows by
138
+ * default, and that turned a moved hostname into eight days of silence: the
139
+ * console's subdomain cutover made `station.cursedalchemy.com` a **301** to
140
+ * `station.cursedalchemy.com`, the dialer followed it, Cloudflare Access answered
141
+ * the un-credentialed redirect with its **login page — HTTP 200, text/html**,
142
+ * and `r.json()` threw `Failed to parse JSON`. The dialer logs that as
143
+ * `[sync] failed (ignored)` and retries forever, so:
144
+ *
145
+ * · every mirrored report on prod froze at the moment of the cutover, while
146
+ * each one still rendered a confident `capturedAt` age;
147
+ * · nothing was red anywhere — not a gate, not a smoke, not `/healthz`;
148
+ * · the error named JSON, which is three layers away from the cause.
149
+ *
150
+ * A 3xx here can only ever mean the peer address is wrong, so it is reported as
151
+ * exactly that, with the destination in the message. Following a redirect
152
+ * silently is how a sync survives its own misconfiguration.
153
+ *
154
+ * The same reasoning covers a 2xx that is not JSON: an Access login page, an
155
+ * nginx error page or a captive portal all arrive as `200 text/html`, and
156
+ * "Failed to parse JSON" is the least useful sentence available about any of
157
+ * them. {@link readJson} names the status, the content type and a snippet.
158
+ */
159
+ export function createHttpRemote(options) {
160
+ const base = options.basePath.replace(/\/$/, "");
161
+ const doFetch = options.fetchImpl ?? fetch;
162
+ const timeout = options.timeoutMs ?? 60_000;
163
+ const peer = encodeURIComponent(options.selfId);
164
+ const request = async (path, init) => {
165
+ const response = await doFetch(`${base}${path}`, {
166
+ ...init,
167
+ // 🔴 See the header note: a followed redirect is how this sync spent
168
+ // eight days reporting a JSON parse error about an Access login page.
169
+ redirect: "manual",
170
+ headers: {
171
+ authorization: `Bearer ${options.token}`,
172
+ "x-sync-peer": options.selfId,
173
+ ...(options.extraHeaders ?? {}),
174
+ ...(init?.headers ?? {}),
175
+ },
176
+ signal: AbortSignal.timeout(timeout),
177
+ });
178
+ if (response.status >= 300 && response.status < 400) {
179
+ throw new Error(`${path}: ${response.status} — the peer address redirects to ` +
180
+ `${response.headers.get("location") ?? "an unnamed destination"}. ` +
181
+ `The sync URL points at a host that has moved; point it at the real one.`);
182
+ }
183
+ if (response.status === 401) {
184
+ throw new Error(`${path}: 401 — the sync token was refused.`);
185
+ }
186
+ return response;
187
+ };
188
+ /**
189
+ * Read a JSON body, or say what actually arrived.
190
+ *
191
+ * A 200 carrying `text/html` is a login page or an error page, and it is the
192
+ * shape every "the address is wrong" failure takes once a redirect has been
193
+ * followed. Naming the content type and the first line turns an unactionable
194
+ * `Failed to parse JSON` into the cause.
195
+ */
196
+ const readJson = async (r, path) => {
197
+ const text = await r.text();
198
+ try {
199
+ return JSON.parse(text);
200
+ }
201
+ catch {
202
+ const type = r.headers.get("content-type") ?? "no content-type";
203
+ throw new Error(`${path}: ${r.status} answered ${type}, not JSON — ` +
204
+ `${text.trim().slice(0, 120).replace(/\s+/g, " ") || "(empty body)"}`);
205
+ }
206
+ };
207
+ return {
208
+ async info() {
209
+ const r = await request("/info");
210
+ if (!r.ok)
211
+ throw new Error(`sync/info: ${r.status}`);
212
+ return await readJson(r, "sync/info");
213
+ },
214
+ async pull(after, excludeOrigin) {
215
+ const r = await request(`/pull?after=${after}&exclude=${encodeURIComponent(excludeOrigin)}&peer=${peer}`);
216
+ if (!r.ok)
217
+ throw new Error(`sync/pull: ${r.status}`);
218
+ return await readJson(r, "sync/pull");
219
+ },
220
+ async push(payload) {
221
+ const r = await request(`/push?peer=${peer}`, {
222
+ method: "POST",
223
+ headers: { "content-type": "application/json" },
224
+ body: JSON.stringify(payload),
225
+ });
226
+ if (!r.ok && r.status !== 409)
227
+ throw new Error(`sync/push: ${r.status}`);
228
+ return await readJson(r, "sync/push");
229
+ },
230
+ async requestedAt() {
231
+ try {
232
+ const r = await request("/requested");
233
+ if (!r.ok)
234
+ return null;
235
+ const body = await readJson(r, "sync/requested");
236
+ return typeof body.requestedAt === "number" ? body.requestedAt : null;
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ },
242
+ };
243
+ }
244
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/server/sync/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `cursedbelt/sync` — the op-log replication engine, shared rather than forked.
3
+ *
4
+ * ## Why it is here and not in the app
5
+ *
6
+ * The retired tree had FOUR consumers of this engine: `apps/vault` (8 files),
7
+ * `apps/station` (6), `apps/notes` (4) and `packages/feedback-kit` (5). `vault`
8
+ * graduated first and carried all 3,245 lines in whole, as
9
+ * `apps/vault/src/kit/sync/`, because a Phase A that also publishes a library is a
10
+ * Phase A that does not land. This is the other half of that trade, and it had a
11
+ * DEADLINE rather than a backlog slot: the moment `apps/station` graduates with the
12
+ * copy still in `vault`, the generation owns two divergent forks of a replication
13
+ * engine — the exact shape the owner ruled against on 2026-09-13, *"if we don't
14
+ * adapt to this at the right time then it will be hard to remember to keep updating
15
+ * multiple copies as the apps move and grow."*
16
+ *
17
+ * ## 🔴 A LEAF export, and it must stay one
18
+ *
19
+ * `cursedbelt/sync` resolves straight to this directory. It is deliberately NOT
20
+ * reachable through the `cursedbelt/server` barrel, which drags `kysely` and
21
+ * `otplib` behind it — an app that wants an op-log should not install two
22
+ * databases to get one. The only runtime dependency beneath this subpath is
23
+ * `hono` (for the receiver routes in `http.ts` and `commands.ts`); everything else
24
+ * is `bun:sqlite`, `node:crypto` and `node:fs`.
25
+ *
26
+ * It lives under `src/server/` because that is where this package's two typecheck
27
+ * projects put Bun's ambient types: the root `tsconfig.json` runs `types: []` to
28
+ * keep the browser/React surface pristine and EXCLUDES `src/server`, so
29
+ * `bun:sqlite` and `node:crypto` only resolve on this side of the split. The
30
+ * directory decides the types; the `exports` key decides the spelling.
31
+ */
32
+ export { StopBatch, type ApplyContext, type ApplyOne, type ApplyReport, type PeerInfo, type RemoteApi, type SyncOp, type SyncSummary, } from "./types";
33
+ export { createOpLog, type OpLog } from "./opLog";
34
+ export { createTokenStore, type DeviceToken, type TokenStore } from "./tokens";
35
+ export { createSyncApplier, pushToRemote, pullFromRemote, runSync, type EngineOptions, type ExportPolicy, type PeerRefusal, } from "./engine";
36
+ export { capPageBytes, createHttpRemote, createSyncReceiver, type ReceiverHooks, type ReceiverOptions, } from "./http";
37
+ export { DEFAULT_LOOP, isUnreachable, planNextSync, type SyncLoopConfig, type SyncLoopState, } from "./planner";
38
+ export { OFFLINE_STATUS, PEER_CONTACT_WRITE_MS, createSyncStatusReporter, type SyncStatus, type SyncStatusReporter, type SyncStatusStore, } from "./status";
39
+ export { REQUEST_POLL_MS, startSyncTimer, type SyncTimerDeps, type SyncTimerHandle } from "./timer";
40
+ export { createSyncAlarm, type AlarmOutcome, type SyncAlarm, type SyncAlarmOptions } from "./alarm";
41
+ export { DEFAULT_HOLD_MS, createCommandQueue, createCommandReceiver, createCommandRemote, startCommandWorker, type CommandQueue, type CommandRemote, type CommandRoutesOptions, type CommandStatus, type CommandWorkerDeps, type CommandWorkerHandle, type SyncCommand, } from "./commands";
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EACN,SAAS,EACT,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,WAAW,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC/E,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,OAAO,EACP,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,WAAW,GAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,eAAe,GACpB,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,aAAa,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EACN,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GAChB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `cursedbelt/sync` — the op-log replication engine, shared rather than forked.
3
+ *
4
+ * ## Why it is here and not in the app
5
+ *
6
+ * The retired tree had FOUR consumers of this engine: `apps/vault` (8 files),
7
+ * `apps/station` (6), `apps/notes` (4) and `packages/feedback-kit` (5). `vault`
8
+ * graduated first and carried all 3,245 lines in whole, as
9
+ * `apps/vault/src/kit/sync/`, because a Phase A that also publishes a library is a
10
+ * Phase A that does not land. This is the other half of that trade, and it had a
11
+ * DEADLINE rather than a backlog slot: the moment `apps/station` graduates with the
12
+ * copy still in `vault`, the generation owns two divergent forks of a replication
13
+ * engine — the exact shape the owner ruled against on 2026-09-13, *"if we don't
14
+ * adapt to this at the right time then it will be hard to remember to keep updating
15
+ * multiple copies as the apps move and grow."*
16
+ *
17
+ * ## 🔴 A LEAF export, and it must stay one
18
+ *
19
+ * `cursedbelt/sync` resolves straight to this directory. It is deliberately NOT
20
+ * reachable through the `cursedbelt/server` barrel, which drags `kysely` and
21
+ * `otplib` behind it — an app that wants an op-log should not install two
22
+ * databases to get one. The only runtime dependency beneath this subpath is
23
+ * `hono` (for the receiver routes in `http.ts` and `commands.ts`); everything else
24
+ * is `bun:sqlite`, `node:crypto` and `node:fs`.
25
+ *
26
+ * It lives under `src/server/` because that is where this package's two typecheck
27
+ * projects put Bun's ambient types: the root `tsconfig.json` runs `types: []` to
28
+ * keep the browser/React surface pristine and EXCLUDES `src/server`, so
29
+ * `bun:sqlite` and `node:crypto` only resolve on this side of the split. The
30
+ * directory decides the types; the `exports` key decides the spelling.
31
+ */
32
+ export { StopBatch, } from "./types";
33
+ export { createOpLog } from "./opLog";
34
+ export { createTokenStore } from "./tokens";
35
+ export { createSyncApplier, pushToRemote, pullFromRemote, runSync, } from "./engine";
36
+ export { capPageBytes, createHttpRemote, createSyncReceiver, } from "./http";
37
+ export { DEFAULT_LOOP, isUnreachable, planNextSync, } from "./planner";
38
+ export { OFFLINE_STATUS, PEER_CONTACT_WRITE_MS, createSyncStatusReporter, } from "./status";
39
+ export { REQUEST_POLL_MS, startSyncTimer } from "./timer";
40
+ export { createSyncAlarm } from "./alarm";
41
+ export { DEFAULT_HOLD_MS, createCommandQueue, createCommandReceiver, createCommandRemote, startCommandWorker, } from "./commands";
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EACN,SAAS,GAQT,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAqC,MAAM,UAAU,CAAC;AAC/E,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,OAAO,GAIP,MAAM,UAAU,CAAC;AAClB,OAAO,EACN,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,GAGlB,MAAM,QAAQ,CAAC;AAChB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,YAAY,GAGZ,MAAM,WAAW,CAAC;AACnB,OAAO,EACN,cAAc,EACd,qBAAqB,EACrB,wBAAwB,GAIxB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,cAAc,EAA4C,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,eAAe,EAA4D,MAAM,SAAS,CAAC;AACpG,OAAO,EACN,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,GAQlB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The durable op log + cursor bookkeeping, over the app's own `bun:sqlite` handle.
3
+ * Fixed table names (`sync_ops`, `sync_cursors`, `sync_meta`) — a consumer of this
4
+ * engine is expected to give sync its own tables rather than remap; the notes/vault
5
+ * retrofit maps their legacy tables behind this interface, not the other way round.
6
+ *
7
+ * Identity: `(origin, origin_seq)` UNIQUE — exactly-once application with no peer
8
+ * coordination (the notes ledger shape). `seq` is this instance's own monotonic
9
+ * stream every peer pages over.
10
+ */
11
+ import type { Database } from "bun:sqlite";
12
+ import type { SyncOp } from "./types";
13
+ export interface OpLog {
14
+ /** This instance's stable id (minted once, persisted in sync_meta). */
15
+ instanceId(): string;
16
+ /** Record a locally-born op; returns it with seq/originSeq assigned. */
17
+ recordLocal(kind: string, payload: unknown, ts?: number): SyncOp;
18
+ /** Record a remote op into the ledger (dedupe identity kept). Caller applies state
19
+ * FIRST in the same transaction scope; see `createSyncApplier`. */
20
+ recordRemote(op: SyncOp): void;
21
+ /** Is this (origin, originSeq) already in the ledger? */
22
+ has(origin: string, originSeq: number): boolean;
23
+ /** Page of ops with seq > after, excluding one origin (the peer's own — it already
24
+ * has them), oldest first. `cursor` = last row's seq served (or `after`). */
25
+ listAfter(after: number, excludeOrigin: string, limit: number): {
26
+ ops: SyncOp[];
27
+ cursor: number;
28
+ head: number;
29
+ };
30
+ /** Highest local seq (0 when empty). */
31
+ head(): number;
32
+ /** Durable per-peer cursors. `direction`: what WE track about the peer. */
33
+ cursor(peerId: string, direction: "push" | "pull" | "served"): number;
34
+ setCursor(peerId: string, direction: "push" | "pull" | "served", seq: number): void;
35
+ /** Compaction for STATE-shaped kinds (a snapshot is not history): delete this
36
+ * origin's OWN earlier ops of `kind` matching `payloadWhere` below `keepSeq`.
37
+ * Safe under the exactly-once ledger — peers dedupe by (origin, originSeq),
38
+ * pull pages tolerate seq gaps, and a deleted op is simply never sent. */
39
+ compactOwn(kind: string, keepSeq: number, payloadLike: string): number;
40
+ /** The same compaction, for a state kind that arrived from a PEER.
41
+ *
42
+ * `compactOwn` is scoped to `origin = instanceId`, so a pure RECEIVER of a
43
+ * state kind matches zero rows and its log grows without bound — prod
44
+ * station reached 5,537 revisions of one `overview` snapshot (963 MB, ~160
45
+ * MB/day) because every one of them originated on the Mac. The safety
46
+ * argument on `compactOwn` never depended on who minted the op, only on the
47
+ * kind being STATE rather than history, so it carries over verbatim.
48
+ *
49
+ * Keyed on `origin_seq`, not local `seq`: a receiver compacts while
50
+ * applying, which happens BEFORE the incoming row is inserted, so there is
51
+ * no local seq yet to compare against. `(origin, origin_seq)` is the
52
+ * ledger's own identity and is monotonic per origin, so it orders the
53
+ * origin's stream exactly as well. */
54
+ compactOrigin(origin: string, kind: string, keepOriginSeq: number, payloadLike: string): number;
55
+ /** Arbitrary sync-scoped metadata (instance id lives here too). */
56
+ getMeta(key: string): string | null;
57
+ setMeta(key: string, value: string): void;
58
+ /** Run `fn` inside one sqlite transaction (apply + ledger-insert atomicity). */
59
+ transaction<T>(fn: () => T): T;
60
+ }
61
+ export declare function createOpLog(db: Database): OpLog;
62
+ //# sourceMappingURL=opLog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opLog.d.ts","sourceRoot":"","sources":["../../../src/server/sync/opLog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,MAAM,WAAW,KAAK;IACrB,uEAAuE;IACvE,UAAU,IAAI,MAAM,CAAC;IACrB,wEAAwE;IACxE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACjE;wEACoE;IACpE,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yDAAyD;IACzD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD;kFAC8E;IAC9E,SAAS,CACR,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,GACX;QAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,wCAAwC;IACxC,IAAI,IAAI,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACpF;;;+EAG2E;IAC3E,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE;;;;;;;;;;;;;2CAauC;IACvC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IAChG,mEAAmE;IACnE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,gFAAgF;IAChF,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CAC/B;AAID,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,KAAK,CA2H/C"}
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const INSTANCE_KEY = "sync.instance_id";
3
+ export function createOpLog(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS sync_ops (
6
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ origin TEXT NOT NULL,
8
+ origin_seq INTEGER NOT NULL,
9
+ ts INTEGER NOT NULL,
10
+ kind TEXT NOT NULL,
11
+ payload TEXT NOT NULL,
12
+ UNIQUE(origin, origin_seq)
13
+ );
14
+ CREATE INDEX IF NOT EXISTS idx_sync_ops_origin ON sync_ops(origin, seq);
15
+ CREATE TABLE IF NOT EXISTS sync_cursors (
16
+ peer_id TEXT NOT NULL,
17
+ direction TEXT NOT NULL,
18
+ seq INTEGER NOT NULL DEFAULT 0,
19
+ PRIMARY KEY(peer_id, direction)
20
+ );
21
+ CREATE TABLE IF NOT EXISTS sync_meta (
22
+ key TEXT PRIMARY KEY,
23
+ value TEXT NOT NULL
24
+ );
25
+ `);
26
+ const getMetaStmt = db.prepare("SELECT value FROM sync_meta WHERE key = ?");
27
+ const setMetaStmt = db.prepare("INSERT INTO sync_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
28
+ const getMeta = (key) => getMetaStmt.get(key)?.value ?? null;
29
+ const setMeta = (key, value) => {
30
+ setMetaStmt.run(key, value);
31
+ };
32
+ let cachedId = getMeta(INSTANCE_KEY);
33
+ if (cachedId === null) {
34
+ // Short + url/label safe; uniqueness across a two-or-three instance fleet is
35
+ // what matters, not global collision resistance.
36
+ cachedId = `i-${randomUUID().slice(0, 12)}`;
37
+ setMeta(INSTANCE_KEY, cachedId);
38
+ }
39
+ const instanceId = cachedId;
40
+ const insertOp = db.prepare("INSERT INTO sync_ops (origin, origin_seq, ts, kind, payload) VALUES (?, ?, ?, ?, ?)");
41
+ const hasStmt = db.prepare("SELECT 1 FROM sync_ops WHERE origin = ? AND origin_seq = ? LIMIT 1");
42
+ const headStmt = db.prepare("SELECT COALESCE(MAX(seq), 0) AS head FROM sync_ops");
43
+ const maxOriginSeqStmt = db.prepare("SELECT COALESCE(MAX(origin_seq), 0) AS m FROM sync_ops WHERE origin = ?");
44
+ const listStmt = db.prepare("SELECT seq, origin, origin_seq, ts, kind, payload FROM sync_ops WHERE seq > ? AND origin != ? ORDER BY seq ASC LIMIT ?");
45
+ const compactStmt = db.prepare("DELETE FROM sync_ops WHERE origin = ? AND kind = ? AND seq < ? AND payload LIKE ?");
46
+ const compactOriginStmt = db.prepare("DELETE FROM sync_ops WHERE origin = ? AND kind = ? AND origin_seq < ? AND payload LIKE ?");
47
+ const cursorStmt = db.prepare("SELECT seq FROM sync_cursors WHERE peer_id = ? AND direction = ?");
48
+ const setCursorStmt = db.prepare("INSERT INTO sync_cursors (peer_id, direction, seq) VALUES (?, ?, ?) ON CONFLICT(peer_id, direction) DO UPDATE SET seq = excluded.seq");
49
+ const rowToOp = (row) => ({
50
+ seq: row.seq,
51
+ origin: row.origin,
52
+ originSeq: row.origin_seq,
53
+ ts: row.ts,
54
+ kind: row.kind,
55
+ payload: JSON.parse(row.payload),
56
+ });
57
+ return {
58
+ instanceId: () => instanceId,
59
+ recordLocal(kind, payload, ts) {
60
+ const at = ts ?? Date.now();
61
+ const originSeq = maxOriginSeqStmt.get(instanceId).m + 1;
62
+ insertOp.run(instanceId, originSeq, at, kind, JSON.stringify(payload ?? null));
63
+ const seq = headStmt.get().head;
64
+ return { seq, origin: instanceId, originSeq, ts: at, kind, payload };
65
+ },
66
+ recordRemote(op) {
67
+ insertOp.run(op.origin, op.originSeq, op.ts, op.kind, JSON.stringify(op.payload ?? null));
68
+ },
69
+ has: (origin, originSeq) => hasStmt.get(origin, originSeq) !== null,
70
+ listAfter(after, excludeOrigin, limit) {
71
+ const rows = listStmt.all(after, excludeOrigin, limit);
72
+ const ops = rows.map(rowToOp);
73
+ const head = headStmt.get().head;
74
+ // An all-excluded page must still advance the peer past what it skipped, or a
75
+ // log dominated by the peer's own reflected ops would page forever. Serve the
76
+ // true high-water mark of the WINDOW scanned: when the page is short, we
77
+ // reached head; otherwise the last row's seq.
78
+ const cursor = rows.length === 0 ? Math.max(after, head) : rows[rows.length - 1].seq;
79
+ return { ops, cursor, head };
80
+ },
81
+ head: () => headStmt.get().head,
82
+ compactOwn(kind, keepSeq, payloadLike) {
83
+ return compactStmt.run(instanceId, kind, keepSeq, payloadLike).changes;
84
+ },
85
+ compactOrigin(origin, kind, keepOriginSeq, payloadLike) {
86
+ return compactOriginStmt.run(origin, kind, keepOriginSeq, payloadLike).changes;
87
+ },
88
+ cursor: (peerId, direction) => cursorStmt.get(peerId, direction)?.seq ?? 0,
89
+ setCursor(peerId, direction, seq) {
90
+ setCursorStmt.run(peerId, direction, seq);
91
+ },
92
+ getMeta,
93
+ setMeta,
94
+ transaction: (fn) => db.transaction(fn)(),
95
+ };
96
+ }
97
+ //# sourceMappingURL=opLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opLog.js","sourceRoot":"","sources":["../../../src/server/sync/opLog.ts"],"names":[],"mappings":"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"}