@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/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/generation-session.integration.test.d.ts +2 -0
- package/dist/__tests__/generation-session.integration.test.d.ts.map +1 -0
- package/dist/__tests__/generation-session.unit.test.d.ts +2 -0
- package/dist/__tests__/generation-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
- package/dist/build-cache.d.ts +18 -0
- package/dist/build-cache.d.ts.map +1 -0
- package/dist/entry-store.d.ts +1 -0
- package/dist/entry-store.d.ts.map +1 -1
- package/dist/entry-store.mjs +28 -18
- package/dist/entry-store.mjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +615 -57
- package/dist/index.mjs.map +1 -1
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/publication.d.ts +50 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/session.d.ts +45 -0
- package/dist/session.d.ts.map +1 -0
- package/package.json +4 -1
- package/src/__tests__/generation-session.integration.test.ts +74 -0
- package/src/__tests__/generation-session.unit.test.ts +35 -0
- package/src/__tests__/owner-chain.integration.test.ts +45 -0
- package/src/build-cache.ts +60 -0
- package/src/entry-store.ts +27 -17
- package/src/index.ts +24 -1
- package/src/lifecycle.ts +38 -0
- package/src/publication.ts +444 -0
- package/src/session.ts +254 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DdcEntry, StagedDdcEntry } from './entry-store.js';
|
|
2
|
+
import type { DdcCommitResult, DdcHead, DdcLease } from './lifecycle.js';
|
|
3
|
+
export interface DdcSessionOptions {
|
|
4
|
+
readonly generation: number;
|
|
5
|
+
readonly leaseTtlMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DdcGenerationCandidate {
|
|
8
|
+
readonly generation: number;
|
|
9
|
+
readonly lease: DdcLease;
|
|
10
|
+
readonly previousHead: DdcHead;
|
|
11
|
+
}
|
|
12
|
+
export interface DdcGenerationEntryCandidate extends DdcGenerationCandidate {
|
|
13
|
+
readonly staged: StagedDdcEntry;
|
|
14
|
+
}
|
|
15
|
+
export interface DdcSessionMetrics {
|
|
16
|
+
readonly hitCount: number;
|
|
17
|
+
readonly missCount: number;
|
|
18
|
+
readonly corruptCount: number;
|
|
19
|
+
readonly writeFailureCount: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class DdcGenerationSession {
|
|
22
|
+
readonly generation: number;
|
|
23
|
+
private readonly lifecycle;
|
|
24
|
+
private readonly entries;
|
|
25
|
+
private readonly candidates;
|
|
26
|
+
private readonly heartbeatTimers;
|
|
27
|
+
private readonly counters;
|
|
28
|
+
private accepting;
|
|
29
|
+
constructor(root: string, options: DdcSessionOptions);
|
|
30
|
+
beginCandidate(guid: string, desiredKey: string): Promise<DdcGenerationCandidate>;
|
|
31
|
+
stageEntry(entry: DdcEntry): Promise<DdcGenerationEntryCandidate>;
|
|
32
|
+
inspect(guid: string, desiredKey: string): Promise<DdcHead>;
|
|
33
|
+
commitCandidate(candidate: DdcGenerationCandidate, validatedKey: string): Promise<DdcCommitResult>;
|
|
34
|
+
commitEntry(candidate: DdcGenerationEntryCandidate, validatedKey: string): Promise<DdcCommitResult>;
|
|
35
|
+
discardCandidate(candidate: DdcGenerationCandidate): Promise<void>;
|
|
36
|
+
discardEntry(candidate: DdcGenerationEntryCandidate): Promise<void>;
|
|
37
|
+
restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<void>;
|
|
38
|
+
metrics(): DdcSessionMetrics;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
private assertOpen;
|
|
41
|
+
private assertCandidate;
|
|
42
|
+
private scheduleHeartbeat;
|
|
43
|
+
private stopHeartbeat;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAGzE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAmBD,qBAAa,oBAAoB;IAC/B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoD;IAC/E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAKvB;IACF,OAAO,CAAC,SAAS,CAAQ;gBAEN,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB;IAY9C,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAajF,UAAU,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAyBjE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ3D,eAAe,CAC1B,SAAS,EAAE,sBAAsB,EACjC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,CAAC;IAed,WAAW,CACtB,SAAS,EAAE,2BAA2B,EACtC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,eAAe,CAAC;IAqBd,gBAAgB,CAAC,SAAS,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAalE,YAAY,CAAC,SAAS,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAInE,YAAY,CAAC,SAAS,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBzE,OAAO,IAAI,iBAAiB;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBnC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,aAAa;CAMtB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-ddc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -84,6 +84,9 @@
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"@forgeax/engine-types": "0.1.4"
|
|
89
|
+
},
|
|
87
90
|
"devDependencies": {
|
|
88
91
|
"@types/node": "^20.14.0",
|
|
89
92
|
"tsup": "^8.3.5",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { type DdcEntry, DdcEntryStore, ddcOutputDigest } from '../entry-store.js';
|
|
6
|
+
import { DdcGenerationSession } from '../session.js';
|
|
7
|
+
|
|
8
|
+
const GUID = '019e3969-1d48-7c3b-ac24-6d68f457065f';
|
|
9
|
+
const KEY = 'a'.repeat(64);
|
|
10
|
+
|
|
11
|
+
function entry(): DdcEntry {
|
|
12
|
+
const value = {
|
|
13
|
+
key: KEY,
|
|
14
|
+
guid: GUID,
|
|
15
|
+
payload: { kind: 'fixture' },
|
|
16
|
+
refs: [],
|
|
17
|
+
artifacts: {},
|
|
18
|
+
receipt: {
|
|
19
|
+
guid: GUID,
|
|
20
|
+
key: KEY,
|
|
21
|
+
producer: 'fixture',
|
|
22
|
+
inputFingerprint: 'sha256:source',
|
|
23
|
+
outputDigest: '',
|
|
24
|
+
},
|
|
25
|
+
} satisfies DdcEntry;
|
|
26
|
+
return { ...value, receipt: { ...value.receipt, outputDigest: ddcOutputDigest(value) } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('DDC generation session integration', () => {
|
|
30
|
+
const roots: string[] = [];
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('accepts one validated candidate and keeps a later generation isolated', async () => {
|
|
37
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-ddc-session-'));
|
|
38
|
+
roots.push(root);
|
|
39
|
+
const store = new DdcEntryStore(root);
|
|
40
|
+
await store.write(entry());
|
|
41
|
+
|
|
42
|
+
const first = new DdcGenerationSession(root, { generation: 1 });
|
|
43
|
+
const candidate = await first.beginCandidate(GUID, KEY);
|
|
44
|
+
await expect(first.commitCandidate(candidate, KEY)).resolves.toMatchObject({
|
|
45
|
+
result: 'current',
|
|
46
|
+
});
|
|
47
|
+
await expect(first.inspect(GUID, KEY)).resolves.toMatchObject({
|
|
48
|
+
state: 'current',
|
|
49
|
+
currentKey: KEY,
|
|
50
|
+
});
|
|
51
|
+
await first.close();
|
|
52
|
+
|
|
53
|
+
const second = new DdcGenerationSession(root, { generation: 2 });
|
|
54
|
+
expect(second.metrics().hitCount).toBe(0);
|
|
55
|
+
await expect(second.inspect(GUID, KEY)).resolves.toMatchObject({ state: 'current' });
|
|
56
|
+
expect(second.metrics().hitCount).toBe(1);
|
|
57
|
+
await second.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('heartbeats staged candidates during a long generation before commit', async () => {
|
|
61
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-ddc-session-'));
|
|
62
|
+
roots.push(root);
|
|
63
|
+
const session = new DdcGenerationSession(root, { generation: 1, leaseTtlMs: 100 });
|
|
64
|
+
try {
|
|
65
|
+
const candidate = await session.stageEntry(entry());
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
67
|
+
await expect(session.commitEntry(candidate, KEY)).resolves.toMatchObject({
|
|
68
|
+
result: 'current',
|
|
69
|
+
});
|
|
70
|
+
} finally {
|
|
71
|
+
await session.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { DdcGenerationSession } from '../session.js';
|
|
6
|
+
|
|
7
|
+
describe('DDC generation session contract', () => {
|
|
8
|
+
const roots: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('fences candidates to one generation and exposes bounded session metrics', async () => {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-ddc-session-'));
|
|
16
|
+
roots.push(root);
|
|
17
|
+
const session = new DdcGenerationSession(root, { generation: 7 });
|
|
18
|
+
|
|
19
|
+
const candidate = await session.beginCandidate('asset-a', 'a'.repeat(64));
|
|
20
|
+
expect(candidate.generation).toBe(7);
|
|
21
|
+
expect(candidate.lease.guid).toBe('asset-a');
|
|
22
|
+
expect(session.metrics()).toEqual({
|
|
23
|
+
hitCount: 0,
|
|
24
|
+
missCount: 1,
|
|
25
|
+
corruptCount: 0,
|
|
26
|
+
writeFailureCount: 0,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await session.discardCandidate(candidate);
|
|
30
|
+
await session.close();
|
|
31
|
+
await expect(session.beginCandidate('asset-b', 'b'.repeat(64))).rejects.toThrow(
|
|
32
|
+
'DDC generation session is closed',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { type DdcEntry, DdcEntryStore, ddcOutputDigest } from '../entry-store.js';
|
|
6
|
+
import { DdcGenerationSession } from '../session.js';
|
|
7
|
+
|
|
8
|
+
const GUID = '019e3969-1d48-7c3b-ac24-6d68f457065f';
|
|
9
|
+
const KEY = 'a'.repeat(64);
|
|
10
|
+
|
|
11
|
+
describe('DDC owner chain', () => {
|
|
12
|
+
const roots: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts only the validated entry for the current candidate', async () => {
|
|
19
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-owner-chain-'));
|
|
20
|
+
roots.push(root);
|
|
21
|
+
const value = {
|
|
22
|
+
key: KEY,
|
|
23
|
+
guid: GUID,
|
|
24
|
+
payload: { kind: 'texture' },
|
|
25
|
+
refs: [],
|
|
26
|
+
artifacts: {},
|
|
27
|
+
receipt: {
|
|
28
|
+
guid: GUID,
|
|
29
|
+
key: KEY,
|
|
30
|
+
producer: 'owner-chain',
|
|
31
|
+
inputFingerprint: 'sha256:source',
|
|
32
|
+
outputDigest: '',
|
|
33
|
+
},
|
|
34
|
+
} satisfies DdcEntry;
|
|
35
|
+
const entry = { ...value, receipt: { ...value.receipt, outputDigest: ddcOutputDigest(value) } };
|
|
36
|
+
await new DdcEntryStore(root).write(entry);
|
|
37
|
+
const session = new DdcGenerationSession(root, { generation: 3 });
|
|
38
|
+
const candidate = await session.beginCandidate(GUID, KEY);
|
|
39
|
+
await expect(session.commitCandidate(candidate, KEY)).resolves.toMatchObject({
|
|
40
|
+
result: 'current',
|
|
41
|
+
});
|
|
42
|
+
await expect(session.inspect(GUID, KEY)).resolves.toMatchObject({ state: 'current' });
|
|
43
|
+
await session.close();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { semanticDdcKey as ownerSemanticDdcKey } from './key.js';
|
|
4
|
+
|
|
5
|
+
export interface BuildDdcInput {
|
|
6
|
+
readonly schemaVersion: string;
|
|
7
|
+
readonly importerVersion: string;
|
|
8
|
+
readonly codecVersion: string;
|
|
9
|
+
readonly sourceDependencies: readonly (
|
|
10
|
+
| string
|
|
11
|
+
| { readonly path: string; readonly digest: string }
|
|
12
|
+
)[];
|
|
13
|
+
readonly settings: unknown;
|
|
14
|
+
readonly declaredGuids: readonly string[];
|
|
15
|
+
readonly cookProfile: string;
|
|
16
|
+
readonly sourceOverrides?: unknown;
|
|
17
|
+
readonly publish?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveDdcRoot(cwd: string): string {
|
|
21
|
+
let current = resolve(cwd);
|
|
22
|
+
while (true) {
|
|
23
|
+
if (existsSync(join(current, 'pnpm-workspace.yaml'))) {
|
|
24
|
+
return join(current, 'node_modules/.cache/forgeax-ddc');
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current) return join(resolve(cwd), 'node_modules/.cache/forgeax-ddc');
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Derive the build key while keeping publish URLs out of the semantic identity. */
|
|
33
|
+
export function semanticBuildKey(input: BuildDdcInput): string {
|
|
34
|
+
const hasSourceOverrides =
|
|
35
|
+
input.sourceOverrides !== undefined &&
|
|
36
|
+
typeof input.sourceOverrides === 'object' &&
|
|
37
|
+
input.sourceOverrides !== null &&
|
|
38
|
+
!Array.isArray(input.sourceOverrides) &&
|
|
39
|
+
Object.keys(input.sourceOverrides).length > 0;
|
|
40
|
+
const settings = hasSourceOverrides
|
|
41
|
+
? { settings: input.settings, sourceOverrides: input.sourceOverrides }
|
|
42
|
+
: input.settings;
|
|
43
|
+
return ownerSemanticDdcKey({
|
|
44
|
+
schemaVersion: input.schemaVersion,
|
|
45
|
+
importer: input.importerVersion,
|
|
46
|
+
codec: input.codecVersion,
|
|
47
|
+
settings,
|
|
48
|
+
sourceBytes: input.sourceDependencies
|
|
49
|
+
.map((dependency) =>
|
|
50
|
+
typeof dependency === 'string'
|
|
51
|
+
? dependency.replaceAll('\\', '/').replace(/^.*\/(assets\/)/, '$1')
|
|
52
|
+
: dependency.digest,
|
|
53
|
+
)
|
|
54
|
+
.sort()
|
|
55
|
+
.map((digest) => new TextEncoder().encode(digest)),
|
|
56
|
+
declaredGuids: input.declaredGuids,
|
|
57
|
+
targetProfile: input.cookProfile,
|
|
58
|
+
producer: input.importerVersion,
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/entry-store.ts
CHANGED
|
@@ -169,19 +169,24 @@ export class DdcEntryStore {
|
|
|
169
169
|
throw new DdcStoreError('ddc-entry-invalid', 'receipt output digest does not match entry');
|
|
170
170
|
}
|
|
171
171
|
const path = join(this.staging, `${entry.key}-${randomUUID()}`);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
try {
|
|
173
|
+
await mkdir(join(path, 'artifacts'), { recursive: true });
|
|
174
|
+
await writeFile(join(path, 'payload.json'), canonicalDdcJson(entry.payload));
|
|
175
|
+
await writeFile(join(path, 'refs.json'), canonicalDdcJson(entry.refs));
|
|
176
|
+
await writeFile(join(path, 'receipt.json'), canonicalDdcJson(entry.receipt));
|
|
177
|
+
const artifacts: Record<string, { mediaType: string; file: string }> = {};
|
|
178
|
+
for (const [key, artifact] of sortedArtifacts(entry)) {
|
|
179
|
+
const file = artifactFile(key);
|
|
180
|
+
artifacts[key] = { mediaType: artifact.mediaType, file };
|
|
181
|
+
await writeFile(join(path, 'artifacts', `${file}.bin`), artifact.bytes);
|
|
182
|
+
}
|
|
183
|
+
await writeFile(join(path, 'artifacts.json'), canonicalDdcJson(artifacts));
|
|
184
|
+
await writeFile(join(path, 'integrity.json'), canonicalDdcJson(integrityFor(entry)));
|
|
185
|
+
return { key: entry.key, path };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await rm(path, { recursive: true, force: true }).catch(() => {});
|
|
188
|
+
throw error;
|
|
181
189
|
}
|
|
182
|
-
await writeFile(join(path, 'artifacts.json'), canonicalDdcJson(artifacts));
|
|
183
|
-
await writeFile(join(path, 'integrity.json'), canonicalDdcJson(integrityFor(entry)));
|
|
184
|
-
return { key: entry.key, path };
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
public async publish(staged: StagedDdcEntry): Promise<PublishDdcEntryResult> {
|
|
@@ -189,11 +194,14 @@ export class DdcEntryStore {
|
|
|
189
194
|
const candidate = await this.readDirectory(staged.path, true);
|
|
190
195
|
const target = join(this.entries, staged.key);
|
|
191
196
|
await mkdir(this.entries, { recursive: true });
|
|
192
|
-
|
|
193
|
-
await stat(target);
|
|
197
|
+
const useExisting = async (): Promise<PublishDdcEntryResult> => {
|
|
194
198
|
const existing = await this.readDirectory(target, false);
|
|
195
199
|
await rm(staged.path, { recursive: true, force: true });
|
|
196
200
|
return existingResult(existing, candidate, staged.key);
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
await stat(target);
|
|
204
|
+
return useExisting();
|
|
197
205
|
} catch (error) {
|
|
198
206
|
if (error instanceof DdcStoreError) throw error;
|
|
199
207
|
if (errorCode(error) !== 'ENOENT') throw error;
|
|
@@ -203,12 +211,14 @@ export class DdcEntryStore {
|
|
|
203
211
|
return { result: 'published', key: staged.key };
|
|
204
212
|
} catch (error) {
|
|
205
213
|
if (!['EEXIST', 'ENOTEMPTY', 'EISDIR'].includes(errorCode(error) ?? '')) throw error;
|
|
206
|
-
|
|
207
|
-
await rm(staged.path, { recursive: true, force: true });
|
|
208
|
-
return existingResult(existing, candidate, staged.key);
|
|
214
|
+
return useExisting();
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
public async discard(staged: StagedDdcEntry): Promise<void> {
|
|
219
|
+
await rm(staged.path, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
212
222
|
public async write(entry: DdcEntry): Promise<PublishDdcEntryResult> {
|
|
213
223
|
const staged = await this.stage(entry);
|
|
214
224
|
return this.publish(staged);
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type BuildDdcInput,
|
|
3
|
+
resolveDdcRoot,
|
|
4
|
+
semanticBuildKey,
|
|
5
|
+
} from './build-cache.js';
|
|
1
6
|
export {
|
|
2
7
|
type DdcArtifact,
|
|
3
8
|
type DdcEntry,
|
|
4
9
|
DdcEntryStore,
|
|
5
10
|
type DdcReceipt,
|
|
11
|
+
DdcStoreError,
|
|
6
12
|
ddcOutputDigest,
|
|
7
13
|
type PublishDdcEntryResult,
|
|
8
14
|
type StagedDdcEntry,
|
|
@@ -15,7 +21,6 @@ export {
|
|
|
15
21
|
type DdcErrorCode,
|
|
16
22
|
type DdcErrorRootKind,
|
|
17
23
|
type DdcRecoveryAction as DdcErrorRecoveryAction,
|
|
18
|
-
DdcStoreError,
|
|
19
24
|
type DdcStoreErrorInit,
|
|
20
25
|
} from './errors.js';
|
|
21
26
|
export { collectDdcGarbage, type DdcGcInput, type DdcGcResult } from './gc.js';
|
|
@@ -42,12 +47,30 @@ export {
|
|
|
42
47
|
DdcLifecycle,
|
|
43
48
|
type DdcLifecycleState,
|
|
44
49
|
} from './lifecycle.js';
|
|
50
|
+
export {
|
|
51
|
+
type AcceptedPublicationCandidate,
|
|
52
|
+
type AcceptedPublicationStore,
|
|
53
|
+
createAcceptedPublication,
|
|
54
|
+
createAcceptedPublicationStore,
|
|
55
|
+
type ScriptablePackPublicationBuildInput,
|
|
56
|
+
type ScriptablePackPublicationError,
|
|
57
|
+
type ScriptablePackPublicationSnapshot,
|
|
58
|
+
scriptablePackOutputSetDigest,
|
|
59
|
+
scriptablePackPublicationGeneration,
|
|
60
|
+
} from './publication.js';
|
|
45
61
|
export {
|
|
46
62
|
assertRuntimeScope,
|
|
47
63
|
createRuntimeScope,
|
|
48
64
|
type DdcRuntimeScope,
|
|
49
65
|
runtimeScopeHash,
|
|
50
66
|
} from './runtime-scope.js';
|
|
67
|
+
export {
|
|
68
|
+
type DdcGenerationCandidate,
|
|
69
|
+
type DdcGenerationEntryCandidate,
|
|
70
|
+
DdcGenerationSession,
|
|
71
|
+
type DdcSessionMetrics,
|
|
72
|
+
type DdcSessionOptions,
|
|
73
|
+
} from './session.js';
|
|
51
74
|
export {
|
|
52
75
|
type BrowserDdcRecoveryAction,
|
|
53
76
|
type BrowserDdcStatus,
|
package/src/lifecycle.ts
CHANGED
|
@@ -464,6 +464,44 @@ export class DdcLifecycle {
|
|
|
464
464
|
});
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
public async discard(lease: DdcLease): Promise<void> {
|
|
468
|
+
await withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
469
|
+
const current = await this.read(lease.guid);
|
|
470
|
+
if (current?.active?.attempt !== lease.attempt) return;
|
|
471
|
+
const { active: _active, ...withoutActive } = current;
|
|
472
|
+
await this.write({
|
|
473
|
+
...withoutActive,
|
|
474
|
+
revision: current.revision + 1,
|
|
475
|
+
generation: lease.generation,
|
|
476
|
+
stale: current.currentKey === undefined,
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Restore the last accepted head after a failed multi-owner generation commit. */
|
|
482
|
+
public async restore(head: DdcHead): Promise<void> {
|
|
483
|
+
await withDdcLock(this.root, `head-${head.guid}`, async () => {
|
|
484
|
+
const path = headFile(this.heads, head.guid);
|
|
485
|
+
if (head.state === 'missing') {
|
|
486
|
+
await rm(path, { force: true });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await this.write({
|
|
490
|
+
guid: head.guid,
|
|
491
|
+
desiredKey: head.desiredKey,
|
|
492
|
+
revision: head.revision ?? 0,
|
|
493
|
+
...(head.currentKey === undefined ? {} : { currentKey: head.currentKey }),
|
|
494
|
+
...(head.lastKnownGoodKey === undefined ? {} : { lastKnownGoodKey: head.lastKnownGoodKey }),
|
|
495
|
+
...(head.generation === undefined ? {} : { generation: head.generation }),
|
|
496
|
+
...(head.activeLease === undefined ? {} : { active: head.activeLease }),
|
|
497
|
+
...(head.state === 'stale' ? { stale: true } : {}),
|
|
498
|
+
...(head.failure === undefined
|
|
499
|
+
? {}
|
|
500
|
+
: { failure: { desiredKey: head.desiredKey, ...head.failure } }),
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
467
505
|
public async revoke(lease: DdcLease): Promise<void> {
|
|
468
506
|
await this.fail(lease, {
|
|
469
507
|
code: 'lease-lost',
|