@githolon/testing 0.12.0 → 0.14.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/package.json +1 -1
- package/vendor/engine/engine.mjs +400 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "@githolon/testing — law TDD for Nomos domains: boot the REAL engine plane in-process (the exact machinery every cloud DO, heavy container and web client runs), dispatch directives, assert rows, assert TYPED REFUSALS, fork for what-ifs — fully offline inside vitest.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/vendor/engine/engine.mjs
CHANGED
|
@@ -39,7 +39,14 @@ function call(ex, mode, fields, STDERR) {
|
|
|
39
39
|
const reqPtr = ex.git_holon_alloc(req.length);
|
|
40
40
|
try {
|
|
41
41
|
new Uint8Array(ex.memory.buffer, reqPtr, req.length).set(req);
|
|
42
|
-
|
|
42
|
+
let packed;
|
|
43
|
+
try {
|
|
44
|
+
packed = ex.git_holon_call(reqPtr, req.length);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const tail = STDERR.length ? ` | stderr: ${STDERR.slice(-8).join(" / ")}` : "";
|
|
47
|
+
throw new Error(`${String((e && e.stack) || e)}${tail}`);
|
|
48
|
+
}
|
|
49
|
+
const { ptr, len } = unpack(packed);
|
|
43
50
|
try {
|
|
44
51
|
const e = JSON.parse(dec.decode(new Uint8Array(ex.memory.buffer, ptr, len).slice()));
|
|
45
52
|
if (!e.ok) throw new Error((e.error || "holon error") + (STDERR.length ? ` | ${STDERR.slice(-4).join(" / ")}` : ""));
|
|
@@ -1018,6 +1025,93 @@ export async function replicateIntents(eng, ws, ledger, intents, { push = true }
|
|
|
1018
1025
|
return { replicated, skipped, refused };
|
|
1019
1026
|
}
|
|
1020
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Open browser/session lane without a client-side Artifacts receive-pack: the
|
|
1030
|
+
* client offers the exact intent bytes it authored locally; the host applies the
|
|
1031
|
+
* same structural fences as session admission, folds accepted bytes, and seals
|
|
1032
|
+
* main once. This is still untrusted work: owner-only/control-plane/receipt
|
|
1033
|
+
* writes are refused or dead-lettered exactly like session refs.
|
|
1034
|
+
*/
|
|
1035
|
+
export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains = [], receiptFence = null } = {}) {
|
|
1036
|
+
const refuse = new Set(refuseDomains);
|
|
1037
|
+
const main = await mainIntentIds(eng, ws);
|
|
1038
|
+
const sessions = new Map();
|
|
1039
|
+
const deadLetters = [];
|
|
1040
|
+
let admittedTotal = 0;
|
|
1041
|
+
const timings = { spans: [], intents: 0, sessions: 0 };
|
|
1042
|
+
const t0 = Date.now(); let seq = 0;
|
|
1043
|
+
const span = (name, kind, depth = 1, extra = {}) => {
|
|
1044
|
+
const s0 = Date.now();
|
|
1045
|
+
return (end = {}) => timings.spans.push({ id: `intent-offer-${t0}-${seq++}`, track: "cloud", name, kind, depth, t0: s0, t1: Date.now(), ...extra, ...end });
|
|
1046
|
+
};
|
|
1047
|
+
for (const offer of Array.isArray(offers) ? offers : []) {
|
|
1048
|
+
const cid = typeof offer?.session === "string" && offer.session ? offer.session : "direct";
|
|
1049
|
+
const rec = sessions.get(cid) || { session: cid, admitted: [], rejected: [], skipped: [] };
|
|
1050
|
+
sessions.set(cid, rec);
|
|
1051
|
+
const done = span(`intent offer ${cid.slice(0, 14)}`, "cadmit", 1, { client: cid });
|
|
1052
|
+
let blob = offer?.blob instanceof Uint8Array ? offer.blob : null;
|
|
1053
|
+
try {
|
|
1054
|
+
if (!blob && typeof offer?.intentB64 === "string") blob = Uint8Array.from(atob(offer.intentB64), (c) => c.charCodeAt(0));
|
|
1055
|
+
if (!blob || blob.byteLength === 0 || blob.byteLength > 256 * 1024) {
|
|
1056
|
+
rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: "intent offer must carry intentB64 <= 256 KiB" });
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
let doc = null;
|
|
1060
|
+
try { doc = JSON.parse(dec.decode(blob)); } catch {}
|
|
1061
|
+
const intentId = typeof doc?.id === "string" ? doc.id : null;
|
|
1062
|
+
const dom = typeof doc?.payload?.domain === "string" ? doc.payload.domain : null;
|
|
1063
|
+
const dirId = typeof doc?.payload?.directiveId === "string" ? doc.payload.directiveId : null;
|
|
1064
|
+
const oid = String(offer?.oid || intentId || "").slice(0, 10);
|
|
1065
|
+
if (!dom || dom === "nomos" || dom === "bootstrap" || refuse.has(dom)) {
|
|
1066
|
+
rec.rejected.push({ oid, error: `session lane refuses ${dom ? `'${dom}'` : "non-authored"} intents — law deploys go through POST /v1/workspaces/${ws}/domains with the workspace secret` });
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (!knownDomains(eng).has(dom)) {
|
|
1070
|
+
const error = `domain '${dom}' is not deployed to this workspace — deploy its law (POST /v1/workspaces/${ws}/domains with the workspace secret), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1071
|
+
rec.rejected.push({ oid, error, deadLettered: true });
|
|
1072
|
+
deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
|
|
1076
|
+
const error = `'${dom}/${dirId}' is a capability RECEIPT — receipts are executor/owner writes, never open-session writes. Author it via POST /v1/workspaces/${ws}/author (owner Bearer), or — if this parked receipt is sanctioned — the owner unjams it: POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1077
|
+
rec.rejected.push({ oid, error, deadLettered: true });
|
|
1078
|
+
deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (intentId && main.ids.has(intentId)) {
|
|
1082
|
+
rec.skipped.push({ oid, intent: intentId.slice(0, 12), alreadyOnMain: true });
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const res = applyIntentBytes(eng, ws, blob);
|
|
1086
|
+
if (res.ok) {
|
|
1087
|
+
if (intentId) main.ids.add(intentId);
|
|
1088
|
+
main.oids.add(res.head); main.head = res.head;
|
|
1089
|
+
rec.admitted.push({ oid, head: res.head });
|
|
1090
|
+
admittedTotal += 1;
|
|
1091
|
+
} else {
|
|
1092
|
+
const error = `${String(res.error || "").slice(0, 300)} — dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1093
|
+
rec.rejected.push({ oid, error, deadLettered: true });
|
|
1094
|
+
deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
|
|
1095
|
+
}
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: String(e).slice(0, 200) });
|
|
1098
|
+
} finally {
|
|
1099
|
+
done();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
timings.sessions = sessions.size;
|
|
1103
|
+
timings.intents = admittedTotal;
|
|
1104
|
+
if (admittedTotal) {
|
|
1105
|
+
const sealDone = span("seal offered intents", "cseal", 1, { admitted: admittedTotal });
|
|
1106
|
+
await commitAndPush(eng, ws, ledger);
|
|
1107
|
+
sealDone();
|
|
1108
|
+
}
|
|
1109
|
+
const gitdir = gitdirOf(ws);
|
|
1110
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => main.head);
|
|
1111
|
+
log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0) });
|
|
1112
|
+
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...(s.skipped.length ? {} : { skipped: undefined }) })), main: head, deadLetters, timings };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1021
1115
|
/**
|
|
1022
1116
|
* §5.5 (slice 4) — the CONTENT HASH of the coordinator's committed subtotal state:
|
|
1023
1117
|
* sha256 over the canonical (row-id-sorted) projection of every
|
|
@@ -1118,31 +1212,93 @@ export async function deepVerifyShard(eng, shardWs, shardLedger, coordinatorRows
|
|
|
1118
1212
|
};
|
|
1119
1213
|
}
|
|
1120
1214
|
|
|
1121
|
-
|
|
1122
|
-
export async function mountWorkspace(eng, ws, ledger) {
|
|
1123
|
-
if (eng.mounted.has(ws)) return eng.mounted.get(ws);
|
|
1215
|
+
function stageWorkspaceDir(eng, ws) {
|
|
1124
1216
|
const wsContents = new Map();
|
|
1125
1217
|
seedManifests(wsContents, eng.manifestStrings);
|
|
1126
1218
|
eng.preopen.dir.contents.get("ws").contents.set(ws, new Directory(wsContents));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** Mount a workspace, restoring its repo from the Artifacts ledger ({remote, headers}). */
|
|
1222
|
+
export async function mountWorkspace(eng, ws, ledger) {
|
|
1223
|
+
if (eng.mounted.has(ws)) return eng.mounted.get(ws);
|
|
1224
|
+
stageWorkspaceDir(eng, ws);
|
|
1127
1225
|
const gitdir = gitdirOf(ws);
|
|
1128
|
-
let restored = false, remoteMain = null, restoreError = null;
|
|
1226
|
+
let restored = false, remoteMain = null, restoreError = null, checkpoint = null;
|
|
1129
1227
|
try {
|
|
1130
1228
|
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
1131
1229
|
remoteMain = (info.refs && info.refs.heads && info.refs.heads.main) || null;
|
|
1132
1230
|
if (remoteMain) {
|
|
1133
1231
|
await git.init({ fs: eng.fs, gitdir, bare: true, defaultBranch: "main" });
|
|
1134
1232
|
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1135
|
-
|
|
1233
|
+
const ckpt = await discoverCheckpointFromCustody(ledger).catch(() => null);
|
|
1234
|
+
let shallowOk = false;
|
|
1235
|
+
if (ckpt?.cursor && ckpt.frontier) {
|
|
1236
|
+
const steps = [32, 96, 256, 1024];
|
|
1237
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1238
|
+
const depth = steps[i], relative = i > 0;
|
|
1239
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, depth, relative, headers: ledger.headers });
|
|
1240
|
+
if (await git.readCommit({ fs: eng.fs, gitdir, oid: ckpt.cursor }).then(() => true, () => false)) { shallowOk = true; break; }
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (!shallowOk) await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, headers: ledger.headers });
|
|
1136
1244
|
await git.writeRef({ fs: eng.fs, gitdir, ref: "refs/heads/main", value: remoteMain, force: true });
|
|
1137
1245
|
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: "ref: refs/heads/main", force: true, symbolic: true });
|
|
1246
|
+
if (shallowOk && ckpt?.blob) {
|
|
1247
|
+
const r = importCheckpoint(eng, ws, ckpt.blob, ckpt.frontier);
|
|
1248
|
+
checkpoint = { restored: !!(r && r.restored), frontierSeeded: !!(r && r.frontierSeeded), cursor: r && r.cursor ? r.cursor : ckpt.cursor, ref: ckpt.ref };
|
|
1249
|
+
}
|
|
1138
1250
|
restored = true;
|
|
1139
1251
|
}
|
|
1140
1252
|
} catch (e) { restoreError = String((e && e.stack) || e).split("\n").slice(0, 5).join(" | "); }
|
|
1141
|
-
const m = { restored, remoteMain, restoreError };
|
|
1253
|
+
const m = { restored, remoteMain, restoreError, checkpoint };
|
|
1142
1254
|
eng.mounted.set(ws, m);
|
|
1143
1255
|
return m;
|
|
1144
1256
|
}
|
|
1145
1257
|
|
|
1258
|
+
function assertAuthored(res, what) {
|
|
1259
|
+
if (!res || res.ok === false) throw new Error(`${what}: ${String((res && res.error) || "law refused the intent").slice(0, 500)}`);
|
|
1260
|
+
return res;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Mount a host-ephemeral workspace. It is still a normal WASI GitHolon under
|
|
1265
|
+
* `/work/ws/<name>`; the host behavior is what differs: no custody restore, no
|
|
1266
|
+
* Artifacts push, and the whole chain dies with the resident host.
|
|
1267
|
+
*/
|
|
1268
|
+
export async function mountEphemeralWorkspace(eng, ws, { domainHash = "", packageUsda = "" } = {}) {
|
|
1269
|
+
let m = eng.mounted.get(ws);
|
|
1270
|
+
if (!m) {
|
|
1271
|
+
stageWorkspaceDir(eng, ws);
|
|
1272
|
+
m = { restored: false, remoteMain: null, restoreError: null, ephemeral: true };
|
|
1273
|
+
eng.mounted.set(ws, m);
|
|
1274
|
+
}
|
|
1275
|
+
if (!nomosActive(eng, ws)) {
|
|
1276
|
+
assertAuthored(
|
|
1277
|
+
author(eng, ws, "bootstrap", "installDomain", installPayload(eng.hashes.nomos, eng.nomosPkg, eng.installedBy), "", { deferProjection: false, rngDraws: 8 }),
|
|
1278
|
+
`ephemeral workspace '${ws}' bootstrap`,
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
if (domainHash && packageUsda && qById(eng, ws, `domain-installation:${domainHash}`).length === 0) {
|
|
1282
|
+
assertAuthored(
|
|
1283
|
+
author(eng, ws, "nomos", "installDomain", installPayload(domainHash, packageUsda, eng.installedBy), eng.hashes.nomos, { deferProjection: false, rngDraws: 8 }),
|
|
1284
|
+
`ephemeral workspace '${ws}' installDomain(${domainHash.slice(0, 12)})`,
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
return m;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/** Author into a host-ephemeral workspace and leave the resulting head in memory only. */
|
|
1291
|
+
export async function authorEphemeral(eng, ws, mountOpts, domain, directiveId, payload, lawHash, opts = {}) {
|
|
1292
|
+
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1293
|
+
return author(eng, ws, domain, directiveId, payload, lawHash, { deferProjection: false, ...opts });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** Query a host-ephemeral workspace, mounting/installing its package first if needed. */
|
|
1297
|
+
export async function queryEphemeral(eng, ws, mountOpts, queryId, paramsJson, principal = "", keys = []) {
|
|
1298
|
+
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1299
|
+
return query(eng, ws, queryId, paramsJson, principal, keys);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1146
1302
|
/** Drop a workspace's in-memory state so the next access re-restores from the ledger. */
|
|
1147
1303
|
export function unmount(eng, ws) {
|
|
1148
1304
|
eng.mounted.delete(ws);
|
|
@@ -1230,8 +1386,8 @@ export function exportCheckpoint(eng, ws) {
|
|
|
1230
1386
|
/** Restore a checkpoint blob into the pool BEFORE the first read on a cold mount (no-op if the pool
|
|
1231
1387
|
* is already warm). The HOST restores ONLY a blob whose stored lawHash matches the workspace's
|
|
1232
1388
|
* CURRENT law (schema parity); a mismatched/absent blob costs at most a cold fold, never a wrong read. */
|
|
1233
|
-
export function importCheckpoint(eng, ws, blobB64) {
|
|
1234
|
-
return JSON.parse(call(eng.ex, "checkpoint_import", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH, blob: blobB64 }, eng.STDERR));
|
|
1389
|
+
export function importCheckpoint(eng, ws, blobB64, frontierB64) {
|
|
1390
|
+
return JSON.parse(call(eng.ex, "checkpoint_import", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH, blob: blobB64, ...(frontierB64 ? { frontier: frontierB64 } : {}) }, eng.STDERR));
|
|
1235
1391
|
}
|
|
1236
1392
|
|
|
1237
1393
|
// ── CHECKPOINT IN CUSTODY (the host-agnostic cold-mount; compute placement is beneath the contract) ──
|
|
@@ -1265,6 +1421,36 @@ async function gunzip(bytes) {
|
|
|
1265
1421
|
return streamAll(new Response(bytes).body.pipeThrough(ds));
|
|
1266
1422
|
}
|
|
1267
1423
|
|
|
1424
|
+
async function discoverCheckpointFromCustody(ledger) {
|
|
1425
|
+
const freshHeaders = () => ({ ...(ledger.headers || {}) });
|
|
1426
|
+
const refs = ((await git.listServerRefs({ http, url: ledger.remote, headers: freshHeaders(), prefix: "refs/meta/checkpoint/", protocolVersion: 2 })) || [])
|
|
1427
|
+
.filter((r) => r.ref && r.ref.startsWith("refs/meta/checkpoint/") && r.oid);
|
|
1428
|
+
if (!refs.length) return null;
|
|
1429
|
+
const root = new Map();
|
|
1430
|
+
root.set("ckpt", new Directory(new Map()));
|
|
1431
|
+
const preopen = new PreopenDirectory("/work", root);
|
|
1432
|
+
const fs = makeGitFs(preopen, "/work", { File, Directory });
|
|
1433
|
+
const gitdir = "/work/ckpt/nomos.git";
|
|
1434
|
+
await git.init({ fs, gitdir, bare: true, defaultBranch: BRANCH });
|
|
1435
|
+
await git.addRemote({ fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1436
|
+
const readFile = async (oid, filepath) => { try { return (await git.readBlob({ fs, gitdir, oid, filepath })).blob; } catch { return null; } };
|
|
1437
|
+
for (let i = refs.length - 1; i >= 0 && i >= refs.length - 3; i--) {
|
|
1438
|
+
const { ref, oid } = refs[i];
|
|
1439
|
+
try {
|
|
1440
|
+
await git.fetch({ fs, http, gitdir, remote: "origin", ref, singleBranch: true, depth: 1, headers: freshHeaders() });
|
|
1441
|
+
const gz = await readFile(oid, "checkpoint");
|
|
1442
|
+
if (!gz) continue;
|
|
1443
|
+
let meta = {}; try { const mb = await readFile(oid, "meta"); if (mb) meta = JSON.parse(dec.decode(mb)); } catch {}
|
|
1444
|
+
const codec = meta.codec ?? "gzip";
|
|
1445
|
+
const blob = dec.decode(codec === "gzip" ? await gunzip(gz) : gz);
|
|
1446
|
+
const fgz = await readFile(oid, "frontier");
|
|
1447
|
+
const frontier = fgz ? dec.decode(codec === "gzip" ? await gunzip(fgz) : fgz) : null;
|
|
1448
|
+
return { ref, oid, cursor: meta.cursor || null, blob, frontier };
|
|
1449
|
+
} catch { /* try an older checkpoint */ }
|
|
1450
|
+
}
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1268
1454
|
/** Wrap an exported checkpoint into git objects under `refs/meta/checkpoint/<lawHash>` (LOCAL only —
|
|
1269
1455
|
* no network). The ref points at a parentless commit whose tree carries `checkpoint` (the b64 blob)
|
|
1270
1456
|
* and `meta` ({cursor,bytes,lawHash}). Returns {ref, commitOid}. Separated from the push so the
|
|
@@ -1273,25 +1459,41 @@ export async function writeCheckpointObjects(eng, ws, ex, lawHash) {
|
|
|
1273
1459
|
const gitdir = gitdirOf(ws);
|
|
1274
1460
|
const gz = await gzip(enc.encode(ex.blob));
|
|
1275
1461
|
const blobOid = await git.writeBlob({ fs: eng.fs, gitdir, blob: gz });
|
|
1276
|
-
|
|
1277
|
-
|
|
1462
|
+
// THE FOLD FRONTIER (the gate's Workspace state @cursor) rides ALONGSIDE the projection so a cold mount
|
|
1463
|
+
// can AUTHOR on a shallow clone (the projection alone is a read-collapsed view the gate can't fold from).
|
|
1464
|
+
// Era-safe: pre-this-era exports carry no `ex.frontier`, so the `frontier` file is simply absent and a
|
|
1465
|
+
// reader falls back to a full clone (writes need full history). Gzipped like the projection blob.
|
|
1466
|
+
const tree = [
|
|
1278
1467
|
{ mode: "100644", path: "checkpoint", oid: blobOid, type: "blob" },
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1468
|
+
];
|
|
1469
|
+
let fgzLen = 0;
|
|
1470
|
+
if (ex.frontier) {
|
|
1471
|
+
const fgz = await gzip(enc.encode(ex.frontier));
|
|
1472
|
+
fgzLen = fgz.length;
|
|
1473
|
+
tree.push({ mode: "100644", path: "frontier", oid: await git.writeBlob({ fs: eng.fs, gitdir, blob: fgz }), type: "blob" });
|
|
1474
|
+
}
|
|
1475
|
+
const metaOid = await git.writeBlob({ fs: eng.fs, gitdir, blob: enc.encode(JSON.stringify({ cursor: ex.cursor, bytes: ex.bytes ?? 0, lawHash, codec: "gzip", raw: ex.blob.length, stored: gz.length, frontierStored: fgzLen })) });
|
|
1476
|
+
tree.push({ mode: "100644", path: "meta", oid: metaOid, type: "blob" });
|
|
1477
|
+
const tree_ = await git.writeTree({ fs: eng.fs, gitdir, tree });
|
|
1478
|
+
const commitOid = await git.writeCommit({ fs: eng.fs, gitdir, commit: { tree: tree_, parent: [], author: CKPT_AUTHOR, committer: CKPT_AUTHOR, message: `checkpoint ${ws} @${String(ex.cursor).slice(0, 16)}` } });
|
|
1282
1479
|
const ref = CKPT_REF(lawHash);
|
|
1283
1480
|
await git.writeRef({ fs: eng.fs, gitdir, ref, value: commitOid, force: true });
|
|
1284
1481
|
return { ref, commitOid };
|
|
1285
1482
|
}
|
|
1286
1483
|
|
|
1287
|
-
/** Read a checkpoint blob back out of a commit oid
|
|
1288
|
-
* via a prior write or a shallow fetch). Returns
|
|
1484
|
+
/** Read a checkpoint's projection blob (and its fold frontier, if present) back out of a commit oid
|
|
1485
|
+
* (LOCAL only — the objects must already be present, via a prior write or a shallow fetch). Returns
|
|
1486
|
+
* `{ blob, frontier }` — both the b64 strings importCheckpoint accepts; `frontier` is null for a
|
|
1487
|
+
* pre-this-era checkpoint (no `frontier` file). */
|
|
1289
1488
|
export async function readCheckpointBlob(eng, ws, commitOid) {
|
|
1290
1489
|
const gitdir = gitdirOf(ws);
|
|
1291
1490
|
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "checkpoint" });
|
|
1292
1491
|
let codec = "gzip";
|
|
1293
1492
|
try { codec = JSON.parse(dec.decode((await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "meta" })).blob)).codec ?? "gzip"; } catch {}
|
|
1294
|
-
|
|
1493
|
+
const dezip = async (b) => dec.decode(codec === "gzip" ? await gunzip(b) : b);
|
|
1494
|
+
let frontier = null;
|
|
1495
|
+
try { frontier = await dezip((await git.readBlob({ fs: eng.fs, gitdir, oid: commitOid, filepath: "frontier" })).blob); } catch { /* pre-this-era: no frontier */ }
|
|
1496
|
+
return { blob: await dezip(blob), frontier };
|
|
1295
1497
|
}
|
|
1296
1498
|
|
|
1297
1499
|
/** The checkpoint's schema-parity key: a hash of the workspace's CURRENT law map (every installed
|
|
@@ -1312,13 +1514,18 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1312
1514
|
try {
|
|
1313
1515
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1314
1516
|
if (!lawHash) return { pushed: false, reason: "no-law" };
|
|
1517
|
+
const gitdir = gitdirOf(ws);
|
|
1518
|
+
const head = await git.resolveRef({ fs: eng.fs, gitdir, ref: "refs/heads/main" }).catch(() => null);
|
|
1519
|
+
eng._ckptPushed ??= new Map();
|
|
1520
|
+
if (head && eng._ckptPushed.get(ws) === `${lawHash}:${head}`) {
|
|
1521
|
+
return { pushed: false, reason: "unchanged" };
|
|
1522
|
+
}
|
|
1315
1523
|
const ex = exportCheckpoint(eng, ws);
|
|
1316
1524
|
if (!ex || ex.empty || !ex.ok || !ex.blob) return { pushed: false, reason: ex && ex.empty ? "empty" : "no-blob" };
|
|
1317
|
-
eng._ckptPushed ??= new Map();
|
|
1318
1525
|
const tag = `${lawHash}:${ex.cursor}`;
|
|
1319
1526
|
if (eng._ckptPushed.get(ws) === tag) return { pushed: false, reason: "unchanged" };
|
|
1320
1527
|
const { ref } = await writeCheckpointObjects(eng, ws, ex, lawHash);
|
|
1321
|
-
await git.push({ fs: eng.fs, http, gitdir
|
|
1528
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, remoteRef: ref, force: true, headers: ledger.headers }));
|
|
1322
1529
|
eng._ckptPushed.set(ws, tag);
|
|
1323
1530
|
return { pushed: true, cursor: ex.cursor, bytes: ex.bytes ?? 0 };
|
|
1324
1531
|
} catch (e) { return { pushed: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
@@ -1329,25 +1536,40 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1329
1536
|
* absent or the law drifted. Returns {restored, cursor?, reason?}. */
|
|
1330
1537
|
export async function restoreCheckpointFromCustody(eng, ws, ledger, lawHash) {
|
|
1331
1538
|
try {
|
|
1539
|
+
const mounted = eng.mounted.get(ws);
|
|
1540
|
+
if (mounted?.checkpoint?.restored) {
|
|
1541
|
+
return { restored: false, reason: "already-mounted-checkpoint" };
|
|
1542
|
+
}
|
|
1332
1543
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1333
1544
|
if (!lawHash) return { restored: false, reason: "no-law" };
|
|
1334
1545
|
const ref = CKPT_REF(lawHash);
|
|
1335
|
-
const
|
|
1546
|
+
const freshHeaders = () => ({ ...(ledger.headers || {}) });
|
|
1547
|
+
const serverRefs = await git.listServerRefs({ http, url: ledger.remote, headers: freshHeaders(), prefix: ref, protocolVersion: 2 });
|
|
1336
1548
|
const found = (serverRefs || []).find((r) => r.ref === ref);
|
|
1337
1549
|
if (!found || !found.oid) return { restored: false, reason: "absent" };
|
|
1338
1550
|
const gitdir = gitdirOf(ws);
|
|
1339
|
-
await git.fetch({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, singleBranch: true, depth: 1, headers:
|
|
1340
|
-
const
|
|
1341
|
-
const r = importCheckpoint(eng, ws,
|
|
1342
|
-
|
|
1551
|
+
await git.fetch({ fs: eng.fs, http, gitdir, url: ledger.remote, ref, singleBranch: true, depth: 1, headers: freshHeaders() });
|
|
1552
|
+
const { blob, frontier } = await readCheckpointBlob(eng, ws, found.oid);
|
|
1553
|
+
const r = importCheckpoint(eng, ws, blob, frontier);
|
|
1554
|
+
if (r && r.restored === true && r.cursor) {
|
|
1555
|
+
eng._ckptPushed ??= new Map();
|
|
1556
|
+
eng._ckptPushed.set(ws, `${lawHash}:${r.cursor}`);
|
|
1557
|
+
}
|
|
1558
|
+
return { restored: r && r.restored === true, frontierSeeded: r && r.frontierSeeded === true, cursor: r && r.cursor, reason: r && r.reason };
|
|
1343
1559
|
} catch (e) { return { restored: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
1344
1560
|
}
|
|
1345
1561
|
|
|
1562
|
+
// Serialize a push through the host's per-repo lane when present (the DO threads `ledger.pushLane`); the
|
|
1563
|
+
// container has none and serializes on its own chain. The point: ONE receive-pack to a given Artifacts repo
|
|
1564
|
+
// at a time — a detached checkpoint/shape push can never race the admit's main push (concurrent receive-packs
|
|
1565
|
+
// were corrupting/500ing the custody store). All same-repo pushes below funnel through here.
|
|
1566
|
+
const pushVia = (ledger, fn) => (ledger && ledger.pushLane ? ledger.pushLane(fn) : fn());
|
|
1567
|
+
|
|
1346
1568
|
// DURABILITY-BEFORE-ACK: only acked once the write LANDED in the ledger; a failed push
|
|
1347
1569
|
// unmounts (drops local state) so the next access re-restores from custody.
|
|
1348
1570
|
export async function commitAndPush(eng, ws, ledger) {
|
|
1349
1571
|
try {
|
|
1350
|
-
await git.push({ fs: eng.fs, http, gitdir: gitdirOf(ws), url: ledger.remote, ref: "main", remoteRef: "main", force: false, headers: ledger.headers });
|
|
1572
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir: gitdirOf(ws), url: ledger.remote, ref: "main", remoteRef: "main", force: false, headers: ledger.headers }));
|
|
1351
1573
|
} catch (e) {
|
|
1352
1574
|
unmount(eng, ws);
|
|
1353
1575
|
throw new Error(`ledger push failed — write NOT durable, rolled back to ledger: ${e}`);
|
|
@@ -1374,9 +1596,9 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1374
1596
|
eng._lastShapeRef ??= new Map();
|
|
1375
1597
|
if (eng._lastShapeRef.get(ws) === newRef) return;
|
|
1376
1598
|
await git.writeRef({ fs: eng.fs, gitdir, ref: newRef, value: head, force: true });
|
|
1377
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: newRef, remoteRef: newRef, force: true, headers: ledger.headers });
|
|
1599
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: newRef, remoteRef: newRef, force: true, headers: ledger.headers }));
|
|
1378
1600
|
const prev = eng._lastShapeRef.get(ws);
|
|
1379
|
-
if (prev && prev !== newRef) { try { await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: prev, remoteRef: prev, delete: true, headers: ledger.headers }); } catch { /* leftover refs are harmless: the reader takes max(bytes) */ } }
|
|
1601
|
+
if (prev && prev !== newRef) { try { await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: prev, remoteRef: prev, delete: true, headers: ledger.headers })); } catch { /* leftover refs are harmless: the reader takes max(bytes) */ } }
|
|
1380
1602
|
eng._lastShapeRef.set(ws, newRef);
|
|
1381
1603
|
} catch { /* best-effort; placement self-heals */ }
|
|
1382
1604
|
}
|
|
@@ -1385,7 +1607,7 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1385
1607
|
// Returns {bytes} (the max, in case a restart left a stale ref) or null (no shape yet).
|
|
1386
1608
|
export async function readShape(ledger) {
|
|
1387
1609
|
try {
|
|
1388
|
-
const refs = await git.listServerRefs({ http, url: ledger.remote, headers: ledger.headers, prefix: "refs/meta/shape/", protocolVersion: 2 });
|
|
1610
|
+
const refs = await git.listServerRefs({ http, url: ledger.remote, headers: { ...(ledger.headers || {}) }, prefix: "refs/meta/shape/", protocolVersion: 2 });
|
|
1389
1611
|
let best = null;
|
|
1390
1612
|
for (const r of refs || []) {
|
|
1391
1613
|
const m = /refs\/meta\/shape\/b(\d+)$/.exec(r.ref || "");
|
|
@@ -1482,13 +1704,53 @@ async function mainIntentIds(eng, ws) {
|
|
|
1482
1704
|
* engine has no durable store), obvious attacks drop. Admitted intents merge to main,
|
|
1483
1705
|
* durably push, the branch is consumed.
|
|
1484
1706
|
*/
|
|
1485
|
-
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null } = {}) {
|
|
1707
|
+
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null, sessionRefs = null } = {}) {
|
|
1486
1708
|
const refuse = new Set(refuseDomains);
|
|
1487
1709
|
const routes = shardRouting ? directiveRoutes(eng) : null;
|
|
1488
1710
|
const gitdir = gitdirOf(ws);
|
|
1489
|
-
|
|
1490
|
-
const
|
|
1711
|
+
// FLAME-GRAPH EDGE TIMINGS — coarse per-stage wall-clock the client stitches into the cross-tier trace.
|
|
1712
|
+
const _t0 = Date.now(); let _fetchMs = 0, _sealMs = 0, _intents = 0;
|
|
1713
|
+
const _spans = []; let _traceSeq = 0;
|
|
1714
|
+
const _span = (name, kind, depth = 1, extra = {}) => {
|
|
1715
|
+
const t0 = Date.now();
|
|
1716
|
+
return (end = {}) => {
|
|
1717
|
+
const t1 = Date.now();
|
|
1718
|
+
_spans.push({ id: `admit-${_t0}-${_traceSeq++}`, track: "cloud", name, kind, depth, t0, t1, ...extra, ...end });
|
|
1719
|
+
};
|
|
1720
|
+
};
|
|
1721
|
+
const sessionRefPrefix = "refs/heads/session/";
|
|
1722
|
+
const sessions = {};
|
|
1723
|
+
const indexedRefs = sessionRefs && typeof sessionRefs === "object" ? sessionRefs : null;
|
|
1724
|
+
const _scanDone = _span(indexedRefs ? "scan session index" : "scan session refs", "cscan", 1);
|
|
1725
|
+
if (indexedRefs) {
|
|
1726
|
+
const entries = Array.isArray(indexedRefs)
|
|
1727
|
+
? indexedRefs.map((r) => [String(r?.session || r?.cid || r?.client || ""), String(r?.head || r?.oid || "")])
|
|
1728
|
+
: Object.entries(indexedRefs).map(([cid, head]) => [cid, String(head || "")]);
|
|
1729
|
+
for (const [cid, head] of entries) {
|
|
1730
|
+
if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(cid) && /^[0-9a-f]{40}$/.test(head)) sessions[cid] = head;
|
|
1731
|
+
}
|
|
1732
|
+
} else {
|
|
1733
|
+
const refs = await git.listServerRefs({
|
|
1734
|
+
http,
|
|
1735
|
+
url: ledger.remote,
|
|
1736
|
+
headers: ledger.headers,
|
|
1737
|
+
prefix: sessionRefPrefix,
|
|
1738
|
+
protocolVersion: 2,
|
|
1739
|
+
});
|
|
1740
|
+
for (const r of refs || []) {
|
|
1741
|
+
const ref = typeof r?.ref === "string" ? r.ref : "";
|
|
1742
|
+
const oid = typeof r?.oid === "string" ? r.oid : "";
|
|
1743
|
+
if (!ref.startsWith(sessionRefPrefix) || !oid) continue;
|
|
1744
|
+
sessions[ref.slice(sessionRefPrefix.length)] = oid;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
_scanDone({ sessions: Object.keys(sessions).length });
|
|
1748
|
+
const _scanMs = Date.now() - _t0;
|
|
1491
1749
|
const results = [], deadLetters = [], placements = [], splits = [], globalsOut = [];
|
|
1750
|
+
// BATCH THE MAIN PUSH: every session folds onto local main in the loop, then ONE commitAndPush seals the
|
|
1751
|
+
// whole cycle (instead of one receive-pack per session). `consumed` are the session branches we processed
|
|
1752
|
+
// and may delete AFTER the batch push lands — a failed batch leaves them for re-admit (idempotent by id).
|
|
1753
|
+
const consumed = []; let totalAdmitted = 0;
|
|
1492
1754
|
// §5.4 (slice 4): on the COORDINATOR of a taxonomy that declares `.global(...)`
|
|
1493
1755
|
// reference data, every admitted intent touching a global aggregate is surfaced
|
|
1494
1756
|
// to the HOST, which replicates the SAME sealed bytes into each shard chain.
|
|
@@ -1518,9 +1780,27 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1518
1780
|
const headBefore = summaryCtx ? await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null) : null;
|
|
1519
1781
|
for (const [cid, head] of Object.entries(sessions)) {
|
|
1520
1782
|
const ref = `refs/heads/session/${cid}`;
|
|
1783
|
+
const shortCid = cid.length > 14 ? `${cid.slice(0, 14)}...` : cid;
|
|
1784
|
+
const _sessionDone = _span(`session ${shortCid}`, "csession", 1, { client: cid });
|
|
1521
1785
|
try {
|
|
1522
1786
|
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1523
|
-
|
|
1787
|
+
const _fetchDone = _span(`fetch ${shortCid}`, "cfetch", 2, { client: cid });
|
|
1788
|
+
const _f = Date.now();
|
|
1789
|
+
try {
|
|
1790
|
+
// THIN-FETCH (the 40s→0.5s fix): a `singleBranch` fetch sets isomorphic-git's have-set to
|
|
1791
|
+
// [the fetched ref] ONLY. The edge holds no local session ref yet ⇒ it offers ZERO haves ⇒ the
|
|
1792
|
+
// server re-ships the ENTIRE history (O(chain): measured ~10MB/40s for a 1-commit delta).
|
|
1793
|
+
// Pre-seed the local session ref to OUR main head so the negotiation offers main as a have and the
|
|
1794
|
+
// server sends only the delta above the common ancestor (~1KB/0.5s). main is append-only, so the
|
|
1795
|
+
// client's fork point is always an ancestor of our head — the thin pack is always sound.
|
|
1796
|
+
const localMain = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
1797
|
+
if (localMain) { try { await git.writeRef({ fs: eng.fs, gitdir, ref, value: localMain, force: true }); } catch {} }
|
|
1798
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref, singleBranch: true, headers: ledger.headers });
|
|
1799
|
+
} finally {
|
|
1800
|
+
_fetchMs += Date.now() - _f;
|
|
1801
|
+
_fetchDone();
|
|
1802
|
+
}
|
|
1803
|
+
const _walkDone = _span(`walk ${shortCid}`, "cwalk", 2, { client: cid });
|
|
1524
1804
|
const main = await mainIntentIds(eng, ws);
|
|
1525
1805
|
const chain = [];
|
|
1526
1806
|
for (const c of await git.log({ fs: eng.fs, gitdir, ref: head, depth: 100 })) {
|
|
@@ -1528,7 +1808,9 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1528
1808
|
chain.push(c.oid);
|
|
1529
1809
|
}
|
|
1530
1810
|
chain.reverse(); // oldest → newest
|
|
1811
|
+
_walkDone({ intents: chain.length });
|
|
1531
1812
|
const admitted = [], rejected = [], skipped = [];
|
|
1813
|
+
const _gateDone = _span(`gate+fold ${shortCid}`, "cgate", 2, { client: cid, intents: chain.length });
|
|
1532
1814
|
for (const oid of chain) {
|
|
1533
1815
|
try {
|
|
1534
1816
|
const { blob } = await git.readBlob({ fs: eng.fs, gitdir, oid, filepath: "intent.json" });
|
|
@@ -1685,11 +1967,40 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1685
1967
|
rejected.push({ oid: oid.slice(0, 10), error: String(e).slice(0, 200) });
|
|
1686
1968
|
}
|
|
1687
1969
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1970
|
+
_gateDone({ admitted: admitted.length, rejected: rejected.length, skipped: skipped.length });
|
|
1971
|
+
// FOLDED onto local main; the receive-pack is BATCHED below (one per cycle, not one per session).
|
|
1972
|
+
// Record the branch as consumed so it's deleted only AFTER the batch push lands.
|
|
1973
|
+
_intents += admitted.length;
|
|
1974
|
+
totalAdmitted += admitted.length;
|
|
1975
|
+
consumed.push({ cid, ref });
|
|
1690
1976
|
results.push({ session: cid, admitted, rejected, ...(skipped.length ? { skipped } : {}) });
|
|
1691
1977
|
} catch (e) {
|
|
1692
1978
|
results.push({ session: cid, error: String(e).slice(0, 200) });
|
|
1979
|
+
} finally {
|
|
1980
|
+
_sessionDone();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
// THE BATCH SEAL — one receive-pack for the whole admit cycle. A failed push unmounts (engine.unmount drops
|
|
1984
|
+
// the ws fs); we then SKIP the branch deletes so every offered intent re-admits next cycle (idempotent by
|
|
1985
|
+
// id). On success, delete the consumed session branches (best-effort cleanup; a stale branch is a harmless
|
|
1986
|
+
// re-fetch). Single-client workspaces were already one push/cycle; this collapses collaborative bursts.
|
|
1987
|
+
let _sealed = true;
|
|
1988
|
+
if (totalAdmitted) {
|
|
1989
|
+
const _sealDone = _span(`seal (batch)`, "cseal", 1, { sessions: consumed.length, admitted: totalAdmitted });
|
|
1990
|
+
const _s = Date.now();
|
|
1991
|
+
try {
|
|
1992
|
+
await commitAndPush(eng, ws, ledger);
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
_sealed = false;
|
|
1995
|
+
log("admit-batch-push-failed", { ws, sessions: consumed.length, admitted: totalAdmitted, error: String(e).slice(0, 200) });
|
|
1996
|
+
} finally {
|
|
1997
|
+
_sealMs += Date.now() - _s;
|
|
1998
|
+
_sealDone();
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
if (_sealed) {
|
|
2002
|
+
for (const { ref } of consumed) {
|
|
2003
|
+
try { await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: ref, delete: true, headers: ledger.headers })); } catch {}
|
|
1693
2004
|
}
|
|
1694
2005
|
}
|
|
1695
2006
|
const newMain = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
@@ -1697,8 +2008,11 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1697
2008
|
// The coalesced §5.2 delta of this batch (null when no scoped read moved): the HOST
|
|
1698
2009
|
// closes it against the coordinator's recorded frontier and authors the Order.
|
|
1699
2010
|
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, newMain) : null;
|
|
2011
|
+
const _total = Date.now() - _t0;
|
|
2012
|
+
// EDGE FLAME SPANS: scan refs → fetch session branches → gate+fold each intent → seal to custody.
|
|
2013
|
+
const timings = { totalMs: _total, scanMs: _scanMs, fetchMs: _fetchMs, sealMs: _sealMs, gateFoldMs: Math.max(0, _total - _scanMs - _fetchMs - _sealMs), intents: _intents, sessions: results.length, spans: _spans };
|
|
1700
2014
|
return {
|
|
1701
|
-
sessions: results, main: newMain, deadLetters,
|
|
2015
|
+
sessions: results, main: newMain, deadLetters, timings,
|
|
1702
2016
|
...(summary ? { summary } : {}),
|
|
1703
2017
|
...(placements.length ? { placements } : {}),
|
|
1704
2018
|
...(splits.length ? { splits } : {}),
|
|
@@ -1755,8 +2069,8 @@ export async function archiveRefusedMain(eng, ws, ledger, label) {
|
|
|
1755
2069
|
if (!m.restored) { unmount(eng, ws); return { archived: false, error: "no main to archive" }; }
|
|
1756
2070
|
const gitdir = gitdirOf(ws);
|
|
1757
2071
|
try {
|
|
1758
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/refused/${label}`, force: true, headers: ledger.headers });
|
|
1759
|
-
await git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/${BRANCH}`, delete: true, headers: ledger.headers });
|
|
2072
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/refused/${label}`, force: true, headers: ledger.headers }));
|
|
2073
|
+
await pushVia(ledger, () => git.push({ fs: eng.fs, http, gitdir, url: ledger.remote, ref: BRANCH, remoteRef: `refs/heads/${BRANCH}`, delete: true, headers: ledger.headers }));
|
|
1760
2074
|
return { archived: true, ref: `refs/heads/refused/${label}` };
|
|
1761
2075
|
} finally {
|
|
1762
2076
|
unmount(eng, ws);
|
|
@@ -1823,6 +2137,55 @@ export function stats(eng) {
|
|
|
1823
2137
|
};
|
|
1824
2138
|
}
|
|
1825
2139
|
|
|
2140
|
+
export async function ledgerStats(eng, ws, ledger = null) {
|
|
2141
|
+
const gitdir = gitdirOf(ws);
|
|
2142
|
+
let head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
2143
|
+
if (!head && ledger?.remote) {
|
|
2144
|
+
try {
|
|
2145
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
2146
|
+
const remoteMain = (info.refs && info.refs.heads && info.refs.heads[BRANCH]) || null;
|
|
2147
|
+
if (!remoteMain) return { head: null, commits: 0 };
|
|
2148
|
+
await git.init({ fs: eng.fs, gitdir, bare: true, defaultBranch: BRANCH }).catch(() => {});
|
|
2149
|
+
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
2150
|
+
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: BRANCH, singleBranch: true, headers: ledger.headers });
|
|
2151
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: `refs/heads/${BRANCH}`, value: remoteMain, force: true });
|
|
2152
|
+
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: `ref: refs/heads/${BRANCH}`, force: true, symbolic: true });
|
|
2153
|
+
head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
2154
|
+
const mounted = eng.mounted.get(ws);
|
|
2155
|
+
if (mounted) {
|
|
2156
|
+
mounted.restored = true;
|
|
2157
|
+
mounted.remoteMain = remoteMain;
|
|
2158
|
+
mounted.restoreError = null;
|
|
2159
|
+
}
|
|
2160
|
+
} catch {}
|
|
2161
|
+
}
|
|
2162
|
+
if (!head) return { head: null, commits: 0 };
|
|
2163
|
+
const mounted = eng.mounted.get(ws);
|
|
2164
|
+
if (mounted?.ledgerStats?.head === head) return mounted.ledgerStats;
|
|
2165
|
+
const commits = (await git.log({ fs: eng.fs, gitdir, ref: head, depth: 10000000 })).length;
|
|
2166
|
+
const out = { head, commits };
|
|
2167
|
+
if (mounted) mounted.ledgerStats = out;
|
|
2168
|
+
return out;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
export async function countRemoteCommits(ledger) {
|
|
2172
|
+
const info = await git.getRemoteInfo({ http, url: ledger.remote, headers: ledger.headers });
|
|
2173
|
+
const head = (info.refs && info.refs.heads && info.refs.heads[BRANCH]) || null;
|
|
2174
|
+
if (!head) return { head: null, commits: 0 };
|
|
2175
|
+
const root = new Map();
|
|
2176
|
+
root.set("count", new Directory(new Map()));
|
|
2177
|
+
const preopen = new PreopenDirectory("/work", root);
|
|
2178
|
+
const fs = makeGitFs(preopen, "/work", { File, Directory });
|
|
2179
|
+
const gitdir = "/work/count/nomos.git";
|
|
2180
|
+
await git.init({ fs, gitdir, bare: true, defaultBranch: BRANCH });
|
|
2181
|
+
await git.addRemote({ fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
2182
|
+
await git.fetch({ fs, http, gitdir, remote: "origin", ref: BRANCH, singleBranch: true, headers: ledger.headers });
|
|
2183
|
+
await git.writeRef({ fs, gitdir, ref: `refs/heads/${BRANCH}`, value: head, force: true });
|
|
2184
|
+
await git.writeRef({ fs, gitdir, ref: "HEAD", value: `ref: refs/heads/${BRANCH}`, force: true, symbolic: true });
|
|
2185
|
+
const commits = (await git.log({ fs, gitdir, ref: head, depth: 10000000 })).length;
|
|
2186
|
+
return { head, commits };
|
|
2187
|
+
}
|
|
2188
|
+
|
|
1826
2189
|
/** Per-branch projection observables (cursor, projectCalls, sqliteBytes — see #37). */
|
|
1827
2190
|
export const projectionStats = (eng, ws) => JSON.parse(call(eng.ex, "projection_stats", ws ? { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH } : {}, eng.STDERR));
|
|
1828
2191
|
|