@forgeax/engine-ddc 0.1.3 → 0.1.4
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/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/generation-session.integration.test.d.ts +2 -0
- package/dist/__tests__/generation-session.integration.test.d.ts.map +1 -0
- package/dist/__tests__/generation-session.unit.test.d.ts +2 -0
- package/dist/__tests__/generation-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
- package/dist/build-cache.d.ts +18 -0
- package/dist/build-cache.d.ts.map +1 -0
- package/dist/entry-store.d.ts +1 -0
- package/dist/entry-store.d.ts.map +1 -1
- package/dist/entry-store.mjs +28 -18
- package/dist/entry-store.mjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +615 -57
- package/dist/index.mjs.map +1 -1
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/publication.d.ts +50 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/session.d.ts +45 -0
- package/dist/session.d.ts.map +1 -0
- package/package.json +4 -1
- package/src/__tests__/generation-session.integration.test.ts +74 -0
- package/src/__tests__/generation-session.unit.test.ts +35 -0
- package/src/__tests__/owner-chain.integration.test.ts +45 -0
- package/src/build-cache.ts +60 -0
- package/src/entry-store.ts +27 -17
- package/src/index.ts +24 -1
- package/src/lifecycle.ts +38 -0
- package/src/publication.ts +444 -0
- package/src/session.ts +254 -0
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,70 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve, join, dirname, isAbsolute, normalize } from 'path';
|
|
1
3
|
import { createHash, randomUUID } from 'crypto';
|
|
2
|
-
import { mkdir, writeFile,
|
|
3
|
-
import {
|
|
4
|
+
import { mkdir, writeFile, rm, stat, rename, readdir, readFile } from 'fs/promises';
|
|
5
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
4
6
|
|
|
5
|
-
// src/
|
|
7
|
+
// src/build-cache.ts
|
|
8
|
+
function canonicalDdcJson(value) {
|
|
9
|
+
const sorted = sortValue(value);
|
|
10
|
+
return JSON.stringify(sorted) ?? "null";
|
|
11
|
+
}
|
|
12
|
+
function sortValue(value) {
|
|
13
|
+
if (value instanceof Uint8Array) {
|
|
14
|
+
return { encoding: "base64", bytes: Buffer.from(value).toString("base64") };
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
17
|
+
if (value !== null && typeof value === "object") {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const key of Object.keys(value).sort()) {
|
|
20
|
+
result[key] = sortValue(value[key]);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function semanticDdcKey(input) {
|
|
27
|
+
const semantic = {
|
|
28
|
+
schemaVersion: input.schemaVersion,
|
|
29
|
+
importer: input.importer,
|
|
30
|
+
codec: input.codec,
|
|
31
|
+
settings: input.settings,
|
|
32
|
+
sourceBytes: input.sourceBytes,
|
|
33
|
+
declaredGuids: [...input.declaredGuids].sort(),
|
|
34
|
+
targetProfile: input.targetProfile,
|
|
35
|
+
producer: input.producer
|
|
36
|
+
};
|
|
37
|
+
return createHash("sha256").update(canonicalDdcJson(semantic)).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/build-cache.ts
|
|
41
|
+
function resolveDdcRoot(cwd) {
|
|
42
|
+
let current = resolve(cwd);
|
|
43
|
+
while (true) {
|
|
44
|
+
if (existsSync(join(current, "pnpm-workspace.yaml"))) {
|
|
45
|
+
return join(current, "node_modules/.cache/forgeax-ddc");
|
|
46
|
+
}
|
|
47
|
+
const parent = dirname(current);
|
|
48
|
+
if (parent === current) return join(resolve(cwd), "node_modules/.cache/forgeax-ddc");
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function semanticBuildKey(input) {
|
|
53
|
+
const hasSourceOverrides = input.sourceOverrides !== void 0 && typeof input.sourceOverrides === "object" && input.sourceOverrides !== null && !Array.isArray(input.sourceOverrides) && Object.keys(input.sourceOverrides).length > 0;
|
|
54
|
+
const settings = hasSourceOverrides ? { settings: input.settings, sourceOverrides: input.sourceOverrides } : input.settings;
|
|
55
|
+
return semanticDdcKey({
|
|
56
|
+
schemaVersion: input.schemaVersion,
|
|
57
|
+
importer: input.importerVersion,
|
|
58
|
+
codec: input.codecVersion,
|
|
59
|
+
settings,
|
|
60
|
+
sourceBytes: input.sourceDependencies.map(
|
|
61
|
+
(dependency) => typeof dependency === "string" ? dependency.replaceAll("\\", "/").replace(/^.*\/(assets\/)/, "$1") : dependency.digest
|
|
62
|
+
).sort().map((digest3) => new TextEncoder().encode(digest3)),
|
|
63
|
+
declaredGuids: input.declaredGuids,
|
|
64
|
+
targetProfile: input.cookProfile,
|
|
65
|
+
producer: input.importerVersion
|
|
66
|
+
});
|
|
67
|
+
}
|
|
6
68
|
|
|
7
69
|
// src/errors.ts
|
|
8
70
|
var DDC_ERROR_CODES = [
|
|
@@ -109,37 +171,6 @@ var DdcStoreError = class extends Error {
|
|
|
109
171
|
};
|
|
110
172
|
}
|
|
111
173
|
};
|
|
112
|
-
function canonicalDdcJson(value) {
|
|
113
|
-
const sorted = sortValue(value);
|
|
114
|
-
return JSON.stringify(sorted) ?? "null";
|
|
115
|
-
}
|
|
116
|
-
function sortValue(value) {
|
|
117
|
-
if (value instanceof Uint8Array) {
|
|
118
|
-
return { encoding: "base64", bytes: Buffer.from(value).toString("base64") };
|
|
119
|
-
}
|
|
120
|
-
if (Array.isArray(value)) return value.map(sortValue);
|
|
121
|
-
if (value !== null && typeof value === "object") {
|
|
122
|
-
const result = {};
|
|
123
|
-
for (const key of Object.keys(value).sort()) {
|
|
124
|
-
result[key] = sortValue(value[key]);
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
function semanticDdcKey(input) {
|
|
131
|
-
const semantic = {
|
|
132
|
-
schemaVersion: input.schemaVersion,
|
|
133
|
-
importer: input.importer,
|
|
134
|
-
codec: input.codec,
|
|
135
|
-
settings: input.settings,
|
|
136
|
-
sourceBytes: input.sourceBytes,
|
|
137
|
-
declaredGuids: [...input.declaredGuids].sort(),
|
|
138
|
-
targetProfile: input.targetProfile,
|
|
139
|
-
producer: input.producer
|
|
140
|
-
};
|
|
141
|
-
return createHash("sha256").update(canonicalDdcJson(semantic)).digest("hex");
|
|
142
|
-
}
|
|
143
174
|
|
|
144
175
|
// src/entry-store.ts
|
|
145
176
|
function digest(value) {
|
|
@@ -238,30 +269,39 @@ var DdcEntryStore = class {
|
|
|
238
269
|
throw new DdcStoreError("ddc-entry-invalid", "receipt output digest does not match entry");
|
|
239
270
|
}
|
|
240
271
|
const path = join(this.staging, `${entry.key}-${randomUUID()}`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
272
|
+
try {
|
|
273
|
+
await mkdir(join(path, "artifacts"), { recursive: true });
|
|
274
|
+
await writeFile(join(path, "payload.json"), canonicalDdcJson(entry.payload));
|
|
275
|
+
await writeFile(join(path, "refs.json"), canonicalDdcJson(entry.refs));
|
|
276
|
+
await writeFile(join(path, "receipt.json"), canonicalDdcJson(entry.receipt));
|
|
277
|
+
const artifacts = {};
|
|
278
|
+
for (const [key, artifact] of sortedArtifacts(entry)) {
|
|
279
|
+
const file = artifactFile(key);
|
|
280
|
+
artifacts[key] = { mediaType: artifact.mediaType, file };
|
|
281
|
+
await writeFile(join(path, "artifacts", `${file}.bin`), artifact.bytes);
|
|
282
|
+
}
|
|
283
|
+
await writeFile(join(path, "artifacts.json"), canonicalDdcJson(artifacts));
|
|
284
|
+
await writeFile(join(path, "integrity.json"), canonicalDdcJson(integrityFor(entry)));
|
|
285
|
+
return { key: entry.key, path };
|
|
286
|
+
} catch (error) {
|
|
287
|
+
await rm(path, { recursive: true, force: true }).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
throw error;
|
|
250
290
|
}
|
|
251
|
-
await writeFile(join(path, "artifacts.json"), canonicalDdcJson(artifacts));
|
|
252
|
-
await writeFile(join(path, "integrity.json"), canonicalDdcJson(integrityFor(entry)));
|
|
253
|
-
return { key: entry.key, path };
|
|
254
291
|
}
|
|
255
292
|
async publish(staged) {
|
|
256
293
|
validateKey(staged.key);
|
|
257
294
|
const candidate = await this.readDirectory(staged.path, true);
|
|
258
295
|
const target = join(this.entries, staged.key);
|
|
259
296
|
await mkdir(this.entries, { recursive: true });
|
|
260
|
-
|
|
261
|
-
await stat(target);
|
|
297
|
+
const useExisting = async () => {
|
|
262
298
|
const existing = await this.readDirectory(target, false);
|
|
263
299
|
await rm(staged.path, { recursive: true, force: true });
|
|
264
300
|
return existingResult(existing, candidate, staged.key);
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
await stat(target);
|
|
304
|
+
return useExisting();
|
|
265
305
|
} catch (error) {
|
|
266
306
|
if (error instanceof DdcStoreError) throw error;
|
|
267
307
|
if (errorCode(error) !== "ENOENT") throw error;
|
|
@@ -271,11 +311,12 @@ var DdcEntryStore = class {
|
|
|
271
311
|
return { result: "published", key: staged.key };
|
|
272
312
|
} catch (error) {
|
|
273
313
|
if (!["EEXIST", "ENOTEMPTY", "EISDIR"].includes(errorCode(error) ?? "")) throw error;
|
|
274
|
-
|
|
275
|
-
await rm(staged.path, { recursive: true, force: true });
|
|
276
|
-
return existingResult(existing, candidate, staged.key);
|
|
314
|
+
return useExisting();
|
|
277
315
|
}
|
|
278
316
|
}
|
|
317
|
+
async discard(staged) {
|
|
318
|
+
await rm(staged.path, { recursive: true, force: true });
|
|
319
|
+
}
|
|
279
320
|
async write(entry) {
|
|
280
321
|
const staged = await this.stage(entry);
|
|
281
322
|
return this.publish(staged);
|
|
@@ -513,9 +554,9 @@ var DdcLifecycle = class {
|
|
|
513
554
|
const recordedFailure = record.failure?.desiredKey === desiredKey ? { code: record.failure.code, detail: record.failure.detail } : void 0;
|
|
514
555
|
const currentEntry = record.currentKey === void 0 ? null : await this.entries.readChecked(record.currentKey);
|
|
515
556
|
const entryFailure = currentEntry !== null && !currentEntry.ok ? { code: currentEntry.error.code, detail: currentEntry.error.detail } : void 0;
|
|
516
|
-
const
|
|
557
|
+
const failure2 = recordedFailure ?? entryFailure;
|
|
517
558
|
const currentValue = currentEntry?.ok === true ? currentEntry.value : null;
|
|
518
|
-
const state =
|
|
559
|
+
const state = failure2 !== void 0 ? "failed" : record.currentKey === desiredKey && currentValue?.guid === guid ? "current" : record.stale === true ? "stale" : record.active?.desiredKey === desiredKey ? "cooking" : "stale";
|
|
519
560
|
return {
|
|
520
561
|
guid,
|
|
521
562
|
desiredKey,
|
|
@@ -525,7 +566,7 @@ var DdcLifecycle = class {
|
|
|
525
566
|
revision: record.revision,
|
|
526
567
|
...record.generation === void 0 ? {} : { generation: record.generation },
|
|
527
568
|
...record.active === void 0 ? {} : { activeLease: record.active },
|
|
528
|
-
...
|
|
569
|
+
...failure2 === void 0 ? {} : { failure: failure2 }
|
|
529
570
|
};
|
|
530
571
|
}
|
|
531
572
|
async begin(guid, desiredKey) {
|
|
@@ -617,7 +658,7 @@ var DdcLifecycle = class {
|
|
|
617
658
|
return withRevision({ result: "current", key: validatedKey }, nextRevision);
|
|
618
659
|
});
|
|
619
660
|
}
|
|
620
|
-
async fail(lease,
|
|
661
|
+
async fail(lease, failure2) {
|
|
621
662
|
await withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
622
663
|
const current = await this.read(lease.guid);
|
|
623
664
|
if (current?.active?.attempt !== lease.attempt) return;
|
|
@@ -628,7 +669,7 @@ var DdcLifecycle = class {
|
|
|
628
669
|
generation: lease.generation,
|
|
629
670
|
...current.currentKey === void 0 ? {} : { currentKey: current.currentKey },
|
|
630
671
|
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
631
|
-
failure: { desiredKey: lease.desiredKey, ...
|
|
672
|
+
failure: { desiredKey: lease.desiredKey, ...failure2 }
|
|
632
673
|
});
|
|
633
674
|
});
|
|
634
675
|
}
|
|
@@ -663,6 +704,40 @@ var DdcLifecycle = class {
|
|
|
663
704
|
});
|
|
664
705
|
});
|
|
665
706
|
}
|
|
707
|
+
async discard(lease) {
|
|
708
|
+
await withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
709
|
+
const current = await this.read(lease.guid);
|
|
710
|
+
if (current?.active?.attempt !== lease.attempt) return;
|
|
711
|
+
const { active: _active, ...withoutActive } = current;
|
|
712
|
+
await this.write({
|
|
713
|
+
...withoutActive,
|
|
714
|
+
revision: current.revision + 1,
|
|
715
|
+
generation: lease.generation,
|
|
716
|
+
stale: current.currentKey === void 0
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
/** Restore the last accepted head after a failed multi-owner generation commit. */
|
|
721
|
+
async restore(head) {
|
|
722
|
+
await withDdcLock(this.root, `head-${head.guid}`, async () => {
|
|
723
|
+
const path = headFile(this.heads, head.guid);
|
|
724
|
+
if (head.state === "missing") {
|
|
725
|
+
await rm(path, { force: true });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
await this.write({
|
|
729
|
+
guid: head.guid,
|
|
730
|
+
desiredKey: head.desiredKey,
|
|
731
|
+
revision: head.revision ?? 0,
|
|
732
|
+
...head.currentKey === void 0 ? {} : { currentKey: head.currentKey },
|
|
733
|
+
...head.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: head.lastKnownGoodKey },
|
|
734
|
+
...head.generation === void 0 ? {} : { generation: head.generation },
|
|
735
|
+
...head.activeLease === void 0 ? {} : { active: head.activeLease },
|
|
736
|
+
...head.state === "stale" ? { stale: true } : {},
|
|
737
|
+
...head.failure === void 0 ? {} : { failure: { desiredKey: head.desiredKey, ...head.failure } }
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
666
741
|
async revoke(lease) {
|
|
667
742
|
await this.fail(lease, {
|
|
668
743
|
code: "lease-lost",
|
|
@@ -814,6 +889,299 @@ function resolveDdcLayout(options) {
|
|
|
814
889
|
function isDdcLayout(value) {
|
|
815
890
|
return value !== null && typeof value === "object" && value.version === DDC_LAYOUT_VERSION;
|
|
816
891
|
}
|
|
892
|
+
function stable(value) {
|
|
893
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
|
|
894
|
+
if (value !== null && typeof value === "object") {
|
|
895
|
+
const record = value;
|
|
896
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stable(record[key])}`).join(",")}}`;
|
|
897
|
+
}
|
|
898
|
+
return JSON.stringify(value) ?? "null";
|
|
899
|
+
}
|
|
900
|
+
function digest2(value) {
|
|
901
|
+
return `sha256:${createHash("sha256").update(stable(value)).digest("hex")}`;
|
|
902
|
+
}
|
|
903
|
+
function scriptablePackOutputSetDigest(outputs) {
|
|
904
|
+
return digest2(
|
|
905
|
+
outputs.map((output) => ({
|
|
906
|
+
guid: output.guid.toLowerCase(),
|
|
907
|
+
sourceKey: output.sourceKey,
|
|
908
|
+
kind: output.kind,
|
|
909
|
+
digest: output.digest,
|
|
910
|
+
refs: [...output.refs].map((guid) => guid.toLowerCase())
|
|
911
|
+
}))
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
function scriptablePackPublicationGeneration(input) {
|
|
915
|
+
const hash = createHash("sha256").update(input.sourceRevision).update("\n").update(input.digest).update("\n").update(input.outputSetDigest).digest("hex");
|
|
916
|
+
const generation = Number.parseInt(hash.slice(0, 8), 16);
|
|
917
|
+
return generation > 0 ? generation : 1;
|
|
918
|
+
}
|
|
919
|
+
function createAcceptedPublication(input) {
|
|
920
|
+
const outputSetDigest = scriptablePackOutputSetDigest(input.outputs);
|
|
921
|
+
const publicationGeneration = input.generation ?? scriptablePackPublicationGeneration({
|
|
922
|
+
sourceRevision: input.sourceRevision,
|
|
923
|
+
digest: input.digest,
|
|
924
|
+
outputSetDigest
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
schemaVersion: "asset-publication/1",
|
|
928
|
+
sourcePath: input.sourcePath,
|
|
929
|
+
sourceRevision: input.sourceRevision,
|
|
930
|
+
generation: publicationGeneration,
|
|
931
|
+
digest: input.digest,
|
|
932
|
+
outputSetDigest,
|
|
933
|
+
outputs: input.outputs,
|
|
934
|
+
receipt: {
|
|
935
|
+
schemaVersion: "asset-publication-receipt/1",
|
|
936
|
+
sourcePath: input.sourcePath,
|
|
937
|
+
sourceRevision: input.sourceRevision,
|
|
938
|
+
inputFingerprint: input.inputFingerprint,
|
|
939
|
+
outputDigest: input.digest,
|
|
940
|
+
outputSetDigest,
|
|
941
|
+
externalEvidence: input.externalEvidence
|
|
942
|
+
},
|
|
943
|
+
externalEvidence: input.externalEvidence,
|
|
944
|
+
current: {
|
|
945
|
+
generation: publicationGeneration,
|
|
946
|
+
digest: input.digest,
|
|
947
|
+
outputSetDigest,
|
|
948
|
+
packageUrl: input.packageUrl,
|
|
949
|
+
receiptKey: input.inputFingerprint
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function locatorFor(envelope) {
|
|
954
|
+
return {
|
|
955
|
+
generation: envelope.generation,
|
|
956
|
+
digest: envelope.digest,
|
|
957
|
+
outputSetDigest: envelope.outputSetDigest,
|
|
958
|
+
packageUrl: envelope.current?.packageUrl ?? "",
|
|
959
|
+
receiptKey: envelope.receipt.inputFingerprint
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function recoveryFor(retryable, useLastKnownGood) {
|
|
963
|
+
return {
|
|
964
|
+
retryable,
|
|
965
|
+
preserveCurrent: true,
|
|
966
|
+
useLastKnownGood,
|
|
967
|
+
actions: retryable ? ["inspect-publication-failure", "retry-rebuild", "continue-last-known-good"] : ["inspect-publication-failure", "edit-source", "continue-last-known-good"]
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function failure(envelope, code, stage, reason, retryable, current, lastKnownGood) {
|
|
971
|
+
return {
|
|
972
|
+
code,
|
|
973
|
+
stage,
|
|
974
|
+
sourcePath: envelope.sourcePath,
|
|
975
|
+
sourceRevision: envelope.sourceRevision,
|
|
976
|
+
generation: envelope.generation,
|
|
977
|
+
reason,
|
|
978
|
+
recovery: recoveryFor(retryable, lastKnownGood !== void 0),
|
|
979
|
+
...current === void 0 ? {} : { current: locatorFor(current) },
|
|
980
|
+
...lastKnownGood === void 0 ? {} : { lastKnownGood: locatorFor(lastKnownGood) }
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function validateOutputs(envelope, current, lastKnownGood) {
|
|
984
|
+
const guids = /* @__PURE__ */ new Set();
|
|
985
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
986
|
+
for (const output of envelope.outputs) {
|
|
987
|
+
const guid = output.guid.toLowerCase();
|
|
988
|
+
if (guids.has(guid) || sourceKeys.has(output.sourceKey)) {
|
|
989
|
+
return failure(
|
|
990
|
+
envelope,
|
|
991
|
+
"asset-publication-output-duplicate",
|
|
992
|
+
"output",
|
|
993
|
+
"publication output GUIDs and sourceKeys must be unique",
|
|
994
|
+
false,
|
|
995
|
+
current,
|
|
996
|
+
lastKnownGood
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
guids.add(guid);
|
|
1000
|
+
sourceKeys.add(output.sourceKey);
|
|
1001
|
+
}
|
|
1002
|
+
if (envelope.outputs.length === 0 || scriptablePackOutputSetDigest(envelope.outputs) !== envelope.outputSetDigest) {
|
|
1003
|
+
return failure(
|
|
1004
|
+
envelope,
|
|
1005
|
+
"asset-publication-output-set-mismatch",
|
|
1006
|
+
"receipt",
|
|
1007
|
+
"receipt outputSetDigest does not match the complete output tuple",
|
|
1008
|
+
false,
|
|
1009
|
+
current,
|
|
1010
|
+
lastKnownGood
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
return void 0;
|
|
1014
|
+
}
|
|
1015
|
+
function validateReceipt(envelope, current, lastKnownGood) {
|
|
1016
|
+
const receipt = envelope.receipt;
|
|
1017
|
+
if (receipt.schemaVersion !== "asset-publication-receipt/1" || receipt.sourcePath !== envelope.sourcePath || receipt.sourceRevision !== envelope.sourceRevision || receipt.outputSetDigest !== envelope.outputSetDigest || receipt.outputDigest !== envelope.digest || stable(receipt.externalEvidence) !== stable(envelope.externalEvidence)) {
|
|
1018
|
+
return failure(
|
|
1019
|
+
envelope,
|
|
1020
|
+
"asset-publication-receipt-mismatch",
|
|
1021
|
+
"receipt",
|
|
1022
|
+
"publication receipt does not match the candidate output tuple",
|
|
1023
|
+
false,
|
|
1024
|
+
current,
|
|
1025
|
+
lastKnownGood
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
1030
|
+
function validateEnvelope(envelope, current, lastKnownGood) {
|
|
1031
|
+
if (envelope.schemaVersion !== "asset-publication/1") {
|
|
1032
|
+
return failure(
|
|
1033
|
+
envelope,
|
|
1034
|
+
"asset-publication-schema-invalid",
|
|
1035
|
+
"receipt",
|
|
1036
|
+
"publication envelope schemaVersion is not supported",
|
|
1037
|
+
false,
|
|
1038
|
+
current,
|
|
1039
|
+
lastKnownGood
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (!Number.isSafeInteger(envelope.generation) || envelope.generation < 1) {
|
|
1043
|
+
return failure(
|
|
1044
|
+
envelope,
|
|
1045
|
+
"asset-publication-generation-invalid",
|
|
1046
|
+
"receipt",
|
|
1047
|
+
"publication generation must be a positive integer",
|
|
1048
|
+
false,
|
|
1049
|
+
current,
|
|
1050
|
+
lastKnownGood
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const outputError = validateOutputs(envelope, current, lastKnownGood);
|
|
1054
|
+
if (outputError !== void 0) return outputError;
|
|
1055
|
+
const receiptError = validateReceipt(envelope, current, lastKnownGood);
|
|
1056
|
+
if (receiptError !== void 0) return receiptError;
|
|
1057
|
+
const sameTuple = current !== void 0 && envelope.generation === current.generation && envelope.sourcePath === current.sourcePath && envelope.sourceRevision === current.sourceRevision && envelope.digest === current.digest && envelope.outputSetDigest === current.outputSetDigest;
|
|
1058
|
+
if (sameTuple) return void 0;
|
|
1059
|
+
if (current !== void 0 && envelope.generation <= current.generation) {
|
|
1060
|
+
return failure(
|
|
1061
|
+
envelope,
|
|
1062
|
+
"asset-publication-stale",
|
|
1063
|
+
"cancelled",
|
|
1064
|
+
`candidate generation ${envelope.generation} is not newer than current ${current.generation}`,
|
|
1065
|
+
true,
|
|
1066
|
+
current,
|
|
1067
|
+
lastKnownGood
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return void 0;
|
|
1071
|
+
}
|
|
1072
|
+
function validateCandidate(state, candidate, cancelledReason) {
|
|
1073
|
+
const current = state.snapshot.current;
|
|
1074
|
+
const lastKnownGood = state.snapshot.lastKnownGood ?? current;
|
|
1075
|
+
if (candidate.cancelled === true) {
|
|
1076
|
+
return failure(
|
|
1077
|
+
candidate.envelope,
|
|
1078
|
+
"asset-publication-cancelled",
|
|
1079
|
+
"cancelled",
|
|
1080
|
+
cancelledReason,
|
|
1081
|
+
true,
|
|
1082
|
+
current,
|
|
1083
|
+
lastKnownGood
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return validateEnvelope(candidate.envelope, current, lastKnownGood);
|
|
1087
|
+
}
|
|
1088
|
+
async function publishCandidate(state, candidate, commitRoute) {
|
|
1089
|
+
const current = state.snapshot.current;
|
|
1090
|
+
const lastKnownGood = state.snapshot.lastKnownGood ?? current;
|
|
1091
|
+
const invalid = validateCandidate(
|
|
1092
|
+
state,
|
|
1093
|
+
candidate,
|
|
1094
|
+
"publication request was cancelled before route commit"
|
|
1095
|
+
);
|
|
1096
|
+
if (invalid !== void 0) {
|
|
1097
|
+
state.snapshot = { ...state.snapshot, failure: invalid };
|
|
1098
|
+
return err(invalid);
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
await commitRoute();
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
const failed = failure(
|
|
1104
|
+
candidate.envelope,
|
|
1105
|
+
"asset-publication-route-failed",
|
|
1106
|
+
"route",
|
|
1107
|
+
error instanceof Error ? error.message : String(error),
|
|
1108
|
+
true,
|
|
1109
|
+
current,
|
|
1110
|
+
lastKnownGood
|
|
1111
|
+
);
|
|
1112
|
+
state.snapshot = { ...state.snapshot, failure: failed };
|
|
1113
|
+
return err(failed);
|
|
1114
|
+
}
|
|
1115
|
+
const published = {
|
|
1116
|
+
...candidate.envelope,
|
|
1117
|
+
current: locatorFor(candidate.envelope),
|
|
1118
|
+
...current === void 0 ? {} : { lastKnownGood: locatorFor(current) },
|
|
1119
|
+
recovery: recoveryFor(false, current !== void 0)
|
|
1120
|
+
};
|
|
1121
|
+
state.snapshot = {
|
|
1122
|
+
current: published,
|
|
1123
|
+
...current === void 0 ? {} : { lastKnownGood: current }
|
|
1124
|
+
};
|
|
1125
|
+
return ok(state.snapshot);
|
|
1126
|
+
}
|
|
1127
|
+
function createAcceptedPublicationStore() {
|
|
1128
|
+
const states = /* @__PURE__ */ new Map();
|
|
1129
|
+
function stateFor(sourcePath) {
|
|
1130
|
+
const existing = states.get(sourcePath);
|
|
1131
|
+
if (existing !== void 0) return existing;
|
|
1132
|
+
const created = { snapshot: {}, staged: /* @__PURE__ */ new Map() };
|
|
1133
|
+
states.set(sourcePath, created);
|
|
1134
|
+
return created;
|
|
1135
|
+
}
|
|
1136
|
+
function candidateKey(candidate) {
|
|
1137
|
+
return `${candidate.generation}\0${candidate.digest}\0${candidate.outputSetDigest}`;
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
observe(sourcePath) {
|
|
1141
|
+
return stateFor(sourcePath).snapshot;
|
|
1142
|
+
},
|
|
1143
|
+
stage(sourcePath, candidate) {
|
|
1144
|
+
const invalid = validateCandidate(
|
|
1145
|
+
stateFor(sourcePath),
|
|
1146
|
+
candidate,
|
|
1147
|
+
"publication request was cancelled before DDC commit"
|
|
1148
|
+
);
|
|
1149
|
+
if (invalid !== void 0) return err(invalid);
|
|
1150
|
+
stateFor(sourcePath).staged.set(candidateKey(candidate.envelope), candidate.envelope);
|
|
1151
|
+
return ok(void 0);
|
|
1152
|
+
},
|
|
1153
|
+
async commit(sourcePath, candidate, commitRoute) {
|
|
1154
|
+
const key = candidateKey(candidate.envelope);
|
|
1155
|
+
const state = stateFor(sourcePath);
|
|
1156
|
+
if (!state.staged.has(key)) {
|
|
1157
|
+
const current = state.snapshot.current;
|
|
1158
|
+
const lastKnownGood = state.snapshot.lastKnownGood ?? current;
|
|
1159
|
+
return err(
|
|
1160
|
+
failure(
|
|
1161
|
+
candidate.envelope,
|
|
1162
|
+
"asset-publication-cancelled",
|
|
1163
|
+
"cancelled",
|
|
1164
|
+
"publication candidate was discarded before DDC commit",
|
|
1165
|
+
true,
|
|
1166
|
+
current,
|
|
1167
|
+
lastKnownGood
|
|
1168
|
+
)
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const result = await publishCandidate(state, candidate, commitRoute);
|
|
1172
|
+
state.staged.delete(key);
|
|
1173
|
+
return result;
|
|
1174
|
+
},
|
|
1175
|
+
discard(sourcePath, candidate) {
|
|
1176
|
+
stateFor(sourcePath).staged.delete(candidateKey(candidate));
|
|
1177
|
+
},
|
|
1178
|
+
restore(sourcePath, snapshot) {
|
|
1179
|
+
const state = stateFor(sourcePath);
|
|
1180
|
+
state.staged.clear();
|
|
1181
|
+
state.snapshot = snapshot;
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
817
1185
|
function scopeHash(scopeId) {
|
|
818
1186
|
return createHash("sha256").update(scopeId, "utf8").digest("hex");
|
|
819
1187
|
}
|
|
@@ -874,6 +1242,196 @@ async function assertRuntimeScope(root, scope) {
|
|
|
874
1242
|
return createRuntimeScope(root, scope.scopeId);
|
|
875
1243
|
}
|
|
876
1244
|
|
|
1245
|
+
// src/session.ts
|
|
1246
|
+
var DdcGenerationSession = class {
|
|
1247
|
+
generation;
|
|
1248
|
+
lifecycle;
|
|
1249
|
+
entries;
|
|
1250
|
+
candidates = /* @__PURE__ */ new Map();
|
|
1251
|
+
heartbeatTimers = /* @__PURE__ */ new Map();
|
|
1252
|
+
counters = {
|
|
1253
|
+
hitCount: 0,
|
|
1254
|
+
missCount: 0,
|
|
1255
|
+
corruptCount: 0,
|
|
1256
|
+
writeFailureCount: 0
|
|
1257
|
+
};
|
|
1258
|
+
accepting = true;
|
|
1259
|
+
constructor(root, options) {
|
|
1260
|
+
if (!Number.isSafeInteger(options.generation) || options.generation < 1) {
|
|
1261
|
+
throw new TypeError("DDC generation must be a positive safe integer");
|
|
1262
|
+
}
|
|
1263
|
+
this.generation = options.generation;
|
|
1264
|
+
this.lifecycle = new DdcLifecycle(
|
|
1265
|
+
root,
|
|
1266
|
+
options.leaseTtlMs === void 0 ? void 0 : { leaseTtlMs: options.leaseTtlMs }
|
|
1267
|
+
);
|
|
1268
|
+
this.entries = new DdcEntryStore(root);
|
|
1269
|
+
}
|
|
1270
|
+
async beginCandidate(guid, desiredKey) {
|
|
1271
|
+
const head = await this.inspect(guid, desiredKey);
|
|
1272
|
+
const lease = await this.lifecycle.begin(guid, desiredKey);
|
|
1273
|
+
const candidate = {
|
|
1274
|
+
generation: this.generation,
|
|
1275
|
+
lease,
|
|
1276
|
+
previousHead: head
|
|
1277
|
+
};
|
|
1278
|
+
this.candidates.set(lease.attempt, candidate);
|
|
1279
|
+
this.scheduleHeartbeat(candidate);
|
|
1280
|
+
return candidate;
|
|
1281
|
+
}
|
|
1282
|
+
async stageEntry(entry) {
|
|
1283
|
+
this.assertOpen();
|
|
1284
|
+
const candidate = await this.beginCandidate(
|
|
1285
|
+
entry.guid,
|
|
1286
|
+
entry.key
|
|
1287
|
+
);
|
|
1288
|
+
try {
|
|
1289
|
+
const staged = await this.entries.stage(entry);
|
|
1290
|
+
const entryCandidate = Object.assign(candidate, {
|
|
1291
|
+
staged
|
|
1292
|
+
});
|
|
1293
|
+
this.candidates.set(candidate.lease.attempt, entryCandidate);
|
|
1294
|
+
return entryCandidate;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
await this.lifecycle.fail(candidate.lease, {
|
|
1297
|
+
code: "ddc-entry-stage-failed",
|
|
1298
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1299
|
+
});
|
|
1300
|
+
this.stopHeartbeat(candidate);
|
|
1301
|
+
this.candidates.delete(candidate.lease.attempt);
|
|
1302
|
+
this.counters.writeFailureCount += 1;
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async inspect(guid, desiredKey) {
|
|
1307
|
+
this.assertOpen();
|
|
1308
|
+
const head = await this.lifecycle.inspect(guid, desiredKey);
|
|
1309
|
+
if (head.state === "current") this.counters.hitCount += 1;
|
|
1310
|
+
else this.counters.missCount += 1;
|
|
1311
|
+
return head;
|
|
1312
|
+
}
|
|
1313
|
+
async commitCandidate(candidate, validatedKey) {
|
|
1314
|
+
const registered = this.assertCandidate(candidate);
|
|
1315
|
+
try {
|
|
1316
|
+
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
1317
|
+
if (result.result === "invalid") this.counters.corruptCount += 1;
|
|
1318
|
+
this.stopHeartbeat(registered);
|
|
1319
|
+
this.candidates.delete(registered.lease.attempt);
|
|
1320
|
+
return result;
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
this.stopHeartbeat(registered);
|
|
1323
|
+
this.counters.writeFailureCount += 1;
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async commitEntry(candidate, validatedKey) {
|
|
1328
|
+
const registered = this.assertCandidate(candidate);
|
|
1329
|
+
try {
|
|
1330
|
+
await this.entries.publish(candidate.staged);
|
|
1331
|
+
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
1332
|
+
this.stopHeartbeat(registered);
|
|
1333
|
+
this.candidates.delete(registered.lease.attempt);
|
|
1334
|
+
return result;
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.stopHeartbeat(registered);
|
|
1337
|
+
await this.lifecycle.fail(registered.lease, {
|
|
1338
|
+
code: "ddc-entry-publish-failed",
|
|
1339
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1340
|
+
});
|
|
1341
|
+
await this.entries.discard(candidate.staged).catch(() => {
|
|
1342
|
+
});
|
|
1343
|
+
this.candidates.delete(registered.lease.attempt);
|
|
1344
|
+
this.counters.writeFailureCount += 1;
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
async discardCandidate(candidate) {
|
|
1349
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
1350
|
+
if (registered === void 0) return;
|
|
1351
|
+
try {
|
|
1352
|
+
this.stopHeartbeat(registered);
|
|
1353
|
+
await this.lifecycle.discard(registered.lease);
|
|
1354
|
+
this.candidates.delete(registered.lease.attempt);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
this.counters.writeFailureCount += 1;
|
|
1357
|
+
throw error;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
async discardEntry(candidate) {
|
|
1361
|
+
await Promise.all([this.discardCandidate(candidate), this.entries.discard(candidate.staged)]);
|
|
1362
|
+
}
|
|
1363
|
+
async restoreEntry(candidate) {
|
|
1364
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
1365
|
+
let restoreError;
|
|
1366
|
+
try {
|
|
1367
|
+
await this.lifecycle.restore(candidate.previousHead);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
restoreError = error;
|
|
1370
|
+
} finally {
|
|
1371
|
+
await this.entries.discard(candidate.staged).catch(() => {
|
|
1372
|
+
});
|
|
1373
|
+
if (registered !== void 0) {
|
|
1374
|
+
this.stopHeartbeat(registered);
|
|
1375
|
+
this.candidates.delete(registered.lease.attempt);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (restoreError !== void 0) throw restoreError;
|
|
1379
|
+
}
|
|
1380
|
+
metrics() {
|
|
1381
|
+
return { ...this.counters };
|
|
1382
|
+
}
|
|
1383
|
+
async close() {
|
|
1384
|
+
if (!this.accepting) return;
|
|
1385
|
+
this.accepting = false;
|
|
1386
|
+
for (const timer of this.heartbeatTimers.values()) clearTimeout(timer);
|
|
1387
|
+
this.heartbeatTimers.clear();
|
|
1388
|
+
const pending = [...this.candidates.values()];
|
|
1389
|
+
for (const candidate of pending) {
|
|
1390
|
+
try {
|
|
1391
|
+
await this.lifecycle.discard(candidate.lease);
|
|
1392
|
+
} catch {
|
|
1393
|
+
this.counters.writeFailureCount += 1;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
this.candidates.clear();
|
|
1397
|
+
}
|
|
1398
|
+
assertOpen() {
|
|
1399
|
+
if (!this.accepting) throw new Error("DDC generation session is closed");
|
|
1400
|
+
}
|
|
1401
|
+
assertCandidate(candidate) {
|
|
1402
|
+
this.assertOpen();
|
|
1403
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
1404
|
+
if (candidate.generation !== this.generation || registered === void 0) {
|
|
1405
|
+
throw new Error("DDC candidate belongs to another generation session");
|
|
1406
|
+
}
|
|
1407
|
+
return registered;
|
|
1408
|
+
}
|
|
1409
|
+
scheduleHeartbeat(candidate) {
|
|
1410
|
+
const attempt = candidate.lease.attempt;
|
|
1411
|
+
this.stopHeartbeat(candidate);
|
|
1412
|
+
const delay2 = Math.max(1, Math.floor((candidate.lease.expiresAt - Date.now()) / 2));
|
|
1413
|
+
const timer = setTimeout(() => {
|
|
1414
|
+
this.heartbeatTimers.delete(attempt);
|
|
1415
|
+
if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
|
|
1416
|
+
void this.lifecycle.heartbeat(candidate.lease).then((lease) => {
|
|
1417
|
+
if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
|
|
1418
|
+
candidate.lease = lease;
|
|
1419
|
+
this.scheduleHeartbeat(candidate);
|
|
1420
|
+
}).catch(() => {
|
|
1421
|
+
this.stopHeartbeat(candidate);
|
|
1422
|
+
});
|
|
1423
|
+
}, delay2);
|
|
1424
|
+
timer.unref?.();
|
|
1425
|
+
this.heartbeatTimers.set(attempt, timer);
|
|
1426
|
+
}
|
|
1427
|
+
stopHeartbeat(candidate) {
|
|
1428
|
+
const timer = this.heartbeatTimers.get(candidate.lease.attempt);
|
|
1429
|
+
if (timer === void 0) return;
|
|
1430
|
+
clearTimeout(timer);
|
|
1431
|
+
this.heartbeatTimers.delete(candidate.lease.attempt);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
877
1435
|
// src/status.ts
|
|
878
1436
|
var DDC_STATUS_SCHEMA = "forgeax-ddc-status/v2";
|
|
879
1437
|
function serializeDdcStatus(status) {
|
|
@@ -891,6 +1449,6 @@ function projectDdcStatusForBrowser(status) {
|
|
|
891
1449
|
}
|
|
892
1450
|
var toBrowserDdcStatus = projectDdcStatusForBrowser;
|
|
893
1451
|
|
|
894
|
-
export { DDC_ERROR_CODES, DDC_LAYOUT_VERSION, DDC_STATUS_SCHEMA, DdcEntryStore, DdcLifecycle, DdcStoreError, assertRuntimeScope, collectDdcGarbage, createRuntimeScope, ddcOutputDigest, isDdcLayout, projectDdcStatusForBrowser, resolveBuildDdcLayout, resolveDdcLayout, runtimeScopeHash, semanticDdcKey, serializeDdcStatus, toBrowserDdcStatus };
|
|
1452
|
+
export { DDC_ERROR_CODES, DDC_LAYOUT_VERSION, DDC_STATUS_SCHEMA, DdcEntryStore, DdcGenerationSession, DdcLifecycle, DdcStoreError, assertRuntimeScope, collectDdcGarbage, createAcceptedPublication, createAcceptedPublicationStore, createRuntimeScope, ddcOutputDigest, isDdcLayout, projectDdcStatusForBrowser, resolveBuildDdcLayout, resolveDdcLayout, resolveDdcRoot, runtimeScopeHash, scriptablePackOutputSetDigest, scriptablePackPublicationGeneration, semanticBuildKey, semanticDdcKey, serializeDdcStatus, toBrowserDdcStatus };
|
|
895
1453
|
//# sourceMappingURL=index.mjs.map
|
|
896
1454
|
//# sourceMappingURL=index.mjs.map
|