@forgeax/engine-ddc 0.1.2 → 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/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, stat, rm, rename, readdir, readFile } from 'fs/promises';
3
- import { join, isAbsolute, normalize } from 'path';
4
+ import { mkdir, writeFile, rm, stat, rename, readdir, readFile } from 'fs/promises';
5
+ import { err, ok } from '@forgeax/engine-types';
4
6
 
5
- // src/entry-store.ts
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
- await mkdir(join(path, "artifacts"), { recursive: true });
242
- await writeFile(join(path, "payload.json"), canonicalDdcJson(entry.payload));
243
- await writeFile(join(path, "refs.json"), canonicalDdcJson(entry.refs));
244
- await writeFile(join(path, "receipt.json"), canonicalDdcJson(entry.receipt));
245
- const artifacts = {};
246
- for (const [key, artifact] of sortedArtifacts(entry)) {
247
- const file = artifactFile(key);
248
- artifacts[key] = { mediaType: artifact.mediaType, file };
249
- await writeFile(join(path, "artifacts", `${file}.bin`), artifact.bytes);
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
- try {
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
- const existing = await this.readDirectory(target, false);
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 failure = recordedFailure ?? entryFailure;
557
+ const failure2 = recordedFailure ?? entryFailure;
517
558
  const currentValue = currentEntry?.ok === true ? currentEntry.value : null;
518
- const state = failure !== void 0 ? "failed" : record.currentKey === desiredKey && currentValue?.guid === guid ? "current" : record.stale === true ? "stale" : record.active?.desiredKey === desiredKey ? "cooking" : "stale";
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
- ...failure === void 0 ? {} : { failure }
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, failure) {
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, ...failure }
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