@forgeax/engine-ddc 0.0.0-dev.8d955ade1c79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +147 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/concurrency.integration.test.d.ts +2 -0
  5. package/dist/__tests__/concurrency.integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/consumer-path.integration.test.d.ts +2 -0
  7. package/dist/__tests__/consumer-path.integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/crash-recovery.integration.test.d.ts +2 -0
  9. package/dist/__tests__/crash-recovery.integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/entry-store.integration.test.d.ts +2 -0
  11. package/dist/__tests__/entry-store.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  13. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/generation-session.integration.test.d.ts +2 -0
  15. package/dist/__tests__/generation-session.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/generation-session.unit.test.d.ts +2 -0
  17. package/dist/__tests__/generation-session.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/key.unit.test.d.ts +2 -0
  19. package/dist/__tests__/key.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/layout.unit.test.d.ts +2 -0
  21. package/dist/__tests__/layout.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/lifecycle.unit.test.d.ts +2 -0
  23. package/dist/__tests__/lifecycle.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/multiprocess-gc.integration.test.d.ts +2 -0
  25. package/dist/__tests__/multiprocess-gc.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/multiprocess-lifecycle.integration.test.d.ts +2 -0
  27. package/dist/__tests__/multiprocess-lifecycle.integration.test.d.ts.map +1 -0
  28. package/dist/__tests__/multiprocess-worker.d.ts +2 -0
  29. package/dist/__tests__/multiprocess-worker.d.ts.map +1 -0
  30. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  31. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/status-root-kind-owner.test-d.d.ts +2 -0
  33. package/dist/__tests__/status-root-kind-owner.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/status.unit.test.d.ts +2 -0
  35. package/dist/__tests__/status.unit.test.d.ts.map +1 -0
  36. package/dist/build-cache.d.ts +18 -0
  37. package/dist/build-cache.d.ts.map +1 -0
  38. package/dist/entry-store.d.ts +53 -0
  39. package/dist/entry-store.d.ts.map +1 -0
  40. package/dist/entry-store.mjs +349 -0
  41. package/dist/entry-store.mjs.map +1 -0
  42. package/dist/errors.d.ts +58 -0
  43. package/dist/errors.d.ts.map +1 -0
  44. package/dist/errors.mjs +109 -0
  45. package/dist/errors.mjs.map +1 -0
  46. package/dist/gc.d.ts +13 -0
  47. package/dist/gc.d.ts.map +1 -0
  48. package/dist/index.d.ts +12 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.mjs +1574 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/dist/key.d.ts +13 -0
  53. package/dist/key.d.ts.map +1 -0
  54. package/dist/key.mjs +38 -0
  55. package/dist/key.mjs.map +1 -0
  56. package/dist/layout.d.ts +51 -0
  57. package/dist/layout.d.ts.map +1 -0
  58. package/dist/layout.mjs +68 -0
  59. package/dist/layout.mjs.map +1 -0
  60. package/dist/lifecycle.d.ts +93 -0
  61. package/dist/lifecycle.d.ts.map +1 -0
  62. package/dist/publication.d.ts +50 -0
  63. package/dist/publication.d.ts.map +1 -0
  64. package/dist/runtime-scope.d.ts +10 -0
  65. package/dist/runtime-scope.d.ts.map +1 -0
  66. package/dist/session.d.ts +46 -0
  67. package/dist/session.d.ts.map +1 -0
  68. package/dist/status.d.ts +39 -0
  69. package/dist/status.d.ts.map +1 -0
  70. package/dist/status.mjs +20 -0
  71. package/dist/status.mjs.map +1 -0
  72. package/package.json +101 -0
  73. package/src/__tests__/concurrency.integration.test.ts +53 -0
  74. package/src/__tests__/consumer-path.integration.test.ts +61 -0
  75. package/src/__tests__/crash-recovery.integration.test.ts +84 -0
  76. package/src/__tests__/entry-store.integration.test.ts +83 -0
  77. package/src/__tests__/errors.unit.test.ts +54 -0
  78. package/src/__tests__/generation-session.integration.test.ts +202 -0
  79. package/src/__tests__/generation-session.unit.test.ts +35 -0
  80. package/src/__tests__/key.unit.test.ts +49 -0
  81. package/src/__tests__/layout.unit.test.ts +50 -0
  82. package/src/__tests__/lifecycle.unit.test.ts +258 -0
  83. package/src/__tests__/multiprocess-gc.integration.test.ts +36 -0
  84. package/src/__tests__/multiprocess-lifecycle.integration.test.ts +129 -0
  85. package/src/__tests__/multiprocess-worker.ts +41 -0
  86. package/src/__tests__/owner-chain.integration.test.ts +45 -0
  87. package/src/__tests__/status-root-kind-owner.test-d.ts +42 -0
  88. package/src/__tests__/status.unit.test.ts +56 -0
  89. package/src/build-cache.ts +60 -0
  90. package/src/entry-store.ts +317 -0
  91. package/src/errors.ts +164 -0
  92. package/src/gc.ts +34 -0
  93. package/src/index.ts +92 -0
  94. package/src/key.ts +46 -0
  95. package/src/layout.ts +132 -0
  96. package/src/lifecycle.ts +792 -0
  97. package/src/publication.ts +446 -0
  98. package/src/runtime-scope.ts +87 -0
  99. package/src/session.ts +281 -0
  100. package/src/status.ts +58 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1574 @@
1
+ import { existsSync } from 'fs';
2
+ import { resolve, join, dirname, isAbsolute, normalize } from 'path';
3
+ import { createHash, randomUUID } from 'crypto';
4
+ import { mkdir, writeFile, rm, stat, rename, readdir, readFile } from 'fs/promises';
5
+ import { err, ok } from '@forgeax/engine-types';
6
+
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
+ }
68
+
69
+ // src/errors.ts
70
+ var DDC_ERROR_CODES = [
71
+ "ddc-project-root-required",
72
+ "ddc-object-conflict",
73
+ "ddc-head-conflict",
74
+ "ddc-generation-conflict",
75
+ "ddc-scope-mismatch",
76
+ "ddc-lease-expired"
77
+ ];
78
+ var defaultHints = {
79
+ "ddc-project-root-required": "inject the canonical projectDdcRoot before serving or publishing",
80
+ "ddc-object-conflict": "keep the verified object and reject the competing publication",
81
+ "ddc-head-conflict": "inspect the current head and retry with a fresh revision",
82
+ "ddc-generation-conflict": "allocate a new persistent generation; do not edit the counter",
83
+ "ddc-scope-mismatch": "reinspect the canonical root and allocate a new scope",
84
+ "ddc-lease-expired": "discard the stale commit and acquire a new lease",
85
+ "ddc-entry-incomplete": "discard the partial entry and cold-cook from author authority",
86
+ "ddc-entry-invalid": "preserve last-known-good, inspect the entry, then cold-cook",
87
+ "ddc-entry-conflict": "keep the verified entry and reject the competing publication"
88
+ };
89
+ function defaultExpected(code) {
90
+ if (code === "ddc-project-root-required") return "projectDdcRoot";
91
+ if (code === "ddc-entry-incomplete" || code === "ddc-entry-invalid") {
92
+ return "a complete DDC entry whose receipt and integrity digests validate";
93
+ }
94
+ return "a valid DDC owner, scope, generation, revision, and lease";
95
+ }
96
+ function defaultActions(code) {
97
+ switch (code) {
98
+ case "ddc-project-root-required":
99
+ case "ddc-scope-mismatch":
100
+ return [
101
+ { kind: "inspect", executable: true },
102
+ { kind: "retry", executable: true }
103
+ ];
104
+ case "ddc-generation-conflict":
105
+ return [{ kind: "allocate-generation", executable: true }];
106
+ case "ddc-lease-expired":
107
+ return [
108
+ { kind: "release-lease", executable: true },
109
+ { kind: "retry", executable: true }
110
+ ];
111
+ case "ddc-object-conflict":
112
+ case "ddc-head-conflict":
113
+ return [
114
+ { kind: "inspect", executable: true },
115
+ { kind: "retry", executable: true }
116
+ ];
117
+ case "ddc-entry-incomplete":
118
+ case "ddc-entry-invalid":
119
+ return [{ kind: "cold-rebuild", executable: true }];
120
+ case "ddc-entry-conflict":
121
+ return [{ kind: "inspect", executable: true }];
122
+ }
123
+ }
124
+ var DdcStoreError = class extends Error {
125
+ code;
126
+ detail;
127
+ hint;
128
+ expected;
129
+ actual;
130
+ owner;
131
+ rootKind;
132
+ scope;
133
+ generation;
134
+ lease;
135
+ revision;
136
+ recoveryActions;
137
+ hostPath;
138
+ constructor(inputOrCode, legacyDetail) {
139
+ const input = typeof inputOrCode === "string" ? { code: inputOrCode, detail: legacyDetail ?? inputOrCode } : inputOrCode;
140
+ super(input.detail);
141
+ this.name = "DdcStoreError";
142
+ this.code = input.code;
143
+ this.detail = input.detail;
144
+ this.hint = input.hint ?? defaultHints[input.code];
145
+ this.expected = input.expected ?? defaultExpected(input.code);
146
+ this.actual = input.actual;
147
+ this.owner = input.owner;
148
+ this.rootKind = input.rootKind;
149
+ this.scope = input.scope;
150
+ this.generation = input.generation;
151
+ this.lease = input.lease;
152
+ this.revision = input.revision;
153
+ this.recoveryActions = input.recoveryActions ?? defaultActions(input.code);
154
+ this.hostPath = input.hostPath;
155
+ }
156
+ toBrowserProjection() {
157
+ return {
158
+ code: this.code,
159
+ hint: this.hint,
160
+ expected: this.expected,
161
+ ...this.actual === void 0 ? {} : { actual: this.actual },
162
+ ...this.owner === void 0 ? {} : { owner: this.owner },
163
+ ...this.rootKind === void 0 ? {} : { rootKind: this.rootKind },
164
+ ...this.scope === void 0 ? {} : { scope: this.scope },
165
+ ...this.generation === void 0 ? {} : { generation: this.generation },
166
+ ...this.lease === void 0 ? {} : { lease: this.lease },
167
+ ...this.revision === void 0 ? {} : { revision: this.revision },
168
+ recoveryActions: this.recoveryActions.map(
169
+ ({ exactTarget: _exactTarget, ...action }) => action
170
+ )
171
+ };
172
+ }
173
+ };
174
+
175
+ // src/entry-store.ts
176
+ function digest(value) {
177
+ return createHash("sha256").update(value).digest("hex");
178
+ }
179
+ function artifactFile(key) {
180
+ if (/^[A-Za-z0-9._-]+$/.test(key)) return key;
181
+ return Buffer.from(key, "utf8").toString("base64url");
182
+ }
183
+ function validateKey(key) {
184
+ if (!/^[a-f0-9]{64}$/.test(key)) {
185
+ throw new DdcStoreError("ddc-entry-invalid", `invalid semantic key: ${key}`);
186
+ }
187
+ }
188
+ function errorCode(error) {
189
+ if (error !== null && typeof error === "object" && "code" in error) {
190
+ const code = error.code;
191
+ return typeof code === "string" ? code : void 0;
192
+ }
193
+ return void 0;
194
+ }
195
+ function sortedArtifacts(entry) {
196
+ return Object.entries(entry.artifacts).sort(([left], [right]) => left.localeCompare(right));
197
+ }
198
+ function ddcOutputDigest(entry) {
199
+ return `sha256:${digest(
200
+ canonicalDdcJson({
201
+ guid: entry.guid,
202
+ payload: entry.payload,
203
+ refs: entry.refs,
204
+ artifacts: Object.fromEntries(
205
+ sortedArtifacts(entry).map(([key, artifact]) => [
206
+ key,
207
+ {
208
+ mediaType: artifact.mediaType,
209
+ bytes: artifact.bytes
210
+ }
211
+ ])
212
+ )
213
+ })
214
+ )}`;
215
+ }
216
+ function existingResult(existing, candidate, key) {
217
+ return canonicalDdcJson(existing) === canonicalDdcJson(candidate) ? { result: "existing", key } : { result: "conflict", key };
218
+ }
219
+ function entryShape(entry) {
220
+ return {
221
+ key: entry.key,
222
+ guid: entry.guid,
223
+ payload: entry.payload,
224
+ refs: entry.refs,
225
+ receipt: entry.receipt,
226
+ artifacts: Object.fromEntries(
227
+ sortedArtifacts(entry).map(([key, artifact]) => [
228
+ key,
229
+ { mediaType: artifact.mediaType, byteLength: artifact.bytes.byteLength }
230
+ ])
231
+ )
232
+ };
233
+ }
234
+ function integrityFor(entry) {
235
+ const artifacts = Object.fromEntries(
236
+ sortedArtifacts(entry).map(([key, artifact]) => [
237
+ key,
238
+ {
239
+ mediaType: artifact.mediaType,
240
+ byteLength: artifact.bytes.byteLength,
241
+ digest: digest(artifact.bytes)
242
+ }
243
+ ])
244
+ );
245
+ return {
246
+ payload: digest(canonicalDdcJson(entry.payload)),
247
+ refs: digest(canonicalDdcJson(entry.refs)),
248
+ receipt: digest(canonicalDdcJson(entry.receipt)),
249
+ artifacts,
250
+ entry: digest(canonicalDdcJson(entryShape(entry)))
251
+ };
252
+ }
253
+ async function readJson(path) {
254
+ return JSON.parse(await readFile(path, "utf8"));
255
+ }
256
+ var DdcEntryStore = class {
257
+ entries;
258
+ staging;
259
+ constructor(root) {
260
+ this.entries = join(root, "entries");
261
+ this.staging = join(root, "staging");
262
+ }
263
+ async stage(entry) {
264
+ validateKey(entry.key);
265
+ if (entry.receipt.key !== entry.key || entry.receipt.guid !== entry.guid) {
266
+ throw new DdcStoreError("ddc-entry-invalid", "receipt identity does not match entry");
267
+ }
268
+ if (entry.receipt.outputDigest !== ddcOutputDigest(entry)) {
269
+ throw new DdcStoreError("ddc-entry-invalid", "receipt output digest does not match entry");
270
+ }
271
+ const path = join(this.staging, `${entry.key}-${randomUUID()}`);
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;
290
+ }
291
+ }
292
+ async publish(staged) {
293
+ validateKey(staged.key);
294
+ const candidate = await this.readDirectory(staged.path, true);
295
+ const target = join(this.entries, staged.key);
296
+ await mkdir(this.entries, { recursive: true });
297
+ const useExisting = async () => {
298
+ const existing = await this.readDirectory(target, false);
299
+ await rm(staged.path, { recursive: true, force: true });
300
+ return existingResult(existing, candidate, staged.key);
301
+ };
302
+ try {
303
+ await stat(target);
304
+ return useExisting();
305
+ } catch (error) {
306
+ if (error instanceof DdcStoreError) throw error;
307
+ if (errorCode(error) !== "ENOENT") throw error;
308
+ }
309
+ try {
310
+ await rename(staged.path, target);
311
+ return { result: "published", key: staged.key };
312
+ } catch (error) {
313
+ if (!["EEXIST", "ENOTEMPTY", "EISDIR"].includes(errorCode(error) ?? "")) throw error;
314
+ return useExisting();
315
+ }
316
+ }
317
+ async discard(staged) {
318
+ await rm(staged.path, { recursive: true, force: true });
319
+ }
320
+ async write(entry) {
321
+ const staged = await this.stage(entry);
322
+ return this.publish(staged);
323
+ }
324
+ async read(key) {
325
+ try {
326
+ validateKey(key);
327
+ return await this.readDirectory(join(this.entries, key), false);
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+ async listKeys() {
333
+ const entries = await readdir(this.entries, { withFileTypes: true }).catch(() => []);
334
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
335
+ }
336
+ async remove(key) {
337
+ validateKey(key);
338
+ await rm(join(this.entries, key), { recursive: true, force: false });
339
+ }
340
+ /** Read with a machine-readable corruption result for recovery tooling. */
341
+ async readChecked(key) {
342
+ try {
343
+ validateKey(key);
344
+ try {
345
+ await stat(join(this.entries, key));
346
+ } catch (error) {
347
+ if (errorCode(error) === "ENOENT") return { ok: true, value: null };
348
+ throw error;
349
+ }
350
+ try {
351
+ return { ok: true, value: await this.readDirectory(join(this.entries, key), false) };
352
+ } catch (error) {
353
+ if (error instanceof DdcStoreError && error.code === "ddc-entry-incomplete") {
354
+ return { ok: true, value: null };
355
+ }
356
+ throw error;
357
+ }
358
+ } catch (error) {
359
+ const normalized = error instanceof DdcStoreError ? error : new DdcStoreError("ddc-entry-invalid", "entry is unreadable");
360
+ return { ok: false, error: normalized };
361
+ }
362
+ }
363
+ async readDirectory(path, incompleteIsError) {
364
+ try {
365
+ const payload = await readJson(join(path, "payload.json"));
366
+ const refs = await readJson(join(path, "refs.json"));
367
+ const receipt = await readJson(join(path, "receipt.json"));
368
+ const manifest = await readJson(join(path, "artifacts.json"));
369
+ const integrity = await readJson(join(path, "integrity.json"));
370
+ const artifacts = {};
371
+ for (const [key, descriptor] of Object.entries(manifest).sort(
372
+ ([left], [right]) => left.localeCompare(right)
373
+ )) {
374
+ const bytes = new Uint8Array(
375
+ await readFile(join(path, "artifacts", `${descriptor.file}.bin`))
376
+ );
377
+ artifacts[key] = { mediaType: descriptor.mediaType, bytes };
378
+ }
379
+ const entry = { key: receipt.key, guid: receipt.guid, payload, refs, artifacts, receipt };
380
+ if (entry.receipt.outputDigest !== ddcOutputDigest(entry)) {
381
+ throw new DdcStoreError("ddc-entry-invalid", "receipt output digest does not match entry");
382
+ }
383
+ const actual = integrityFor(entry);
384
+ if (canonicalDdcJson(actual) !== canonicalDdcJson(integrity)) {
385
+ throw new DdcStoreError("ddc-entry-invalid", "entry integrity mismatch");
386
+ }
387
+ return entry;
388
+ } catch (error) {
389
+ if (incompleteIsError) {
390
+ if (error instanceof DdcStoreError) throw error;
391
+ throw new DdcStoreError("ddc-entry-incomplete", "staged entry is incomplete");
392
+ }
393
+ if (error instanceof DdcStoreError) throw error;
394
+ throw new DdcStoreError("ddc-entry-invalid", "entry is unreadable");
395
+ }
396
+ }
397
+ };
398
+ var LOCK_WAIT_MS = 10;
399
+ var LOCK_TIMEOUT_MS = 5e3;
400
+ var LEASE_TTL_MS = 3e4;
401
+ function headFile(heads, guid) {
402
+ return join(heads, `${encodeURIComponent(guid)}.json`);
403
+ }
404
+ function lockFile(root, name) {
405
+ return join(root, "locks", `${encodeURIComponent(name)}.lock`);
406
+ }
407
+ function withRevision(result, revision, restoreFence) {
408
+ const value = { ...result };
409
+ Object.defineProperty(value, "revision", { value: revision, enumerable: false });
410
+ if (restoreFence !== void 0) {
411
+ Object.defineProperty(value, "restoreFence", { value: restoreFence, enumerable: false });
412
+ }
413
+ return value;
414
+ }
415
+ async function delay(ms) {
416
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
417
+ }
418
+ function isRecord(value) {
419
+ return value !== null && typeof value === "object" && !Array.isArray(value);
420
+ }
421
+ function isString(value) {
422
+ return typeof value === "string";
423
+ }
424
+ function isNonNegativeInteger(value) {
425
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
426
+ }
427
+ function isLeaseRecord(value) {
428
+ if (!isRecord(value)) return false;
429
+ return isString(value.guid) && isString(value.desiredKey) && isString(value.attempt) && isNonNegativeInteger(value.generation) && isNonNegativeInteger(value.expectedRevision) && isString(value.instanceId) && typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt);
430
+ }
431
+ function isHeadRecord(value) {
432
+ if (!isRecord(value)) return false;
433
+ if (!isString(value.guid) || !isString(value.desiredKey) || !isNonNegativeInteger(value.revision)) {
434
+ return false;
435
+ }
436
+ if (value.currentKey !== void 0 && !isString(value.currentKey)) return false;
437
+ if (value.lastKnownGoodKey !== void 0 && !isString(value.lastKnownGoodKey)) return false;
438
+ if (value.generation !== void 0 && (!isNonNegativeInteger(value.generation) || value.generation === 0)) {
439
+ return false;
440
+ }
441
+ if (value.active !== void 0 && !isLeaseRecord(value.active)) return false;
442
+ if (value.supersededAttempts !== void 0 && (!Array.isArray(value.supersededAttempts) || !value.supersededAttempts.every(isString))) {
443
+ return false;
444
+ }
445
+ if (value.stale !== void 0 && typeof value.stale !== "boolean") return false;
446
+ if (value.empty !== void 0 && typeof value.empty !== "boolean") return false;
447
+ if (value.failure !== void 0) {
448
+ if (!isRecord(value.failure)) return false;
449
+ if (!isString(value.failure.desiredKey) || !isString(value.failure.code) || !isString(value.failure.detail)) {
450
+ return false;
451
+ }
452
+ }
453
+ return true;
454
+ }
455
+ function malformedHeadError(actual) {
456
+ const detail = actual === "syntax-invalid" ? "DDC head JSON is syntactically invalid" : actual === "schema-invalid" ? "DDC head record does not match the lifecycle schema" : "DDC head file is not readable";
457
+ return new DdcStoreError({
458
+ code: "ddc-head-conflict",
459
+ detail,
460
+ expected: "a valid DDC head record",
461
+ actual,
462
+ hint: "inspect the current head and retry with a fresh revision",
463
+ owner: "engine-ddc",
464
+ rootKind: "project-ddc",
465
+ recoveryActions: [
466
+ { kind: "inspect", executable: true },
467
+ { kind: "retry", executable: true }
468
+ ]
469
+ });
470
+ }
471
+ async function withDdcLock(root, name, operation) {
472
+ const path = lockFile(root, name);
473
+ await mkdir(join(root, "locks"), { recursive: true });
474
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
475
+ while (true) {
476
+ try {
477
+ await mkdir(path);
478
+ await writeFile(
479
+ join(path, "owner.json"),
480
+ JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })
481
+ );
482
+ try {
483
+ return await operation();
484
+ } finally {
485
+ await rm(path, { recursive: true, force: true });
486
+ }
487
+ } catch (error) {
488
+ if (error.code !== "EEXIST") throw error;
489
+ let ownerPid;
490
+ try {
491
+ ownerPid = JSON.parse(await readFile(join(path, "owner.json"), "utf8")).pid;
492
+ } catch {
493
+ ownerPid = void 0;
494
+ }
495
+ if (ownerPid === void 0) {
496
+ if (Date.now() >= deadline) {
497
+ throw new DdcStoreError({
498
+ code: "ddc-lease-expired",
499
+ detail: `DDC lock ${name} did not publish an owner record`,
500
+ hint: "inspect the lock owner and retry after the incomplete instance exits",
501
+ expected: "a lock owner record",
502
+ actual: { path },
503
+ rootKind: "project-ddc"
504
+ });
505
+ }
506
+ await delay(LOCK_WAIT_MS);
507
+ continue;
508
+ }
509
+ let alive = false;
510
+ try {
511
+ process.kill(ownerPid, 0);
512
+ alive = true;
513
+ } catch {
514
+ alive = false;
515
+ }
516
+ if (!alive) {
517
+ await rm(path, { recursive: true, force: true });
518
+ continue;
519
+ }
520
+ if (Date.now() >= deadline) {
521
+ throw new DdcStoreError({
522
+ code: "ddc-lease-expired",
523
+ detail: `DDC lock ${name} remained owned by process ${ownerPid}`,
524
+ hint: "inspect the owner and retry after the stale instance exits",
525
+ expected: "an available project lock",
526
+ actual: { ownerPid },
527
+ rootKind: "project-ddc",
528
+ lease: String(ownerPid)
529
+ });
530
+ }
531
+ await delay(LOCK_WAIT_MS);
532
+ }
533
+ }
534
+ }
535
+ var DdcLifecycle = class {
536
+ heads;
537
+ entries;
538
+ root;
539
+ leaseTtlMs;
540
+ constructor(root, options) {
541
+ this.root = root;
542
+ this.heads = join(root, "heads");
543
+ this.entries = new DdcEntryStore(root);
544
+ this.leaseTtlMs = options?.leaseTtlMs ?? LEASE_TTL_MS;
545
+ }
546
+ async inspect(guid, desiredKey) {
547
+ const record = await this.read(guid);
548
+ return this.projectHead(guid, desiredKey, record);
549
+ }
550
+ /** Begin a lease and capture the accepted rollback snapshot under one head lock. */
551
+ async beginWithSnapshot(guid, desiredKey) {
552
+ return withDdcLock(this.root, `head-${guid}`, async () => {
553
+ const previous = await this.read(guid);
554
+ const previousHead = await this.projectHead(guid, desiredKey, previous, false);
555
+ const generation = await this.allocateGeneration(previous?.generation ?? 0);
556
+ const lease = {
557
+ guid,
558
+ desiredKey,
559
+ attempt: randomUUID(),
560
+ generation,
561
+ expectedRevision: previous?.revision ?? 0,
562
+ instanceId: `${process.pid}:${randomUUID()}`,
563
+ expiresAt: Date.now() + this.leaseTtlMs
564
+ };
565
+ const lastKnownGoodKey = previous?.currentKey !== void 0 && previous.currentKey !== desiredKey ? previous.currentKey : previous?.lastKnownGoodKey;
566
+ const supersededAttempts = [
567
+ ...previous?.supersededAttempts ?? [],
568
+ ...previous?.active === void 0 ? [] : [previous.active.attempt]
569
+ ];
570
+ await this.write({
571
+ guid,
572
+ desiredKey,
573
+ revision: previous?.revision ?? 0,
574
+ generation,
575
+ active: lease,
576
+ ...previous?.currentKey === void 0 ? {} : { currentKey: previous.currentKey },
577
+ ...supersededAttempts.length === 0 ? {} : { supersededAttempts },
578
+ ...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
579
+ });
580
+ return { lease, previousHead };
581
+ });
582
+ }
583
+ async begin(guid, desiredKey) {
584
+ return (await this.beginWithSnapshot(guid, desiredKey)).lease;
585
+ }
586
+ async commit(lease, validatedKey) {
587
+ return withDdcLock(this.root, `head-${lease.guid}`, async () => {
588
+ const current = await this.read(lease.guid);
589
+ if (current?.active?.attempt !== lease.attempt) {
590
+ if (current?.supersededAttempts?.includes(lease.attempt)) {
591
+ await this.write({ ...current, stale: true });
592
+ return withRevision({ result: "stale", key: validatedKey }, current.revision);
593
+ }
594
+ return withRevision({ result: "lease-lost", key: validatedKey }, current?.revision ?? 0);
595
+ }
596
+ if (Date.now() > lease.expiresAt) {
597
+ throw new DdcStoreError({
598
+ code: "ddc-lease-expired",
599
+ detail: "cook lease expired before commit fencing",
600
+ expected: { attempt: lease.attempt, revision: lease.expectedRevision },
601
+ actual: { revision: current.revision },
602
+ owner: "engine-ddc",
603
+ rootKind: "project-ddc",
604
+ generation: lease.generation,
605
+ lease: lease.attempt,
606
+ revision: current.revision
607
+ });
608
+ }
609
+ if (current.revision !== lease.expectedRevision || current.desiredKey !== lease.desiredKey || validatedKey !== lease.desiredKey) {
610
+ await this.write({ ...current, stale: true });
611
+ return withRevision({ result: "stale", key: validatedKey }, current.revision);
612
+ }
613
+ const entry = await this.entries.read(validatedKey);
614
+ if (entry === null || entry.guid !== lease.guid || entry.receipt.key !== validatedKey) {
615
+ const nextRevision2 = current.revision + 1;
616
+ const failure2 = {
617
+ desiredKey: lease.desiredKey,
618
+ code: "entry-invalid",
619
+ detail: "validated DDC key has no readable entry for this asset"
620
+ };
621
+ await this.write({
622
+ guid: lease.guid,
623
+ desiredKey: lease.desiredKey,
624
+ revision: nextRevision2,
625
+ generation: lease.generation,
626
+ ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
627
+ ...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
628
+ failure: failure2
629
+ });
630
+ return withRevision({ result: "invalid", key: validatedKey }, nextRevision2, {
631
+ attempt: lease.attempt,
632
+ generation: lease.generation,
633
+ revision: nextRevision2,
634
+ desiredKey: lease.desiredKey,
635
+ outcome: "invalid",
636
+ ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
637
+ failure: { code: failure2.code, detail: failure2.detail }
638
+ });
639
+ }
640
+ const lastKnownGoodKey = current.currentKey !== void 0 && current.currentKey !== validatedKey ? current.currentKey : current.lastKnownGoodKey;
641
+ const nextRevision = current.revision + 1;
642
+ await this.write({
643
+ guid: lease.guid,
644
+ desiredKey: lease.desiredKey,
645
+ revision: nextRevision,
646
+ generation: lease.generation,
647
+ currentKey: validatedKey,
648
+ stale: false,
649
+ ...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
650
+ ...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
651
+ });
652
+ return withRevision({ result: "current", key: validatedKey }, nextRevision, {
653
+ attempt: lease.attempt,
654
+ generation: lease.generation,
655
+ revision: nextRevision,
656
+ desiredKey: lease.desiredKey,
657
+ outcome: "current",
658
+ key: validatedKey,
659
+ currentKey: validatedKey,
660
+ ...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
661
+ });
662
+ });
663
+ }
664
+ async fail(lease, failure2) {
665
+ return withDdcLock(this.root, `head-${lease.guid}`, async () => {
666
+ const current = await this.read(lease.guid);
667
+ if (current?.active?.attempt !== lease.attempt) return;
668
+ const nextRevision = current.revision + 1;
669
+ await this.write({
670
+ guid: lease.guid,
671
+ desiredKey: lease.desiredKey,
672
+ revision: nextRevision,
673
+ generation: lease.generation,
674
+ ...current.currentKey === void 0 ? {} : { currentKey: current.currentKey },
675
+ ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
676
+ ...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
677
+ failure: { desiredKey: lease.desiredKey, ...failure2 }
678
+ });
679
+ return {
680
+ attempt: lease.attempt,
681
+ generation: lease.generation,
682
+ revision: nextRevision,
683
+ desiredKey: lease.desiredKey,
684
+ outcome: "failed",
685
+ ...current.currentKey === void 0 ? {} : { currentKey: current.currentKey },
686
+ ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
687
+ failure: failure2
688
+ };
689
+ });
690
+ }
691
+ async heartbeat(lease) {
692
+ return withDdcLock(this.root, `head-${lease.guid}`, async () => {
693
+ const current = await this.read(lease.guid);
694
+ if (current?.active?.attempt !== lease.attempt) {
695
+ throw new DdcStoreError({
696
+ code: "ddc-lease-expired",
697
+ detail: "heartbeat belongs to a stale lease",
698
+ expected: lease.attempt,
699
+ actual: current?.active?.attempt,
700
+ lease: lease.attempt,
701
+ generation: lease.generation,
702
+ rootKind: "project-ddc"
703
+ });
704
+ }
705
+ const refreshed = { ...lease, expiresAt: Date.now() + this.leaseTtlMs };
706
+ await this.write({ ...current, active: refreshed });
707
+ return refreshed;
708
+ });
709
+ }
710
+ async close(lease) {
711
+ await withDdcLock(this.root, `head-${lease.guid}`, async () => {
712
+ const current = await this.read(lease.guid);
713
+ if (current?.active?.instanceId !== lease.instanceId) return;
714
+ const { active: _active, ...withoutActive } = current;
715
+ await this.write({
716
+ ...withoutActive,
717
+ revision: current.revision + 1,
718
+ stale: current.currentKey === void 0
719
+ });
720
+ });
721
+ }
722
+ async discard(lease) {
723
+ await withDdcLock(this.root, `head-${lease.guid}`, async () => {
724
+ const current = await this.read(lease.guid);
725
+ if (current?.active?.attempt !== lease.attempt) return;
726
+ const { active: _active, ...withoutActive } = current;
727
+ await this.write({
728
+ ...withoutActive,
729
+ revision: current.revision + 1,
730
+ generation: lease.generation,
731
+ stale: current.currentKey === void 0
732
+ });
733
+ });
734
+ }
735
+ /**
736
+ * Restore accepted content only when this lease still owns the mutable head.
737
+ * A newer active or terminal mutation makes rollback a deliberate no-op.
738
+ */
739
+ async restore(head, lease, fence) {
740
+ const inferredLease = lease ?? ("activeLease" in head && head.activeLease !== void 0 ? head.activeLease : void 0);
741
+ if (inferredLease === void 0) {
742
+ return {
743
+ result: "not-owner",
744
+ revision: head.revision ?? 0,
745
+ ...head.generation === void 0 ? {} : { generation: head.generation }
746
+ };
747
+ }
748
+ const snapshot = this.rollbackSnapshot(head);
749
+ return this.restoreIfCurrent(snapshot, inferredLease, fence);
750
+ }
751
+ async restoreIfCurrent(snapshot, lease, fence) {
752
+ return withDdcLock(this.root, `head-${lease.guid}`, async () => {
753
+ const current = await this.read(lease.guid);
754
+ if (current === null || current.guid !== snapshot.guid) {
755
+ return {
756
+ result: "not-owner",
757
+ revision: current?.revision ?? 0,
758
+ ...current?.generation === void 0 ? {} : { generation: current.generation }
759
+ };
760
+ }
761
+ const ownsActive = current.active?.attempt === lease.attempt && current.active.generation === lease.generation && current.active.desiredKey === lease.desiredKey;
762
+ const ownsTerminal = fence !== void 0 && this.matchesRestoreFence(current, lease, fence);
763
+ if (!ownsActive && !ownsTerminal) {
764
+ return {
765
+ result: "not-owner",
766
+ revision: current.revision,
767
+ ...current.generation === void 0 ? {} : { generation: current.generation }
768
+ };
769
+ }
770
+ const nextRevision = current.revision + 1;
771
+ const generation = Math.max(
772
+ current.generation ?? 0,
773
+ lease.generation,
774
+ snapshot.generation ?? 0
775
+ );
776
+ const supersededAttempts = current.supersededAttempts;
777
+ const restored = {
778
+ guid: snapshot.guid,
779
+ desiredKey: snapshot.desiredKey,
780
+ revision: nextRevision,
781
+ generation,
782
+ ...snapshot.currentKey === void 0 ? {} : { currentKey: snapshot.currentKey },
783
+ ...snapshot.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: snapshot.lastKnownGoodKey },
784
+ ...supersededAttempts === void 0 ? {} : { supersededAttempts },
785
+ ...snapshot.state === "missing" ? { empty: true } : {},
786
+ ...snapshot.state === "stale" ? { stale: true } : {},
787
+ ...snapshot.failure === void 0 ? {} : { failure: { desiredKey: snapshot.desiredKey, ...snapshot.failure } }
788
+ };
789
+ await this.write(restored);
790
+ return { result: "restored", revision: nextRevision, generation };
791
+ });
792
+ }
793
+ async revoke(lease) {
794
+ await this.fail(lease, {
795
+ code: "lease-lost",
796
+ detail: "cook lease was revoked before validation"
797
+ });
798
+ }
799
+ async recover(guid, desiredKey) {
800
+ return withDdcLock(this.root, `head-${guid}`, async () => {
801
+ const current = await this.read(guid);
802
+ if (current?.active?.desiredKey === desiredKey) {
803
+ await this.write({
804
+ guid,
805
+ desiredKey,
806
+ revision: current.revision + 1,
807
+ ...current.generation === void 0 ? {} : { generation: current.generation },
808
+ ...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
809
+ ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
810
+ failure: {
811
+ desiredKey,
812
+ code: "writer-crashed",
813
+ detail: "active cook attempt was recovered after process interruption"
814
+ }
815
+ });
816
+ }
817
+ return this.inspect(guid, desiredKey);
818
+ });
819
+ }
820
+ rollbackSnapshot(head) {
821
+ const { activeLease: _activeLease, ...snapshot } = head;
822
+ if (snapshot.state !== "cooking") return snapshot;
823
+ return {
824
+ ...snapshot,
825
+ state: snapshot.currentKey === void 0 ? "missing" : "stale"
826
+ };
827
+ }
828
+ matchesRestoreFence(current, lease, fence) {
829
+ if (fence.attempt !== lease.attempt || fence.generation !== lease.generation || current.active !== void 0 || current.revision !== fence.revision || current.generation !== fence.generation || current.desiredKey !== fence.desiredKey || current.currentKey !== fence.currentKey || current.lastKnownGoodKey !== fence.lastKnownGoodKey) {
830
+ return false;
831
+ }
832
+ if (fence.outcome === "current") {
833
+ return fence.key !== void 0 && current.currentKey === fence.key && current.failure === void 0 && current.stale !== true;
834
+ }
835
+ if (fence.failure === void 0 || current.failure === void 0 || current.failure.desiredKey !== fence.desiredKey || current.failure.code !== fence.failure.code || current.failure.detail !== fence.failure.detail) {
836
+ return false;
837
+ }
838
+ return fence.outcome === "invalid" ? fence.currentKey === void 0 : true;
839
+ }
840
+ async projectHead(guid, desiredKey, record, includeActive = true) {
841
+ if (record === null) {
842
+ return {
843
+ guid,
844
+ desiredKey,
845
+ state: "missing",
846
+ currentKey: void 0,
847
+ lastKnownGoodKey: void 0,
848
+ revision: 0
849
+ };
850
+ }
851
+ const recordedFailure = record.failure?.desiredKey === desiredKey ? { code: record.failure.code, detail: record.failure.detail } : void 0;
852
+ const currentEntry = record.currentKey === void 0 ? null : await this.entries.readChecked(record.currentKey);
853
+ const entryFailure = currentEntry !== null && !currentEntry.ok ? { code: currentEntry.error.code, detail: currentEntry.error.detail } : void 0;
854
+ const failure2 = recordedFailure ?? entryFailure;
855
+ const currentValue = currentEntry?.ok === true ? currentEntry.value : null;
856
+ const state = failure2 !== void 0 ? "failed" : record.currentKey === desiredKey && currentValue?.guid === guid ? "current" : record.stale === true ? "stale" : includeActive && record.active?.desiredKey === desiredKey ? "cooking" : record.empty === true ? "missing" : "stale";
857
+ return {
858
+ guid,
859
+ desiredKey,
860
+ state,
861
+ currentKey: record.currentKey,
862
+ lastKnownGoodKey: record.lastKnownGoodKey,
863
+ revision: record.revision,
864
+ ...record.generation === void 0 ? {} : { generation: record.generation },
865
+ ...includeActive && record.active === void 0 ? {} : includeActive && record.active !== void 0 ? { activeLease: record.active } : {},
866
+ ...failure2 === void 0 ? {} : { failure: failure2 }
867
+ };
868
+ }
869
+ async allocateGeneration(minimumExclusive = 0) {
870
+ return withDdcLock(this.root, "generation-counter", async () => {
871
+ const path = join(this.root, "generations", "counter.json");
872
+ await mkdir(join(this.root, "generations"), { recursive: true });
873
+ let next = 1;
874
+ try {
875
+ const record = JSON.parse(await readFile(path, "utf8"));
876
+ if (record.schemaVersion === "forgeax-ddc-generation/v2" && Number.isSafeInteger(record.next) && record.next >= 1)
877
+ next = Math.max(1, record.next);
878
+ } catch {
879
+ }
880
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
881
+ next = Math.max(next, minimumExclusive + 1);
882
+ await writeFile(
883
+ temporary,
884
+ JSON.stringify({ schemaVersion: "forgeax-ddc-generation/v2", next: next + 1 })
885
+ );
886
+ await rename(temporary, path);
887
+ return next;
888
+ });
889
+ }
890
+ async read(guid) {
891
+ const path = headFile(this.heads, guid);
892
+ let source;
893
+ try {
894
+ source = await readFile(path, "utf8");
895
+ } catch (error) {
896
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") {
897
+ return null;
898
+ }
899
+ throw malformedHeadError("unreadable");
900
+ }
901
+ let value;
902
+ try {
903
+ value = JSON.parse(source);
904
+ } catch {
905
+ throw malformedHeadError("syntax-invalid");
906
+ }
907
+ if (!isHeadRecord(value) || value.guid !== guid) throw malformedHeadError("schema-invalid");
908
+ return value;
909
+ }
910
+ async write(record) {
911
+ await mkdir(this.heads, { recursive: true });
912
+ const path = headFile(this.heads, record.guid);
913
+ const temporary = `${path}.${randomUUID()}.tmp`;
914
+ await writeFile(temporary, JSON.stringify(record));
915
+ await rename(temporary, path);
916
+ }
917
+ };
918
+
919
+ // src/gc.ts
920
+ async function collectDdcGarbage(root, input) {
921
+ return withDdcLock(root, "gc", async () => {
922
+ const protectedKeys = /* @__PURE__ */ new Set([
923
+ ...input.currentKeys,
924
+ ...input.lastKnownGoodKeys,
925
+ ...input.activeLeaseKeys
926
+ ]);
927
+ const entries = await readdir(join(root, "entries"), { withFileTypes: true }).catch(() => []);
928
+ const deleted = [];
929
+ for (const entry of entries) {
930
+ if (!entry.isDirectory() || protectedKeys.has(entry.name)) continue;
931
+ await rm(join(root, "entries", entry.name), { recursive: true, force: false });
932
+ deleted.push(entry.name);
933
+ }
934
+ return { deleted, protected: [...protectedKeys].sort() };
935
+ });
936
+ }
937
+ var DDC_LAYOUT_VERSION = "v2";
938
+ function rootOrError(root, name) {
939
+ if (!isAbsolute(root)) {
940
+ return {
941
+ code: "ddc-root-absolute-required",
942
+ detail: `${name} must be an absolute injected path`,
943
+ hint: `inject the canonical ${name} from the host root policy`,
944
+ expected: "an absolute filesystem path",
945
+ actual: root
946
+ };
947
+ }
948
+ const normalized = normalize(root);
949
+ return normalized.replace(/[\\/]+$/, "") || normalized;
950
+ }
951
+ function buildLayout(root) {
952
+ return { objects: `${root}/objects`, staging: `${root}/staging` };
953
+ }
954
+ function projectLayout(root) {
955
+ return {
956
+ root,
957
+ scope: `${root}/scope.json`,
958
+ heads: `${root}/heads`,
959
+ generations: `${root}/generations`,
960
+ leases: `${root}/leases`,
961
+ staging: `${root}/staging`
962
+ };
963
+ }
964
+ function resolveBuildDdcLayout(options) {
965
+ const root = rootOrError(options.buildCacheRoot, "buildCacheRoot");
966
+ return typeof root === "string" ? { ok: true, value: buildLayout(root) } : { ok: false, error: root };
967
+ }
968
+ function resolveDdcLayout(options) {
969
+ const buildRoot = rootOrError(options.buildCacheRoot, "buildCacheRoot");
970
+ if (typeof buildRoot !== "string") return { ok: false, error: buildRoot };
971
+ if (options.projectDdcRoot === void 0 || options.projectDdcRoot.trim().length === 0) {
972
+ return {
973
+ ok: false,
974
+ error: {
975
+ code: "ddc-project-root-required",
976
+ detail: "serve and publication require an explicitly injected projectDdcRoot",
977
+ hint: "canonicalize the game directory in the host and inject its .forgeax/ddc/v2 root",
978
+ expected: "projectDdcRoot"
979
+ }
980
+ };
981
+ }
982
+ const projectRoot = rootOrError(options.projectDdcRoot, "projectDdcRoot");
983
+ if (typeof projectRoot !== "string") return { ok: false, error: projectRoot };
984
+ return {
985
+ ok: true,
986
+ value: {
987
+ version: DDC_LAYOUT_VERSION,
988
+ buildCacheRoot: buildRoot,
989
+ projectDdcRoot: projectRoot,
990
+ build: buildLayout(buildRoot),
991
+ project: projectLayout(projectRoot)
992
+ }
993
+ };
994
+ }
995
+ function isDdcLayout(value) {
996
+ return value !== null && typeof value === "object" && value.version === DDC_LAYOUT_VERSION;
997
+ }
998
+ function stable(value) {
999
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
1000
+ if (value !== null && typeof value === "object") {
1001
+ const record = value;
1002
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stable(record[key])}`).join(",")}}`;
1003
+ }
1004
+ return JSON.stringify(value) ?? "null";
1005
+ }
1006
+ function digest2(value) {
1007
+ return `sha256:${createHash("sha256").update(stable(value)).digest("hex")}`;
1008
+ }
1009
+ function scriptablePackOutputSetDigest(outputs) {
1010
+ return digest2(
1011
+ outputs.map((output) => ({
1012
+ guid: output.guid.toLowerCase(),
1013
+ sourceKey: output.sourceKey,
1014
+ kind: output.kind,
1015
+ digest: output.digest,
1016
+ refs: [...output.refs].map((guid) => guid.toLowerCase())
1017
+ }))
1018
+ );
1019
+ }
1020
+ function scriptablePackPublicationGeneration(input) {
1021
+ const hash = createHash("sha256").update(input.sourceRevision).update("\n").update(input.digest).update("\n").update(input.outputSetDigest).digest("hex");
1022
+ const generation = Number.parseInt(hash.slice(0, 8), 16);
1023
+ return generation > 0 ? generation : 1;
1024
+ }
1025
+ function createAcceptedPublication(input) {
1026
+ const outputSetDigest = scriptablePackOutputSetDigest(input.outputs);
1027
+ const publicationGeneration = input.generation ?? scriptablePackPublicationGeneration({
1028
+ sourceRevision: input.sourceRevision,
1029
+ digest: input.digest,
1030
+ outputSetDigest
1031
+ });
1032
+ return {
1033
+ schemaVersion: "asset-publication/1",
1034
+ sourcePath: input.sourcePath,
1035
+ sourceRevision: input.sourceRevision,
1036
+ generation: publicationGeneration,
1037
+ digest: input.digest,
1038
+ outputSetDigest,
1039
+ outputs: input.outputs,
1040
+ receipt: {
1041
+ schemaVersion: "asset-publication-receipt/1",
1042
+ sourcePath: input.sourcePath,
1043
+ sourceRevision: input.sourceRevision,
1044
+ inputFingerprint: input.inputFingerprint,
1045
+ outputDigest: input.digest,
1046
+ outputSetDigest,
1047
+ externalEvidence: input.externalEvidence
1048
+ },
1049
+ externalEvidence: input.externalEvidence,
1050
+ current: {
1051
+ generation: publicationGeneration,
1052
+ digest: input.digest,
1053
+ outputSetDigest,
1054
+ packageUrl: input.packageUrl,
1055
+ receiptKey: input.inputFingerprint
1056
+ }
1057
+ };
1058
+ }
1059
+ function locatorFor(envelope) {
1060
+ return {
1061
+ generation: envelope.generation,
1062
+ digest: envelope.digest,
1063
+ outputSetDigest: envelope.outputSetDigest,
1064
+ packageUrl: envelope.current?.packageUrl ?? "",
1065
+ receiptKey: envelope.receipt.inputFingerprint
1066
+ };
1067
+ }
1068
+ function recoveryFor(retryable, useLastKnownGood) {
1069
+ return {
1070
+ retryable,
1071
+ preserveCurrent: true,
1072
+ useLastKnownGood,
1073
+ actions: retryable ? ["inspect-publication-failure", "retry-rebuild", "continue-last-known-good"] : ["inspect-publication-failure", "edit-source", "continue-last-known-good"]
1074
+ };
1075
+ }
1076
+ function failure(envelope, code, stage, reason, retryable, current, lastKnownGood) {
1077
+ return {
1078
+ code,
1079
+ stage,
1080
+ sourcePath: envelope.sourcePath,
1081
+ sourceRevision: envelope.sourceRevision,
1082
+ generation: envelope.generation,
1083
+ reason,
1084
+ recovery: recoveryFor(retryable, lastKnownGood !== void 0),
1085
+ ...current === void 0 ? {} : { current: locatorFor(current) },
1086
+ ...lastKnownGood === void 0 ? {} : { lastKnownGood: locatorFor(lastKnownGood) }
1087
+ };
1088
+ }
1089
+ function validateOutputs(envelope, current, lastKnownGood) {
1090
+ const guids = /* @__PURE__ */ new Set();
1091
+ const sourceKeys = /* @__PURE__ */ new Set();
1092
+ for (const output of envelope.outputs) {
1093
+ const guid = output.guid.toLowerCase();
1094
+ if (guids.has(guid) || sourceKeys.has(output.sourceKey)) {
1095
+ return failure(
1096
+ envelope,
1097
+ "asset-publication-output-duplicate",
1098
+ "output",
1099
+ "publication output GUIDs and sourceKeys must be unique",
1100
+ false,
1101
+ current,
1102
+ lastKnownGood
1103
+ );
1104
+ }
1105
+ guids.add(guid);
1106
+ sourceKeys.add(output.sourceKey);
1107
+ }
1108
+ if (envelope.outputs.length === 0 || scriptablePackOutputSetDigest(envelope.outputs) !== envelope.outputSetDigest) {
1109
+ return failure(
1110
+ envelope,
1111
+ "asset-publication-output-set-mismatch",
1112
+ "receipt",
1113
+ "receipt outputSetDigest does not match the complete output tuple",
1114
+ false,
1115
+ current,
1116
+ lastKnownGood
1117
+ );
1118
+ }
1119
+ return void 0;
1120
+ }
1121
+ function validateReceipt(envelope, current, lastKnownGood) {
1122
+ const receipt = envelope.receipt;
1123
+ 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)) {
1124
+ return failure(
1125
+ envelope,
1126
+ "asset-publication-receipt-mismatch",
1127
+ "receipt",
1128
+ "publication receipt does not match the candidate output tuple",
1129
+ false,
1130
+ current,
1131
+ lastKnownGood
1132
+ );
1133
+ }
1134
+ return void 0;
1135
+ }
1136
+ function validateEnvelope(envelope, current, lastKnownGood) {
1137
+ if (envelope.schemaVersion !== "asset-publication/1") {
1138
+ return failure(
1139
+ envelope,
1140
+ "asset-publication-schema-invalid",
1141
+ "receipt",
1142
+ "publication envelope schemaVersion is not supported",
1143
+ false,
1144
+ current,
1145
+ lastKnownGood
1146
+ );
1147
+ }
1148
+ if (!Number.isSafeInteger(envelope.generation) || envelope.generation < 1) {
1149
+ return failure(
1150
+ envelope,
1151
+ "asset-publication-generation-invalid",
1152
+ "receipt",
1153
+ "publication generation must be a positive integer",
1154
+ false,
1155
+ current,
1156
+ lastKnownGood
1157
+ );
1158
+ }
1159
+ const outputError = validateOutputs(envelope, current, lastKnownGood);
1160
+ if (outputError !== void 0) return outputError;
1161
+ const receiptError = validateReceipt(envelope, current, lastKnownGood);
1162
+ if (receiptError !== void 0) return receiptError;
1163
+ 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;
1164
+ if (sameTuple) return void 0;
1165
+ if (current !== void 0 && envelope.generation <= current.generation) {
1166
+ return failure(
1167
+ envelope,
1168
+ "asset-publication-stale",
1169
+ "cancelled",
1170
+ `candidate generation ${envelope.generation} is not newer than current ${current.generation}`,
1171
+ true,
1172
+ current,
1173
+ lastKnownGood
1174
+ );
1175
+ }
1176
+ return void 0;
1177
+ }
1178
+ function validateCandidate(state, candidate, cancelledReason) {
1179
+ const current = state.snapshot.current;
1180
+ const lastKnownGood = state.snapshot.lastKnownGood ?? current;
1181
+ if (candidate.cancelled === true) {
1182
+ return failure(
1183
+ candidate.envelope,
1184
+ "asset-publication-cancelled",
1185
+ "cancelled",
1186
+ cancelledReason,
1187
+ true,
1188
+ current,
1189
+ lastKnownGood
1190
+ );
1191
+ }
1192
+ return validateEnvelope(candidate.envelope, current, lastKnownGood);
1193
+ }
1194
+ async function publishCandidate(state, candidate, commitRoute) {
1195
+ const current = state.snapshot.current;
1196
+ const lastKnownGood = state.snapshot.lastKnownGood ?? current;
1197
+ const invalid = validateCandidate(
1198
+ state,
1199
+ candidate,
1200
+ "publication request was cancelled before route commit"
1201
+ );
1202
+ if (invalid !== void 0) {
1203
+ state.snapshot = { ...state.snapshot, failure: invalid };
1204
+ return err(invalid);
1205
+ }
1206
+ try {
1207
+ await commitRoute();
1208
+ } catch (error) {
1209
+ const failed = failure(
1210
+ candidate.envelope,
1211
+ "asset-publication-route-failed",
1212
+ "route",
1213
+ error instanceof Error ? error.message : String(error),
1214
+ true,
1215
+ current,
1216
+ lastKnownGood
1217
+ );
1218
+ state.snapshot = { ...state.snapshot, failure: failed };
1219
+ return err(failed);
1220
+ }
1221
+ const published = {
1222
+ ...candidate.envelope,
1223
+ current: locatorFor(candidate.envelope),
1224
+ ...current === void 0 ? {} : { lastKnownGood: locatorFor(current) },
1225
+ recovery: recoveryFor(false, current !== void 0)
1226
+ };
1227
+ state.snapshot = {
1228
+ current: published,
1229
+ ...current === void 0 ? {} : { lastKnownGood: current }
1230
+ };
1231
+ return ok(state.snapshot);
1232
+ }
1233
+ function createAcceptedPublicationStore() {
1234
+ const states = /* @__PURE__ */ new Map();
1235
+ function stateFor(sourcePath) {
1236
+ const existing = states.get(sourcePath);
1237
+ if (existing !== void 0) return existing;
1238
+ const created = { snapshot: {}, staged: /* @__PURE__ */ new Set() };
1239
+ states.set(sourcePath, created);
1240
+ return created;
1241
+ }
1242
+ return {
1243
+ observe(sourcePath) {
1244
+ return stateFor(sourcePath).snapshot;
1245
+ },
1246
+ stage(sourcePath, candidate) {
1247
+ const invalid = validateCandidate(
1248
+ stateFor(sourcePath),
1249
+ candidate,
1250
+ "publication request was cancelled before DDC commit"
1251
+ );
1252
+ if (invalid !== void 0) return err(invalid);
1253
+ stateFor(sourcePath).staged.add(candidate.envelope);
1254
+ return ok(void 0);
1255
+ },
1256
+ async commit(sourcePath, candidate, commitRoute) {
1257
+ const state = stateFor(sourcePath);
1258
+ if (!state.staged.has(candidate.envelope)) {
1259
+ const current = state.snapshot.current;
1260
+ const lastKnownGood = state.snapshot.lastKnownGood ?? current;
1261
+ return err(
1262
+ failure(
1263
+ candidate.envelope,
1264
+ "asset-publication-cancelled",
1265
+ "cancelled",
1266
+ "publication candidate was discarded before DDC commit",
1267
+ true,
1268
+ current,
1269
+ lastKnownGood
1270
+ )
1271
+ );
1272
+ }
1273
+ const result = await publishCandidate(state, candidate, commitRoute);
1274
+ state.staged.delete(candidate.envelope);
1275
+ return result;
1276
+ },
1277
+ discard(sourcePath, candidate) {
1278
+ stateFor(sourcePath).staged.delete(candidate);
1279
+ },
1280
+ restore(sourcePath, snapshot) {
1281
+ const state = stateFor(sourcePath);
1282
+ state.staged.clear();
1283
+ state.snapshot = snapshot;
1284
+ }
1285
+ };
1286
+ }
1287
+ function scopeHash(scopeId) {
1288
+ return createHash("sha256").update(scopeId, "utf8").digest("hex");
1289
+ }
1290
+ function runtimeScopeHash(scopeId) {
1291
+ return scopeHash(scopeId);
1292
+ }
1293
+ async function createRuntimeScope(root, scopeId) {
1294
+ if (scopeId.length === 0) {
1295
+ throw new DdcStoreError({
1296
+ code: "ddc-scope-mismatch",
1297
+ detail: "runtime scope id must not be empty",
1298
+ expected: "a non-empty original scope id",
1299
+ actual: scopeId,
1300
+ rootKind: "project-ddc"
1301
+ });
1302
+ }
1303
+ const hash = scopeHash(scopeId);
1304
+ const scopeRoot = join(root, "scopes", hash);
1305
+ const metadataPath = join(scopeRoot, "scope.json");
1306
+ await mkdir(scopeRoot, { recursive: true });
1307
+ const expected = {
1308
+ schemaVersion: "forgeax-ddc-scope/v2",
1309
+ scopeId,
1310
+ scopeHash: hash
1311
+ };
1312
+ try {
1313
+ const actual = JSON.parse(await readFile(metadataPath, "utf8"));
1314
+ if (actual.schemaVersion !== expected.schemaVersion || actual.scopeId !== expected.scopeId || actual.scopeHash !== expected.scopeHash) {
1315
+ throw new DdcStoreError({
1316
+ code: "ddc-scope-mismatch",
1317
+ detail: "runtime scope metadata does not match the requested scope",
1318
+ expected,
1319
+ actual,
1320
+ rootKind: "project-ddc",
1321
+ scope: scopeId
1322
+ });
1323
+ }
1324
+ } catch (error) {
1325
+ if (error instanceof DdcStoreError) throw error;
1326
+ const temporary = `${metadataPath}.${process.pid}.tmp`;
1327
+ await writeFile(temporary, JSON.stringify(expected));
1328
+ await rename(temporary, metadataPath);
1329
+ }
1330
+ return { scopeId, scopeHash: hash, root: scopeRoot, metadataPath };
1331
+ }
1332
+ async function assertRuntimeScope(root, scope) {
1333
+ const expectedHash = scopeHash(scope.scopeId);
1334
+ if (scope.scopeHash !== expectedHash) {
1335
+ throw new DdcStoreError({
1336
+ code: "ddc-scope-mismatch",
1337
+ detail: "runtime scope hash does not match the original scope id",
1338
+ expected: expectedHash,
1339
+ actual: scope.scopeHash,
1340
+ rootKind: "project-ddc",
1341
+ scope: scope.scopeId
1342
+ });
1343
+ }
1344
+ return createRuntimeScope(root, scope.scopeId);
1345
+ }
1346
+
1347
+ // src/session.ts
1348
+ var DdcGenerationSession = class {
1349
+ generation;
1350
+ lifecycle;
1351
+ entries;
1352
+ candidates = /* @__PURE__ */ new Map();
1353
+ restoreFences = /* @__PURE__ */ new WeakMap();
1354
+ heartbeatTimers = /* @__PURE__ */ new Map();
1355
+ counters = {
1356
+ hitCount: 0,
1357
+ missCount: 0,
1358
+ corruptCount: 0,
1359
+ writeFailureCount: 0
1360
+ };
1361
+ accepting = true;
1362
+ constructor(root, options) {
1363
+ if (!Number.isSafeInteger(options.generation) || options.generation < 1) {
1364
+ throw new TypeError("DDC generation must be a positive safe integer");
1365
+ }
1366
+ this.generation = options.generation;
1367
+ this.lifecycle = new DdcLifecycle(
1368
+ root,
1369
+ options.leaseTtlMs === void 0 ? void 0 : { leaseTtlMs: options.leaseTtlMs }
1370
+ );
1371
+ this.entries = new DdcEntryStore(root);
1372
+ }
1373
+ async beginCandidate(guid, desiredKey) {
1374
+ this.assertOpen();
1375
+ const { lease, previousHead } = await this.lifecycle.beginWithSnapshot(guid, desiredKey);
1376
+ if (previousHead.state === "current") this.counters.hitCount += 1;
1377
+ else this.counters.missCount += 1;
1378
+ const candidate = {
1379
+ generation: this.generation,
1380
+ lease,
1381
+ previousHead
1382
+ };
1383
+ this.candidates.set(lease.attempt, candidate);
1384
+ this.scheduleHeartbeat(candidate);
1385
+ return candidate;
1386
+ }
1387
+ async stageEntry(entry) {
1388
+ this.assertOpen();
1389
+ const candidate = await this.beginCandidate(
1390
+ entry.guid,
1391
+ entry.key
1392
+ );
1393
+ try {
1394
+ const staged = await this.entries.stage(entry);
1395
+ const entryCandidate = Object.assign(candidate, {
1396
+ staged
1397
+ });
1398
+ this.candidates.set(candidate.lease.attempt, entryCandidate);
1399
+ return entryCandidate;
1400
+ } catch (error) {
1401
+ const fence = await this.lifecycle.fail(candidate.lease, {
1402
+ code: "ddc-entry-stage-failed",
1403
+ detail: error instanceof Error ? error.message : String(error)
1404
+ });
1405
+ if (fence !== void 0) this.restoreFences.set(candidate, fence);
1406
+ this.stopHeartbeat(candidate);
1407
+ this.candidates.delete(candidate.lease.attempt);
1408
+ this.counters.writeFailureCount += 1;
1409
+ throw error;
1410
+ }
1411
+ }
1412
+ async inspect(guid, desiredKey) {
1413
+ this.assertOpen();
1414
+ const head = await this.lifecycle.inspect(guid, desiredKey);
1415
+ if (head.state === "current") this.counters.hitCount += 1;
1416
+ else this.counters.missCount += 1;
1417
+ return head;
1418
+ }
1419
+ async commitCandidate(candidate, validatedKey) {
1420
+ const registered = this.assertCandidate(candidate);
1421
+ try {
1422
+ const result = await this.lifecycle.commit(registered.lease, validatedKey);
1423
+ if (result.restoreFence !== void 0)
1424
+ this.restoreFences.set(registered, result.restoreFence);
1425
+ if (result.result === "invalid") this.counters.corruptCount += 1;
1426
+ this.stopHeartbeat(registered);
1427
+ this.candidates.delete(registered.lease.attempt);
1428
+ return result;
1429
+ } catch (error) {
1430
+ this.stopHeartbeat(registered);
1431
+ this.counters.writeFailureCount += 1;
1432
+ throw error;
1433
+ }
1434
+ }
1435
+ async commitEntry(candidate, validatedKey) {
1436
+ const registered = this.assertCandidate(candidate);
1437
+ try {
1438
+ await this.entries.publish(candidate.staged);
1439
+ const result = await this.lifecycle.commit(registered.lease, validatedKey);
1440
+ if (result.restoreFence !== void 0)
1441
+ this.restoreFences.set(registered, result.restoreFence);
1442
+ this.stopHeartbeat(registered);
1443
+ this.candidates.delete(registered.lease.attempt);
1444
+ return result;
1445
+ } catch (error) {
1446
+ this.stopHeartbeat(registered);
1447
+ const fence = await this.lifecycle.fail(registered.lease, {
1448
+ code: "ddc-entry-publish-failed",
1449
+ detail: error instanceof Error ? error.message : String(error)
1450
+ });
1451
+ if (fence !== void 0) this.restoreFences.set(registered, fence);
1452
+ await this.entries.discard(candidate.staged).catch(() => {
1453
+ });
1454
+ this.candidates.delete(registered.lease.attempt);
1455
+ this.counters.writeFailureCount += 1;
1456
+ throw error;
1457
+ }
1458
+ }
1459
+ async discardCandidate(candidate) {
1460
+ const registered = this.candidates.get(candidate.lease.attempt);
1461
+ if (registered === void 0) return;
1462
+ try {
1463
+ this.stopHeartbeat(registered);
1464
+ await this.lifecycle.discard(registered.lease);
1465
+ this.candidates.delete(registered.lease.attempt);
1466
+ } catch (error) {
1467
+ this.counters.writeFailureCount += 1;
1468
+ throw error;
1469
+ }
1470
+ }
1471
+ async discardEntry(candidate) {
1472
+ await Promise.all([this.discardCandidate(candidate), this.entries.discard(candidate.staged)]);
1473
+ }
1474
+ async restoreEntry(candidate) {
1475
+ const registered = this.candidates.get(candidate.lease.attempt);
1476
+ const ownerLease = registered?.lease ?? candidate.lease;
1477
+ const fence = this.restoreFences.get(candidate);
1478
+ this.stopHeartbeat(ownerLease);
1479
+ let result = {
1480
+ result: "not-owner",
1481
+ revision: candidate.previousHead.revision ?? 0,
1482
+ ...candidate.previousHead.generation === void 0 ? {} : { generation: candidate.previousHead.generation }
1483
+ };
1484
+ let restoreError;
1485
+ try {
1486
+ result = await this.lifecycle.restoreIfCurrent(candidate.previousHead, ownerLease, fence);
1487
+ } catch (error) {
1488
+ restoreError = error;
1489
+ } finally {
1490
+ await this.entries.discard(candidate.staged).catch(() => {
1491
+ });
1492
+ if (registered !== void 0) {
1493
+ this.candidates.delete(registered.lease.attempt);
1494
+ }
1495
+ }
1496
+ if (restoreError !== void 0) throw restoreError;
1497
+ return result;
1498
+ }
1499
+ metrics() {
1500
+ return { ...this.counters };
1501
+ }
1502
+ async close() {
1503
+ if (!this.accepting) return;
1504
+ this.accepting = false;
1505
+ for (const timer of this.heartbeatTimers.values()) clearTimeout(timer);
1506
+ this.heartbeatTimers.clear();
1507
+ const pending = [...this.candidates.values()];
1508
+ for (const candidate of pending) {
1509
+ try {
1510
+ await this.lifecycle.discard(candidate.lease);
1511
+ } catch {
1512
+ this.counters.writeFailureCount += 1;
1513
+ }
1514
+ }
1515
+ this.candidates.clear();
1516
+ }
1517
+ assertOpen() {
1518
+ if (!this.accepting) throw new Error("DDC generation session is closed");
1519
+ }
1520
+ assertCandidate(candidate) {
1521
+ this.assertOpen();
1522
+ const registered = this.candidates.get(candidate.lease.attempt);
1523
+ if (candidate.generation !== this.generation || registered === void 0) {
1524
+ throw new Error("DDC candidate belongs to another generation session");
1525
+ }
1526
+ return registered;
1527
+ }
1528
+ scheduleHeartbeat(candidate) {
1529
+ const attempt = candidate.lease.attempt;
1530
+ this.stopHeartbeat(candidate);
1531
+ const delay2 = Math.max(1, Math.floor((candidate.lease.expiresAt - Date.now()) / 2));
1532
+ const timer = setTimeout(() => {
1533
+ this.heartbeatTimers.delete(attempt);
1534
+ if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
1535
+ void this.lifecycle.heartbeat(candidate.lease).then((lease) => {
1536
+ if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
1537
+ candidate.lease = lease;
1538
+ this.scheduleHeartbeat(candidate);
1539
+ }).catch(() => {
1540
+ this.stopHeartbeat(candidate);
1541
+ });
1542
+ }, delay2);
1543
+ timer.unref?.();
1544
+ this.heartbeatTimers.set(attempt, timer);
1545
+ }
1546
+ stopHeartbeat(candidate) {
1547
+ const attempt = "lease" in candidate ? candidate.lease.attempt : candidate.attempt;
1548
+ const timer = this.heartbeatTimers.get(attempt);
1549
+ if (timer === void 0) return;
1550
+ clearTimeout(timer);
1551
+ this.heartbeatTimers.delete(attempt);
1552
+ }
1553
+ };
1554
+
1555
+ // src/status.ts
1556
+ var DDC_STATUS_SCHEMA = "forgeax-ddc-status/v2";
1557
+ function serializeDdcStatus(status) {
1558
+ return JSON.stringify(status);
1559
+ }
1560
+ function projectDdcStatusForBrowser(status) {
1561
+ return {
1562
+ schemaVersion: DDC_STATUS_SCHEMA,
1563
+ health: status.health,
1564
+ layers: status.layers.map((layer) => ({
1565
+ ...layer,
1566
+ actions: layer.actions.map(({ exactTarget: _exactTarget, ...action }) => action)
1567
+ }))
1568
+ };
1569
+ }
1570
+ var toBrowserDdcStatus = projectDdcStatusForBrowser;
1571
+
1572
+ 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 };
1573
+ //# sourceMappingURL=index.mjs.map
1574
+ //# sourceMappingURL=index.mjs.map