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.
- package/dist/core/file-tree/fileTreeModel.d.ts +18 -0
- package/dist/core/file-tree/fileTreeModel.d.ts.map +1 -1
- package/dist/core/file-tree/fileTreeModel.js +24 -0
- package/dist/core/file-tree/fileTreeModel.js.map +1 -1
- package/dist/react/components/TagsInput.d.ts +8 -1
- package/dist/react/components/TagsInput.d.ts.map +1 -1
- package/dist/react/components/TagsInput.js +10 -4
- package/dist/react/components/TagsInput.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts +14 -2
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +3 -9
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/index.d.ts +1 -1
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +1 -1
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/server/sync/alarm.d.ts +35 -0
- package/dist/server/sync/alarm.d.ts.map +1 -0
- package/dist/server/sync/alarm.js +92 -0
- package/dist/server/sync/alarm.js.map +1 -0
- package/dist/server/sync/commands.d.ts +88 -0
- package/dist/server/sync/commands.d.ts.map +1 -0
- package/dist/server/sync/commands.js +242 -0
- package/dist/server/sync/commands.js.map +1 -0
- package/dist/server/sync/engine.d.ts +63 -0
- package/dist/server/sync/engine.d.ts.map +1 -0
- package/dist/server/sync/engine.js +185 -0
- package/dist/server/sync/engine.js.map +1 -0
- package/dist/server/sync/http.d.ts +107 -0
- package/dist/server/sync/http.d.ts.map +1 -0
- package/dist/server/sync/http.js +244 -0
- package/dist/server/sync/http.js.map +1 -0
- package/dist/server/sync/index.d.ts +42 -0
- package/dist/server/sync/index.d.ts.map +1 -0
- package/dist/server/sync/index.js +42 -0
- package/dist/server/sync/index.js.map +1 -0
- package/dist/server/sync/opLog.d.ts +62 -0
- package/dist/server/sync/opLog.d.ts.map +1 -0
- package/dist/server/sync/opLog.js +97 -0
- package/dist/server/sync/opLog.js.map +1 -0
- package/dist/server/sync/planner.d.ts +32 -0
- package/dist/server/sync/planner.d.ts.map +1 -0
- package/dist/server/sync/planner.js +31 -0
- package/dist/server/sync/planner.js.map +1 -0
- package/dist/server/sync/status.d.ts +64 -0
- package/dist/server/sync/status.d.ts.map +1 -0
- package/dist/server/sync/status.js +50 -0
- package/dist/server/sync/status.js.map +1 -0
- package/dist/server/sync/timer.d.ts +53 -0
- package/dist/server/sync/timer.d.ts.map +1 -0
- package/dist/server/sync/timer.js +171 -0
- package/dist/server/sync/timer.js.map +1 -0
- package/dist/server/sync/tokens.d.ts +26 -0
- package/dist/server/sync/tokens.d.ts.map +1 -0
- package/dist/server/sync/tokens.js +52 -0
- package/dist/server/sync/tokens.js.map +1 -0
- package/dist/server/sync/types.d.ts +102 -0
- package/dist/server/sync/types.d.ts.map +1 -0
- package/dist/server/sync/types.js +30 -0
- package/dist/server/sync/types.js.map +1 -0
- package/package.json +25 -15
- package/scripts/verify.ts +292 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +23 -0
- package/src/core/file-tree/fileTreeModel.ts +33 -0
- package/src/demoFixture.spec.ts +11 -1
- package/src/react/components/TagsInput.spec.tsx +34 -0
- package/src/react/components/TagsInput.tsx +23 -1
- package/src/react/file-tree/FileTree.tsx +19 -11
- package/src/react/file-tree/index.ts +2 -0
- package/src/server/sync/alarm.spec.ts +149 -0
- package/src/server/sync/alarm.ts +137 -0
- package/src/server/sync/commands.spec.ts +145 -0
- package/src/server/sync/commands.ts +361 -0
- package/src/server/sync/engine.spec.ts +496 -0
- package/src/server/sync/engine.ts +255 -0
- package/src/server/sync/http.ts +316 -0
- package/src/server/sync/httpRemote.spec.ts +158 -0
- package/src/server/sync/index.ts +90 -0
- package/src/server/sync/opLog.spec.ts +110 -0
- package/src/server/sync/opLog.ts +189 -0
- package/src/server/sync/planner.spec.ts +53 -0
- package/src/server/sync/planner.ts +62 -0
- package/src/server/sync/status.spec.ts +92 -0
- package/src/server/sync/status.ts +108 -0
- package/src/server/sync/timer.spec.ts +150 -0
- package/src/server/sync/timer.ts +207 -0
- package/src/server/sync/tokens.spec.ts +38 -0
- package/src/server/sync/tokens.ts +94 -0
- package/src/server/sync/types.ts +108 -0
- package/src/verifyGraph.spec.ts +181 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alarm.js","sourceRoot":"","sources":["../../../src/server/sync/alarm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command relay — notes' ask-responder shape generalized: the internet-facing
|
|
3
|
+
* instance PARKS an action it cannot perform (it has no Mac powers), and the Mac's
|
|
4
|
+
* resident worker long-polls for it, executes locally, and posts the outcome back.
|
|
5
|
+
* The Mac dials OUT; the receiving instance never reaches into the house.
|
|
6
|
+
*
|
|
7
|
+
* Delivery is a held GET (default hold 25s) — remember the two fleet traps: the
|
|
8
|
+
* host server needs `idleTimeoutSeconds` ABOVE the hold (Bun's 10s default closed
|
|
9
|
+
* notes' 25s poll and nginx reported a bogus 502), and the routes must be mounted
|
|
10
|
+
* BEFORE any browser-session gate.
|
|
11
|
+
*
|
|
12
|
+
* A command left `claimed` past the stale window is reclaimable — a worker that
|
|
13
|
+
* died mid-execution must not strand the queue. Executors must therefore be
|
|
14
|
+
* idempotent or cheap to re-run.
|
|
15
|
+
*/
|
|
16
|
+
import type { Database } from "bun:sqlite";
|
|
17
|
+
import { Hono } from "hono";
|
|
18
|
+
export type CommandStatus = "queued" | "claimed" | "done" | "failed";
|
|
19
|
+
export interface SyncCommand {
|
|
20
|
+
uid: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
args: unknown;
|
|
23
|
+
status: CommandStatus;
|
|
24
|
+
requestedBy: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
claimedAt: number | null;
|
|
27
|
+
claimedBy: string | null;
|
|
28
|
+
finishedAt: number | null;
|
|
29
|
+
result: unknown;
|
|
30
|
+
error: string | null;
|
|
31
|
+
}
|
|
32
|
+
export interface CommandQueue {
|
|
33
|
+
enqueue(kind: string, args: unknown, requestedBy: string): SyncCommand;
|
|
34
|
+
/** Atomically claim the oldest queued (or stale-claimed) command. */
|
|
35
|
+
claimNext(claimedBy: string, staleMs?: number): SyncCommand | null;
|
|
36
|
+
complete(uid: string, result: unknown): boolean;
|
|
37
|
+
fail(uid: string, error: string): boolean;
|
|
38
|
+
get(uid: string): SyncCommand | null;
|
|
39
|
+
list(limit?: number): SyncCommand[];
|
|
40
|
+
pendingCount(): number;
|
|
41
|
+
}
|
|
42
|
+
export declare function createCommandQueue(db: Database): CommandQueue;
|
|
43
|
+
export declare const DEFAULT_HOLD_MS = 25000;
|
|
44
|
+
export interface CommandRoutesOptions {
|
|
45
|
+
queue: CommandQueue;
|
|
46
|
+
verifyToken: (presented: string) => boolean;
|
|
47
|
+
/** Cap on the held poll. The HOST SERVER's idleTimeout must exceed this. */
|
|
48
|
+
maxHoldMs?: number;
|
|
49
|
+
pollStepMs?: number;
|
|
50
|
+
}
|
|
51
|
+
/** GET /pending (held) + POST /pending/:uid — the worker's two verbs. */
|
|
52
|
+
export declare function createCommandReceiver(options: CommandRoutesOptions): Hono;
|
|
53
|
+
export interface CommandRemote {
|
|
54
|
+
poll(worker: string, waitMs: number): Promise<SyncCommand | null>;
|
|
55
|
+
report(uid: string, outcome: {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
result?: unknown;
|
|
58
|
+
error?: string;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare function createCommandRemote(options: {
|
|
62
|
+
basePath: string;
|
|
63
|
+
token: string;
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
/** Host-scoped extras for this peer — see `createHttpRemote`'s note. */
|
|
66
|
+
extraHeaders?: Record<string, string>;
|
|
67
|
+
}): CommandRemote;
|
|
68
|
+
export interface CommandWorkerDeps {
|
|
69
|
+
/** Re-read every cycle, like the sync timer's peer — null = not linked. */
|
|
70
|
+
resolveRemote: () => CommandRemote | null;
|
|
71
|
+
execute: (command: SyncCommand) => Promise<unknown>;
|
|
72
|
+
worker?: string;
|
|
73
|
+
holdMs?: number;
|
|
74
|
+
/** Idle gap between an empty poll and the next (on top of the hold). */
|
|
75
|
+
idleGapMs?: number;
|
|
76
|
+
backoffMaxMs?: number;
|
|
77
|
+
log?: (msg: string) => void;
|
|
78
|
+
warn?: (msg: string) => void;
|
|
79
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
80
|
+
clearTimer?: (handle: unknown) => void;
|
|
81
|
+
}
|
|
82
|
+
export interface CommandWorkerHandle {
|
|
83
|
+
stop(): void;
|
|
84
|
+
}
|
|
85
|
+
/** The resident long-poll → execute → report loop. Unreachable remote backs off to
|
|
86
|
+
* `backoffMaxMs` and recovers silently — the nightly window rule. */
|
|
87
|
+
export declare function startCommandWorker(deps: CommandWorkerDeps): CommandWorkerHandle;
|
|
88
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/server/sync/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
const STALE_CLAIM_MS = 5 * 60_000;
|
|
4
|
+
const toCommand = (r) => ({
|
|
5
|
+
uid: r.uid,
|
|
6
|
+
kind: r.kind,
|
|
7
|
+
args: JSON.parse(r.args),
|
|
8
|
+
status: r.status,
|
|
9
|
+
requestedBy: r.requested_by,
|
|
10
|
+
createdAt: r.created_at,
|
|
11
|
+
claimedAt: r.claimed_at,
|
|
12
|
+
claimedBy: r.claimed_by,
|
|
13
|
+
finishedAt: r.finished_at,
|
|
14
|
+
result: r.result === null ? null : JSON.parse(r.result),
|
|
15
|
+
error: r.error,
|
|
16
|
+
});
|
|
17
|
+
export function createCommandQueue(db) {
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS sync_commands (
|
|
20
|
+
uid TEXT PRIMARY KEY,
|
|
21
|
+
kind TEXT NOT NULL,
|
|
22
|
+
args TEXT NOT NULL,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
24
|
+
requested_by TEXT NOT NULL,
|
|
25
|
+
created_at INTEGER NOT NULL,
|
|
26
|
+
claimed_at INTEGER,
|
|
27
|
+
claimed_by TEXT,
|
|
28
|
+
finished_at INTEGER,
|
|
29
|
+
result TEXT,
|
|
30
|
+
error TEXT
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_sync_commands_status ON sync_commands(status, created_at);
|
|
33
|
+
`);
|
|
34
|
+
const insert = db.prepare("INSERT INTO sync_commands (uid, kind, args, status, requested_by, created_at) VALUES (?, ?, ?, 'queued', ?, ?)");
|
|
35
|
+
const getStmt = db.prepare("SELECT * FROM sync_commands WHERE uid = ?");
|
|
36
|
+
const listStmt = db.prepare("SELECT * FROM sync_commands ORDER BY created_at DESC LIMIT ?");
|
|
37
|
+
const pendingStmt = db.prepare("SELECT COUNT(*) AS n FROM sync_commands WHERE status IN ('queued','claimed')");
|
|
38
|
+
// The atomic claim: one UPDATE whose WHERE picks the oldest eligible row. sqlite
|
|
39
|
+
// serializes writers, so two workers can never claim the same row.
|
|
40
|
+
const claimStmt = db.prepare(`
|
|
41
|
+
UPDATE sync_commands SET status = 'claimed', claimed_at = ?, claimed_by = ?
|
|
42
|
+
WHERE uid = (
|
|
43
|
+
SELECT uid FROM sync_commands
|
|
44
|
+
WHERE status = 'queued' OR (status = 'claimed' AND claimed_at < ?)
|
|
45
|
+
ORDER BY created_at ASC LIMIT 1
|
|
46
|
+
)
|
|
47
|
+
RETURNING *
|
|
48
|
+
`);
|
|
49
|
+
const finishStmt = db.prepare("UPDATE sync_commands SET status = ?, finished_at = ?, result = ?, error = ? WHERE uid = ? AND status IN ('queued','claimed')");
|
|
50
|
+
return {
|
|
51
|
+
enqueue(kind, args, requestedBy) {
|
|
52
|
+
const uid = `cmd-${randomUUID().slice(0, 18)}`;
|
|
53
|
+
insert.run(uid, kind, JSON.stringify(args ?? null), requestedBy, Date.now());
|
|
54
|
+
return toCommand(getStmt.get(uid));
|
|
55
|
+
},
|
|
56
|
+
claimNext(claimedBy, staleMs = STALE_CLAIM_MS) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const row = claimStmt.get(now, claimedBy, now - staleMs);
|
|
59
|
+
return row ? toCommand(row) : null;
|
|
60
|
+
},
|
|
61
|
+
complete: (uid, result) => finishStmt.run("done", Date.now(), JSON.stringify(result ?? null), null, uid).changes > 0,
|
|
62
|
+
fail: (uid, error) => finishStmt.run("failed", Date.now(), null, error, uid).changes > 0,
|
|
63
|
+
get: (uid) => {
|
|
64
|
+
const row = getStmt.get(uid);
|
|
65
|
+
return row ? toCommand(row) : null;
|
|
66
|
+
},
|
|
67
|
+
list: (limit = 100) => listStmt.all(limit).map(toCommand),
|
|
68
|
+
pendingCount: () => pendingStmt.get().n,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ── the receiver routes (mount beside the sync receiver, same token gate) ────────
|
|
72
|
+
const noStore = { "cache-control": "no-store" };
|
|
73
|
+
export const DEFAULT_HOLD_MS = 25_000;
|
|
74
|
+
/** GET /pending (held) + POST /pending/:uid — the worker's two verbs. */
|
|
75
|
+
export function createCommandReceiver(options) {
|
|
76
|
+
const maxHold = options.maxHoldMs ?? DEFAULT_HOLD_MS;
|
|
77
|
+
const step = options.pollStepMs ?? 1_000;
|
|
78
|
+
const api = new Hono();
|
|
79
|
+
api.use("*", async (c, next) => {
|
|
80
|
+
const header = c.req.header("authorization") ?? "";
|
|
81
|
+
const presented = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
82
|
+
if (presented.length === 0 || !options.verifyToken(presented)) {
|
|
83
|
+
return c.json({ error: "unauthorized" }, 401, noStore);
|
|
84
|
+
}
|
|
85
|
+
await next();
|
|
86
|
+
});
|
|
87
|
+
api.get("/pending", async (c) => {
|
|
88
|
+
const worker = c.req.query("worker") ?? "worker";
|
|
89
|
+
const waitRaw = c.req.query("wait");
|
|
90
|
+
const wait = Math.min(maxHold, waitRaw !== undefined && /^\d+$/.test(waitRaw) ? Number(waitRaw) : maxHold);
|
|
91
|
+
const deadline = Date.now() + wait;
|
|
92
|
+
for (;;) {
|
|
93
|
+
const command = options.queue.claimNext(worker);
|
|
94
|
+
if (command)
|
|
95
|
+
return c.json({ command }, 200, noStore);
|
|
96
|
+
if (Date.now() >= deadline)
|
|
97
|
+
return c.json({ command: null }, 200, noStore);
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(step, Math.max(1, deadline - Date.now()))));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
api.post("/pending/:uid", async (c) => {
|
|
102
|
+
const uid = c.req.param("uid");
|
|
103
|
+
let body;
|
|
104
|
+
try {
|
|
105
|
+
body = await c.req.json();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return c.json({ error: "bad_request" }, 400, noStore);
|
|
109
|
+
}
|
|
110
|
+
const { ok, result, error } = (body ?? {});
|
|
111
|
+
if (typeof ok !== "boolean")
|
|
112
|
+
return c.json({ error: "bad_request" }, 400, noStore);
|
|
113
|
+
const changed = ok
|
|
114
|
+
? options.queue.complete(uid, result ?? null)
|
|
115
|
+
: options.queue.fail(uid, typeof error === "string" ? error : "failed");
|
|
116
|
+
if (!changed)
|
|
117
|
+
return c.json({ error: "not_found" }, 404, noStore);
|
|
118
|
+
return c.json({ ok: true }, 200, noStore);
|
|
119
|
+
});
|
|
120
|
+
return api;
|
|
121
|
+
}
|
|
122
|
+
export function createCommandRemote(options) {
|
|
123
|
+
const base = options.basePath.replace(/\/$/, "");
|
|
124
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
125
|
+
const headers = (extra) => ({
|
|
126
|
+
authorization: `Bearer ${options.token}`,
|
|
127
|
+
...(options.extraHeaders ?? {}),
|
|
128
|
+
...(extra ?? {}),
|
|
129
|
+
});
|
|
130
|
+
/** 🔴 `redirect: "manual"`, for the reason in `http.ts`'s header: the relay
|
|
131
|
+
* travels the same road as the sync, so a moved peer hostname must fail here
|
|
132
|
+
* with the destination in the message rather than silently landing on a
|
|
133
|
+
* Cloudflare Access login page and reporting a JSON parse error. */
|
|
134
|
+
const refuseRedirect = (r, path) => {
|
|
135
|
+
if (r.status >= 300 && r.status < 400)
|
|
136
|
+
throw new Error(`${path}: ${r.status} — the peer address redirects to ` +
|
|
137
|
+
`${r.headers.get("location") ?? "an unnamed destination"}; the relay URL points at a host that has moved.`);
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
async poll(worker, waitMs) {
|
|
141
|
+
const r = await doFetch(`${base}/pending?worker=${encodeURIComponent(worker)}&wait=${waitMs}`, {
|
|
142
|
+
headers: headers(),
|
|
143
|
+
redirect: "manual",
|
|
144
|
+
// The abort budget must clear the hold plus network slack.
|
|
145
|
+
signal: AbortSignal.timeout(waitMs + 15_000),
|
|
146
|
+
});
|
|
147
|
+
refuseRedirect(r, "commands/pending");
|
|
148
|
+
if (!r.ok)
|
|
149
|
+
throw new Error(`commands/pending: ${r.status}`);
|
|
150
|
+
const body = (await r.json());
|
|
151
|
+
return body.command;
|
|
152
|
+
},
|
|
153
|
+
async report(uid, outcome) {
|
|
154
|
+
const r = await doFetch(`${base}/pending/${encodeURIComponent(uid)}`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: headers({ "content-type": "application/json" }),
|
|
157
|
+
redirect: "manual",
|
|
158
|
+
body: JSON.stringify(outcome),
|
|
159
|
+
signal: AbortSignal.timeout(30_000),
|
|
160
|
+
});
|
|
161
|
+
refuseRedirect(r, "commands/report");
|
|
162
|
+
if (!r.ok)
|
|
163
|
+
throw new Error(`commands/report: ${r.status}`);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** The resident long-poll → execute → report loop. Unreachable remote backs off to
|
|
168
|
+
* `backoffMaxMs` and recovers silently — the nightly window rule. */
|
|
169
|
+
export function startCommandWorker(deps) {
|
|
170
|
+
const worker = deps.worker ?? "mac";
|
|
171
|
+
const holdMs = deps.holdMs ?? DEFAULT_HOLD_MS;
|
|
172
|
+
const idleGapMs = deps.idleGapMs ?? 1_000;
|
|
173
|
+
const backoffMax = deps.backoffMaxMs ?? 10 * 60_000;
|
|
174
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
175
|
+
const warn = deps.warn ?? ((m) => console.error(m));
|
|
176
|
+
const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
177
|
+
const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
|
|
178
|
+
let stopped = false;
|
|
179
|
+
let handle = null;
|
|
180
|
+
let failures = 0;
|
|
181
|
+
let reportedUnreachable = false;
|
|
182
|
+
const schedule = (ms) => {
|
|
183
|
+
if (stopped)
|
|
184
|
+
return;
|
|
185
|
+
if (handle !== null)
|
|
186
|
+
clearTimer(handle);
|
|
187
|
+
handle = setTimer(cycle, ms);
|
|
188
|
+
handle?.unref?.();
|
|
189
|
+
};
|
|
190
|
+
const cycle = () => {
|
|
191
|
+
if (stopped)
|
|
192
|
+
return;
|
|
193
|
+
const remote = deps.resolveRemote();
|
|
194
|
+
if (remote === null) {
|
|
195
|
+
schedule(60_000);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
void remote
|
|
199
|
+
.poll(worker, holdMs)
|
|
200
|
+
.then(async (command) => {
|
|
201
|
+
failures = 0;
|
|
202
|
+
reportedUnreachable = false;
|
|
203
|
+
if (command === null)
|
|
204
|
+
return;
|
|
205
|
+
log(`[commands] executing ${command.kind} (${command.uid})`);
|
|
206
|
+
try {
|
|
207
|
+
const result = await deps.execute(command);
|
|
208
|
+
await remote.report(command.uid, { ok: true, result });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
212
|
+
warn(`[commands] ${command.kind} failed: ${message}`);
|
|
213
|
+
// Best-effort: a failed report leaves the row claimed; the stale-claim
|
|
214
|
+
// window re-serves it, which is why executors must tolerate a re-run.
|
|
215
|
+
await remote.report(command.uid, { ok: false, error: message }).catch(() => { });
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.then(() => {
|
|
219
|
+
if (!stopped)
|
|
220
|
+
schedule(idleGapMs);
|
|
221
|
+
})
|
|
222
|
+
.catch((err) => {
|
|
223
|
+
failures += 1;
|
|
224
|
+
const wait = Math.min(backoffMax, 5_000 * 2 ** Math.min(failures - 1, 7));
|
|
225
|
+
if (!reportedUnreachable) {
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
log(`[commands] remote unreachable (${message}) — backing off. This is normal offline.`);
|
|
228
|
+
reportedUnreachable = true;
|
|
229
|
+
}
|
|
230
|
+
schedule(wait);
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
schedule(1_000);
|
|
234
|
+
return {
|
|
235
|
+
stop: () => {
|
|
236
|
+
stopped = true;
|
|
237
|
+
if (handle !== null)
|
|
238
|
+
clearTimer(handle);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../../src/server/sync/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dialer half (push-then-pull) and the batch applier. Pure over {@link RemoteApi}
|
|
3
|
+
* and {@link OpLog} so tests drive two REAL stores through the real receiver — no
|
|
4
|
+
* HTTP mocks (the notes/vault discipline).
|
|
5
|
+
*
|
|
6
|
+
* Push-before-pull is load-bearing: the push cursor must be current before the pull
|
|
7
|
+
* computes concurrency, so exactly ONE side materializes any given conflict artifact
|
|
8
|
+
* (vault's rule). Cursors advance only past durably-processed ops; a stopped batch
|
|
9
|
+
* advances exactly to the failure and the next run resumes there.
|
|
10
|
+
*
|
|
11
|
+
* Version skew (notes' hardest-won lesson): the peer's `/info` advertises `opKinds`;
|
|
12
|
+
* the push stops BEFORE the first op the peer cannot take and reports `blockedBy`
|
|
13
|
+
* instead of wedging with an error that reads like corruption. Nothing is dropped —
|
|
14
|
+
* the moment the peer is upgraded, the next push resumes from exactly that op.
|
|
15
|
+
*/
|
|
16
|
+
import type { OpLog } from "./opLog";
|
|
17
|
+
import { type ApplyContext, type ApplyOne, type ApplyReport, type PeerInfo, type RemoteApi, type SyncOp, type SyncSummary } from "./types";
|
|
18
|
+
/** Kit-owned dedupe + ledger insert around the app's per-op merge logic. Everything
|
|
19
|
+
* for one op happens in one transaction: state change AND ledger insert, so a crash
|
|
20
|
+
* can never apply without recording (double-apply) or record without applying
|
|
21
|
+
* (silent drop). */
|
|
22
|
+
export declare function createSyncApplier(log: OpLog, applyOne: ApplyOne): (ops: SyncOp[], ctx: ApplyContext) => ApplyReport;
|
|
23
|
+
/** Hold back ops the far side must never see (vault values, local-only kinds).
|
|
24
|
+
* Return null to hold; return a (possibly redacted) op to send. */
|
|
25
|
+
export type ExportPolicy = (op: SyncOp) => SyncOp | null;
|
|
26
|
+
/**
|
|
27
|
+
* A refusal the peer's `opKinds` cannot express — the SAME "stop before it, never drop
|
|
28
|
+
* it" contract, one layer deeper. Return a reason to block, or null to send.
|
|
29
|
+
*
|
|
30
|
+
* The kind check answers "can the peer take this SHAPE". It is not always the whole
|
|
31
|
+
* compatibility surface: notes negotiates `meta.set` a second time on the KEY, because
|
|
32
|
+
* `meta.set` has been a known kind since the log existed, so a NEW key rides a kind the
|
|
33
|
+
* peer recognizes and fails one layer deeper — as a skip on the receiver that looks like
|
|
34
|
+
* a delivered op, or, against a build predating that skip, as a wedged push.
|
|
35
|
+
*
|
|
36
|
+
* 🔴 This is deliberately NOT the {@link ExportPolicy}. A policy HOLDS an op back
|
|
37
|
+
* forever (the far side must never see it); a refusal PAUSES in front of it and keeps
|
|
38
|
+
* the cursor there, so the moment the peer is upgraded the next run resumes from exactly
|
|
39
|
+
* that op. Expressing "the peer is too old for this" as a hold would silently lose the
|
|
40
|
+
* mutation.
|
|
41
|
+
*/
|
|
42
|
+
export type PeerRefusal = (op: SyncOp, info: PeerInfo) => string | null;
|
|
43
|
+
export interface EngineOptions {
|
|
44
|
+
log: OpLog;
|
|
45
|
+
/** The op kinds THIS build understands — advertised via /info and used to stop a
|
|
46
|
+
* push into an older peer before the first op it cannot take. */
|
|
47
|
+
opKinds: readonly string[];
|
|
48
|
+
policy?: ExportPolicy;
|
|
49
|
+
/** Optional second compatibility check, finer than `opKinds`. See {@link PeerRefusal}. */
|
|
50
|
+
peerRefusal?: PeerRefusal;
|
|
51
|
+
/** Byte budget per push request (default ~1 MiB). A backlog of large ops is
|
|
52
|
+
* sent as several requests instead of one that trips the proxy's body cap —
|
|
53
|
+
* the 413 WEDGES otherwise: the batch never shrinks, the cursor never moves.
|
|
54
|
+
* A single op larger than the budget still goes alone. */
|
|
55
|
+
maxPushBytes?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Split ops into request-sized chunks by serialized payload weight. */
|
|
58
|
+
export declare function chunkByBytes(ops: SyncOp[], maxBytes: number): SyncOp[][];
|
|
59
|
+
export declare function pushToRemote(opts: EngineOptions, remote: RemoteApi): Promise<Pick<SyncSummary, "remoteId" | "pushed" | "held" | "conflicts" | "blockedBy">>;
|
|
60
|
+
export declare function pullFromRemote(opts: EngineOptions, remote: RemoteApi, applyBatch: (ops: SyncOp[], ctx: ApplyContext) => ApplyReport): Promise<Pick<SyncSummary, "remoteId" | "pulled" | "conflicts">>;
|
|
61
|
+
/** One full reconciliation: push, then pull. */
|
|
62
|
+
export declare function runSync(opts: EngineOptions, remote: RemoteApi, applyBatch: (ops: SyncOp[], ctx: ApplyContext) => ApplyReport): Promise<SyncSummary>;
|
|
63
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/server/sync/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EAEd,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,MAAM,SAAS,CAAC;AAIjB;;;qBAGqB;AACrB,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,IACvD,KAAK,MAAM,EAAE,EAAE,KAAK,YAAY,KAAG,WAAW,CAkCtD;AAED;oEACoE;AACpE,MAAM,MAAM,YAAY,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEzD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,KAAK,MAAM,GAAG,IAAI,CAAC;AAExE,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,KAAK,CAAC;IACX;sEACkE;IAClE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;+DAG2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAgBxE;AAED,wBAAsB,YAAY,CACjC,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC,CA+ExF;AAED,wBAAsB,cAAc,CACnC,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,KAAK,WAAW,GAC3D,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,GAAG,WAAW,CAAC,CAAC,CA2BjE;AAED,gDAAgD;AAChD,wBAAsB,OAAO,CAC5B,IAAI,EAAE,aAAa,EACnB,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,YAAY,KAAK,WAAW,GAC3D,OAAO,CAAC,WAAW,CAAC,CAItB"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { StopBatch, } from "./types";
|
|
2
|
+
const BATCH = 500;
|
|
3
|
+
/** Kit-owned dedupe + ledger insert around the app's per-op merge logic. Everything
|
|
4
|
+
* for one op happens in one transaction: state change AND ledger insert, so a crash
|
|
5
|
+
* can never apply without recording (double-apply) or record without applying
|
|
6
|
+
* (silent drop). */
|
|
7
|
+
export function createSyncApplier(log, applyOne) {
|
|
8
|
+
return (ops, ctx) => {
|
|
9
|
+
let applied = 0;
|
|
10
|
+
let seen = 0;
|
|
11
|
+
let conflicts = 0;
|
|
12
|
+
let cursor = ops.length > 0 ? ops[0].seq - 1 : 0;
|
|
13
|
+
let error = null;
|
|
14
|
+
for (const op of ops) {
|
|
15
|
+
if (log.has(op.origin, op.originSeq)) {
|
|
16
|
+
seen++;
|
|
17
|
+
cursor = op.seq;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const outcome = log.transaction(() => {
|
|
22
|
+
const result = applyOne(op, ctx);
|
|
23
|
+
log.recordRemote(op);
|
|
24
|
+
return result;
|
|
25
|
+
});
|
|
26
|
+
if (outcome === "applied")
|
|
27
|
+
applied++;
|
|
28
|
+
else if (outcome === "conflict") {
|
|
29
|
+
applied++;
|
|
30
|
+
conflicts++;
|
|
31
|
+
}
|
|
32
|
+
cursor = op.seq;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err instanceof StopBatch) {
|
|
36
|
+
error = err.message;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { applied, seen, conflicts, cursor, error };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const DEFAULT_MAX_PUSH_BYTES = 1_000_000;
|
|
46
|
+
/** Split ops into request-sized chunks by serialized payload weight. */
|
|
47
|
+
export function chunkByBytes(ops, maxBytes) {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
let current = [];
|
|
50
|
+
let weight = 0;
|
|
51
|
+
for (const op of ops) {
|
|
52
|
+
const size = JSON.stringify(op).length + 1;
|
|
53
|
+
if (current.length > 0 && weight + size > maxBytes) {
|
|
54
|
+
chunks.push(current);
|
|
55
|
+
current = [];
|
|
56
|
+
weight = 0;
|
|
57
|
+
}
|
|
58
|
+
current.push(op);
|
|
59
|
+
weight += size;
|
|
60
|
+
}
|
|
61
|
+
if (current.length > 0)
|
|
62
|
+
chunks.push(current);
|
|
63
|
+
return chunks;
|
|
64
|
+
}
|
|
65
|
+
export async function pushToRemote(opts, remote) {
|
|
66
|
+
const info = await remote.info();
|
|
67
|
+
const remoteId = info.instanceId;
|
|
68
|
+
const remoteKinds = new Set(info.opKinds);
|
|
69
|
+
// One predicate for both compatibility layers, so the "stop before it, leave the
|
|
70
|
+
// cursor there" path is shared and a caller cannot accidentally give the finer
|
|
71
|
+
// check the weaker (drop-it) semantics.
|
|
72
|
+
const refusedBecause = (op) => {
|
|
73
|
+
if (!remoteKinds.has(op.kind))
|
|
74
|
+
return op.kind;
|
|
75
|
+
return opts.peerRefusal?.(op, info) ?? null;
|
|
76
|
+
};
|
|
77
|
+
const { log } = opts;
|
|
78
|
+
const peerHas = log.cursor(remoteId, "pull");
|
|
79
|
+
let cursor = log.cursor(remoteId, "push");
|
|
80
|
+
let pushed = 0;
|
|
81
|
+
let held = 0;
|
|
82
|
+
// Conflicts the RECEIVER detected while applying our push — push-before-pull means
|
|
83
|
+
// exactly one side detects any given concurrency, and when that side is the
|
|
84
|
+
// receiver the count would otherwise vanish from the dialer's summary.
|
|
85
|
+
let conflicts = 0;
|
|
86
|
+
let blockedBy;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const page = log.listAfter(cursor, remoteId, BATCH);
|
|
89
|
+
if (page.ops.length === 0) {
|
|
90
|
+
// Nothing sendable in the window — advance past the peer's own reflected ops.
|
|
91
|
+
if (page.cursor > cursor) {
|
|
92
|
+
cursor = page.cursor;
|
|
93
|
+
log.setCursor(remoteId, "push", cursor);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const blockedAt = page.ops.findIndex((op) => refusedBecause(op) !== null);
|
|
98
|
+
const sendableRaw = blockedAt === -1 ? page.ops : page.ops.slice(0, blockedAt);
|
|
99
|
+
if (blockedAt !== -1) {
|
|
100
|
+
const op = page.ops[blockedAt];
|
|
101
|
+
blockedBy = refusedBecause(op) ?? op.kind;
|
|
102
|
+
}
|
|
103
|
+
const outbound = [];
|
|
104
|
+
for (const op of sendableRaw) {
|
|
105
|
+
const filtered = opts.policy ? opts.policy(op) : op;
|
|
106
|
+
if (filtered === null)
|
|
107
|
+
held++;
|
|
108
|
+
else
|
|
109
|
+
outbound.push(filtered);
|
|
110
|
+
}
|
|
111
|
+
if (outbound.length > 0) {
|
|
112
|
+
// Chunked by bytes so a large backlog cannot trip the proxy's body cap;
|
|
113
|
+
// the cursor advances per DELIVERED chunk, so an interrupted push resumes
|
|
114
|
+
// at the failure with nothing lost or re-sent.
|
|
115
|
+
for (const chunk of chunkByBytes(outbound, opts.maxPushBytes ?? DEFAULT_MAX_PUSH_BYTES)) {
|
|
116
|
+
const report = await remote.push({ ops: chunk, peerHas });
|
|
117
|
+
pushed += report.applied;
|
|
118
|
+
conflicts += report.conflicts;
|
|
119
|
+
if (report.error !== null) {
|
|
120
|
+
// The remote stopped inside the chunk (its own ordering guard). Advance
|
|
121
|
+
// to what it durably took and stop; the next run retries from there.
|
|
122
|
+
const processed = report.applied + report.seen;
|
|
123
|
+
if (processed > 0) {
|
|
124
|
+
cursor = chunk[processed - 1].seq;
|
|
125
|
+
log.setCursor(remoteId, "push", cursor);
|
|
126
|
+
}
|
|
127
|
+
return { remoteId, pushed, held, conflicts, blockedBy: report.error };
|
|
128
|
+
}
|
|
129
|
+
cursor = chunk[chunk.length - 1].seq;
|
|
130
|
+
log.setCursor(remoteId, "push", cursor);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (sendableRaw.length === 0)
|
|
134
|
+
break; // the very next op is blocked — nothing to advance past
|
|
135
|
+
cursor = sendableRaw[sendableRaw.length - 1].seq;
|
|
136
|
+
log.setCursor(remoteId, "push", cursor);
|
|
137
|
+
if (blockedBy)
|
|
138
|
+
break;
|
|
139
|
+
if (page.cursor >= page.head && blockedAt === -1) {
|
|
140
|
+
// Window served through head with nothing blocked — take the page cursor so
|
|
141
|
+
// trailing excluded ops don't re-page forever.
|
|
142
|
+
cursor = page.cursor;
|
|
143
|
+
log.setCursor(remoteId, "push", cursor);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { remoteId, pushed, held, conflicts, blockedBy };
|
|
148
|
+
}
|
|
149
|
+
export async function pullFromRemote(opts, remote, applyBatch) {
|
|
150
|
+
const info = await remote.info();
|
|
151
|
+
const remoteId = info.instanceId;
|
|
152
|
+
const { log } = opts;
|
|
153
|
+
const self = log.instanceId();
|
|
154
|
+
const peerReceivedUpTo = log.cursor(remoteId, "push");
|
|
155
|
+
let cursor = log.cursor(remoteId, "pull");
|
|
156
|
+
let pulled = 0;
|
|
157
|
+
let conflicts = 0;
|
|
158
|
+
for (;;) {
|
|
159
|
+
const page = await remote.pull(cursor, self);
|
|
160
|
+
if (page.ops.length === 0 && page.cursor <= cursor)
|
|
161
|
+
break;
|
|
162
|
+
const report = applyBatch(page.ops, { peerId: remoteId, peerReceivedUpTo });
|
|
163
|
+
pulled += report.applied;
|
|
164
|
+
conflicts += report.conflicts;
|
|
165
|
+
if (report.error !== null) {
|
|
166
|
+
if (report.cursor > cursor) {
|
|
167
|
+
cursor = report.cursor;
|
|
168
|
+
log.setCursor(remoteId, "pull", cursor);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
cursor = page.ops.length === 0 ? page.cursor : Math.max(report.cursor, page.cursor);
|
|
173
|
+
log.setCursor(remoteId, "pull", cursor);
|
|
174
|
+
if (page.cursor >= page.head)
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
return { remoteId, pulled, conflicts };
|
|
178
|
+
}
|
|
179
|
+
/** One full reconciliation: push, then pull. */
|
|
180
|
+
export async function runSync(opts, remote, applyBatch) {
|
|
181
|
+
const push = await pushToRemote(opts, remote);
|
|
182
|
+
const pull = await pullFromRemote(opts, remote, applyBatch);
|
|
183
|
+
return { ...push, ...pull, conflicts: push.conflicts + pull.conflicts, remoteId: push.remoteId };
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/server/sync/engine.ts"],"names":[],"mappings":"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"}
|