@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.
@@ -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.2",
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
+ }
@@ -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
- await mkdir(join(path, 'artifacts'), { recursive: true });
173
- await writeFile(join(path, 'payload.json'), canonicalDdcJson(entry.payload));
174
- await writeFile(join(path, 'refs.json'), canonicalDdcJson(entry.refs));
175
- await writeFile(join(path, 'receipt.json'), canonicalDdcJson(entry.receipt));
176
- const artifacts: Record<string, { mediaType: string; file: string }> = {};
177
- for (const [key, artifact] of sortedArtifacts(entry)) {
178
- const file = artifactFile(key);
179
- artifacts[key] = { mediaType: artifact.mediaType, file };
180
- await writeFile(join(path, 'artifacts', `${file}.bin`), artifact.bytes);
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
- try {
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
- const existing = await this.readDirectory(target, false);
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',