@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.
- package/LICENSE +202 -0
- package/README.md +147 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/concurrency.integration.test.d.ts +2 -0
- package/dist/__tests__/concurrency.integration.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-path.integration.test.d.ts +2 -0
- package/dist/__tests__/consumer-path.integration.test.d.ts.map +1 -0
- package/dist/__tests__/crash-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/crash-recovery.integration.test.d.ts.map +1 -0
- package/dist/__tests__/entry-store.integration.test.d.ts +2 -0
- package/dist/__tests__/entry-store.integration.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- 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__/key.unit.test.d.ts +2 -0
- package/dist/__tests__/key.unit.test.d.ts.map +1 -0
- package/dist/__tests__/layout.unit.test.d.ts +2 -0
- package/dist/__tests__/layout.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/multiprocess-gc.integration.test.d.ts +2 -0
- package/dist/__tests__/multiprocess-gc.integration.test.d.ts.map +1 -0
- package/dist/__tests__/multiprocess-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/multiprocess-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/multiprocess-worker.d.ts +2 -0
- package/dist/__tests__/multiprocess-worker.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/__tests__/status-root-kind-owner.test-d.d.ts +2 -0
- package/dist/__tests__/status-root-kind-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/status.unit.test.d.ts +2 -0
- package/dist/__tests__/status.unit.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 +53 -0
- package/dist/entry-store.d.ts.map +1 -0
- package/dist/entry-store.mjs +349 -0
- package/dist/entry-store.mjs.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +109 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1574 -0
- package/dist/index.mjs.map +1 -0
- package/dist/key.d.ts +13 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/key.mjs +38 -0
- package/dist/key.mjs.map +1 -0
- package/dist/layout.d.ts +51 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.mjs +68 -0
- package/dist/layout.mjs.map +1 -0
- package/dist/lifecycle.d.ts +93 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/publication.d.ts +50 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/runtime-scope.d.ts +10 -0
- package/dist/runtime-scope.d.ts.map +1 -0
- package/dist/session.d.ts +46 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/status.d.ts +39 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.mjs +20 -0
- package/dist/status.mjs.map +1 -0
- package/package.json +101 -0
- package/src/__tests__/concurrency.integration.test.ts +53 -0
- package/src/__tests__/consumer-path.integration.test.ts +61 -0
- package/src/__tests__/crash-recovery.integration.test.ts +84 -0
- package/src/__tests__/entry-store.integration.test.ts +83 -0
- package/src/__tests__/errors.unit.test.ts +54 -0
- package/src/__tests__/generation-session.integration.test.ts +202 -0
- package/src/__tests__/generation-session.unit.test.ts +35 -0
- package/src/__tests__/key.unit.test.ts +49 -0
- package/src/__tests__/layout.unit.test.ts +50 -0
- package/src/__tests__/lifecycle.unit.test.ts +258 -0
- package/src/__tests__/multiprocess-gc.integration.test.ts +36 -0
- package/src/__tests__/multiprocess-lifecycle.integration.test.ts +129 -0
- package/src/__tests__/multiprocess-worker.ts +41 -0
- package/src/__tests__/owner-chain.integration.test.ts +45 -0
- package/src/__tests__/status-root-kind-owner.test-d.ts +42 -0
- package/src/__tests__/status.unit.test.ts +56 -0
- package/src/build-cache.ts +60 -0
- package/src/entry-store.ts +317 -0
- package/src/errors.ts +164 -0
- package/src/gc.ts +34 -0
- package/src/index.ts +92 -0
- package/src/key.ts +46 -0
- package/src/layout.ts +132 -0
- package/src/lifecycle.ts +792 -0
- package/src/publication.ts +446 -0
- package/src/runtime-scope.ts +87 -0
- package/src/session.ts +281 -0
- package/src/status.ts +58 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { DdcEntry, StagedDdcEntry } from './entry-store.js';
|
|
2
|
+
import { DdcEntryStore } from './entry-store.js';
|
|
3
|
+
import type {
|
|
4
|
+
DdcCommitResult,
|
|
5
|
+
DdcHead,
|
|
6
|
+
DdcLease,
|
|
7
|
+
DdcRestoreFence,
|
|
8
|
+
DdcRestoreResult,
|
|
9
|
+
DdcRollbackSnapshot,
|
|
10
|
+
} from './lifecycle.js';
|
|
11
|
+
import { DdcLifecycle } from './lifecycle.js';
|
|
12
|
+
|
|
13
|
+
export interface DdcSessionOptions {
|
|
14
|
+
readonly generation: number;
|
|
15
|
+
readonly leaseTtlMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DdcGenerationCandidate {
|
|
19
|
+
readonly generation: number;
|
|
20
|
+
readonly lease: DdcLease;
|
|
21
|
+
readonly previousHead: DdcRollbackSnapshot;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DdcGenerationEntryCandidate extends DdcGenerationCandidate {
|
|
25
|
+
readonly staged: StagedDdcEntry;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DdcSessionMetrics {
|
|
29
|
+
readonly hitCount: number;
|
|
30
|
+
readonly missCount: number;
|
|
31
|
+
readonly corruptCount: number;
|
|
32
|
+
readonly writeFailureCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type MutableDdcGenerationCandidate = Omit<DdcGenerationCandidate, 'lease'> & {
|
|
36
|
+
lease: DdcLease;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type MutableDdcGenerationEntryCandidate = Omit<DdcGenerationEntryCandidate, 'lease'> & {
|
|
40
|
+
lease: DdcLease;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type HeartbeatTimer = ReturnType<typeof setTimeout>;
|
|
44
|
+
|
|
45
|
+
interface MutableMetrics {
|
|
46
|
+
hitCount: number;
|
|
47
|
+
missCount: number;
|
|
48
|
+
corruptCount: number;
|
|
49
|
+
writeFailureCount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class DdcGenerationSession {
|
|
53
|
+
public readonly generation: number;
|
|
54
|
+
private readonly lifecycle: DdcLifecycle;
|
|
55
|
+
private readonly entries: DdcEntryStore;
|
|
56
|
+
private readonly candidates = new Map<string, MutableDdcGenerationCandidate>();
|
|
57
|
+
private readonly restoreFences = new WeakMap<DdcGenerationCandidate, DdcRestoreFence>();
|
|
58
|
+
private readonly heartbeatTimers = new Map<string, HeartbeatTimer>();
|
|
59
|
+
private readonly counters: MutableMetrics = {
|
|
60
|
+
hitCount: 0,
|
|
61
|
+
missCount: 0,
|
|
62
|
+
corruptCount: 0,
|
|
63
|
+
writeFailureCount: 0,
|
|
64
|
+
};
|
|
65
|
+
private accepting = true;
|
|
66
|
+
|
|
67
|
+
public constructor(root: string, options: DdcSessionOptions) {
|
|
68
|
+
if (!Number.isSafeInteger(options.generation) || options.generation < 1) {
|
|
69
|
+
throw new TypeError('DDC generation must be a positive safe integer');
|
|
70
|
+
}
|
|
71
|
+
this.generation = options.generation;
|
|
72
|
+
this.lifecycle = new DdcLifecycle(
|
|
73
|
+
root,
|
|
74
|
+
options.leaseTtlMs === undefined ? undefined : { leaseTtlMs: options.leaseTtlMs },
|
|
75
|
+
);
|
|
76
|
+
this.entries = new DdcEntryStore(root);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async beginCandidate(guid: string, desiredKey: string): Promise<DdcGenerationCandidate> {
|
|
80
|
+
this.assertOpen();
|
|
81
|
+
const { lease, previousHead } = await this.lifecycle.beginWithSnapshot(guid, desiredKey);
|
|
82
|
+
if (previousHead.state === 'current') this.counters.hitCount += 1;
|
|
83
|
+
else this.counters.missCount += 1;
|
|
84
|
+
const candidate: MutableDdcGenerationCandidate = {
|
|
85
|
+
generation: this.generation,
|
|
86
|
+
lease,
|
|
87
|
+
previousHead,
|
|
88
|
+
};
|
|
89
|
+
this.candidates.set(lease.attempt, candidate);
|
|
90
|
+
this.scheduleHeartbeat(candidate);
|
|
91
|
+
return candidate;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async stageEntry(entry: DdcEntry): Promise<DdcGenerationEntryCandidate> {
|
|
95
|
+
this.assertOpen();
|
|
96
|
+
const candidate = (await this.beginCandidate(
|
|
97
|
+
entry.guid,
|
|
98
|
+
entry.key,
|
|
99
|
+
)) as MutableDdcGenerationCandidate;
|
|
100
|
+
try {
|
|
101
|
+
const staged = await this.entries.stage(entry);
|
|
102
|
+
const entryCandidate: MutableDdcGenerationEntryCandidate = Object.assign(candidate, {
|
|
103
|
+
staged,
|
|
104
|
+
});
|
|
105
|
+
this.candidates.set(candidate.lease.attempt, entryCandidate);
|
|
106
|
+
return entryCandidate;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const fence = await this.lifecycle.fail(candidate.lease, {
|
|
109
|
+
code: 'ddc-entry-stage-failed',
|
|
110
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
111
|
+
});
|
|
112
|
+
if (fence !== undefined) this.restoreFences.set(candidate, fence);
|
|
113
|
+
this.stopHeartbeat(candidate);
|
|
114
|
+
this.candidates.delete(candidate.lease.attempt);
|
|
115
|
+
this.counters.writeFailureCount += 1;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async inspect(guid: string, desiredKey: string): Promise<DdcHead> {
|
|
121
|
+
this.assertOpen();
|
|
122
|
+
const head = await this.lifecycle.inspect(guid, desiredKey);
|
|
123
|
+
if (head.state === 'current') this.counters.hitCount += 1;
|
|
124
|
+
else this.counters.missCount += 1;
|
|
125
|
+
return head;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async commitCandidate(
|
|
129
|
+
candidate: DdcGenerationCandidate,
|
|
130
|
+
validatedKey: string,
|
|
131
|
+
): Promise<DdcCommitResult> {
|
|
132
|
+
const registered = this.assertCandidate(candidate);
|
|
133
|
+
try {
|
|
134
|
+
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
135
|
+
if (result.restoreFence !== undefined)
|
|
136
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
137
|
+
if (result.result === 'invalid') this.counters.corruptCount += 1;
|
|
138
|
+
this.stopHeartbeat(registered);
|
|
139
|
+
this.candidates.delete(registered.lease.attempt);
|
|
140
|
+
return result;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.stopHeartbeat(registered);
|
|
143
|
+
this.counters.writeFailureCount += 1;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public async commitEntry(
|
|
149
|
+
candidate: DdcGenerationEntryCandidate,
|
|
150
|
+
validatedKey: string,
|
|
151
|
+
): Promise<DdcCommitResult> {
|
|
152
|
+
const registered = this.assertCandidate(candidate);
|
|
153
|
+
try {
|
|
154
|
+
await this.entries.publish(candidate.staged);
|
|
155
|
+
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
156
|
+
if (result.restoreFence !== undefined)
|
|
157
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
158
|
+
this.stopHeartbeat(registered);
|
|
159
|
+
this.candidates.delete(registered.lease.attempt);
|
|
160
|
+
return result;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.stopHeartbeat(registered);
|
|
163
|
+
const fence = await this.lifecycle.fail(registered.lease, {
|
|
164
|
+
code: 'ddc-entry-publish-failed',
|
|
165
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
166
|
+
});
|
|
167
|
+
if (fence !== undefined) this.restoreFences.set(registered, fence);
|
|
168
|
+
await this.entries.discard(candidate.staged).catch(() => {});
|
|
169
|
+
this.candidates.delete(registered.lease.attempt);
|
|
170
|
+
this.counters.writeFailureCount += 1;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public async discardCandidate(candidate: DdcGenerationCandidate): Promise<void> {
|
|
176
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
177
|
+
if (registered === undefined) return;
|
|
178
|
+
try {
|
|
179
|
+
this.stopHeartbeat(registered);
|
|
180
|
+
await this.lifecycle.discard(registered.lease);
|
|
181
|
+
this.candidates.delete(registered.lease.attempt);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.counters.writeFailureCount += 1;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public async discardEntry(candidate: DdcGenerationEntryCandidate): Promise<void> {
|
|
189
|
+
await Promise.all([this.discardCandidate(candidate), this.entries.discard(candidate.staged)]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public async restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<DdcRestoreResult> {
|
|
193
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
194
|
+
const ownerLease = registered?.lease ?? candidate.lease;
|
|
195
|
+
const fence = this.restoreFences.get(candidate);
|
|
196
|
+
this.stopHeartbeat(ownerLease);
|
|
197
|
+
let result: DdcRestoreResult = {
|
|
198
|
+
result: 'not-owner',
|
|
199
|
+
revision: candidate.previousHead.revision ?? 0,
|
|
200
|
+
...(candidate.previousHead.generation === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { generation: candidate.previousHead.generation }),
|
|
203
|
+
};
|
|
204
|
+
let restoreError: unknown;
|
|
205
|
+
try {
|
|
206
|
+
result = await this.lifecycle.restoreIfCurrent(candidate.previousHead, ownerLease, fence);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
restoreError = error;
|
|
209
|
+
} finally {
|
|
210
|
+
await this.entries.discard(candidate.staged).catch(() => {});
|
|
211
|
+
if (registered !== undefined) {
|
|
212
|
+
this.candidates.delete(registered.lease.attempt);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (restoreError !== undefined) throw restoreError;
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public metrics(): DdcSessionMetrics {
|
|
220
|
+
return { ...this.counters };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public async close(): Promise<void> {
|
|
224
|
+
if (!this.accepting) return;
|
|
225
|
+
this.accepting = false;
|
|
226
|
+
for (const timer of this.heartbeatTimers.values()) clearTimeout(timer);
|
|
227
|
+
this.heartbeatTimers.clear();
|
|
228
|
+
const pending = [...this.candidates.values()];
|
|
229
|
+
for (const candidate of pending) {
|
|
230
|
+
try {
|
|
231
|
+
await this.lifecycle.discard(candidate.lease);
|
|
232
|
+
} catch {
|
|
233
|
+
this.counters.writeFailureCount += 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.candidates.clear();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private assertOpen(): void {
|
|
240
|
+
if (!this.accepting) throw new Error('DDC generation session is closed');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private assertCandidate(candidate: DdcGenerationCandidate): MutableDdcGenerationCandidate {
|
|
244
|
+
this.assertOpen();
|
|
245
|
+
const registered = this.candidates.get(candidate.lease.attempt);
|
|
246
|
+
if (candidate.generation !== this.generation || registered === undefined) {
|
|
247
|
+
throw new Error('DDC candidate belongs to another generation session');
|
|
248
|
+
}
|
|
249
|
+
return registered;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private scheduleHeartbeat(candidate: MutableDdcGenerationCandidate): void {
|
|
253
|
+
const attempt = candidate.lease.attempt;
|
|
254
|
+
this.stopHeartbeat(candidate);
|
|
255
|
+
const delay = Math.max(1, Math.floor((candidate.lease.expiresAt - Date.now()) / 2));
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
this.heartbeatTimers.delete(attempt);
|
|
258
|
+
if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
|
|
259
|
+
void this.lifecycle
|
|
260
|
+
.heartbeat(candidate.lease)
|
|
261
|
+
.then((lease) => {
|
|
262
|
+
if (!this.accepting || this.candidates.get(attempt) !== candidate) return;
|
|
263
|
+
candidate.lease = lease;
|
|
264
|
+
this.scheduleHeartbeat(candidate);
|
|
265
|
+
})
|
|
266
|
+
.catch(() => {
|
|
267
|
+
this.stopHeartbeat(candidate);
|
|
268
|
+
});
|
|
269
|
+
}, delay);
|
|
270
|
+
timer.unref?.();
|
|
271
|
+
this.heartbeatTimers.set(attempt, timer);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private stopHeartbeat(candidate: DdcGenerationCandidate | DdcLease): void {
|
|
275
|
+
const attempt = 'lease' in candidate ? candidate.lease.attempt : candidate.attempt;
|
|
276
|
+
const timer = this.heartbeatTimers.get(attempt);
|
|
277
|
+
if (timer === undefined) return;
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
this.heartbeatTimers.delete(attempt);
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DdcErrorRootKind } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export const DDC_STATUS_SCHEMA = 'forgeax-ddc-status/v2' as const;
|
|
4
|
+
|
|
5
|
+
export type DdcStatusHealth = 'ready' | 'blocked' | 'missing' | 'stale' | 'failed';
|
|
6
|
+
export type DdcStatusLayerKind = 'build' | 'project' | 'runtime';
|
|
7
|
+
export type DdcStatusRootKind = DdcErrorRootKind;
|
|
8
|
+
|
|
9
|
+
export interface DdcRecoveryAction {
|
|
10
|
+
readonly kind: 'inspect' | 'allocate-generation' | 'retry' | 'cold-rebuild' | 'prune';
|
|
11
|
+
readonly executable: boolean;
|
|
12
|
+
readonly exactTarget?: string;
|
|
13
|
+
readonly reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DdcStatusLayer {
|
|
17
|
+
readonly kind: DdcStatusLayerKind;
|
|
18
|
+
readonly owner: 'engine-ddc' | 'engine-pack' | 'plugin' | 'runtime';
|
|
19
|
+
readonly rootKind: DdcStatusRootKind;
|
|
20
|
+
readonly scopeId?: string;
|
|
21
|
+
readonly current?: number | string;
|
|
22
|
+
readonly lastKnownGood?: number | string;
|
|
23
|
+
readonly protection: readonly string[];
|
|
24
|
+
readonly actions: readonly DdcRecoveryAction[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DdcStatus {
|
|
28
|
+
readonly schemaVersion: typeof DDC_STATUS_SCHEMA;
|
|
29
|
+
readonly health: DdcStatusHealth;
|
|
30
|
+
readonly layers: readonly DdcStatusLayer[];
|
|
31
|
+
readonly gameDir?: string;
|
|
32
|
+
readonly projectDdcRoot?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BrowserDdcRecoveryAction = Omit<DdcRecoveryAction, 'exactTarget'>;
|
|
36
|
+
export type BrowserDdcStatusLayer = Omit<DdcStatusLayer, 'actions'> & {
|
|
37
|
+
readonly actions: readonly BrowserDdcRecoveryAction[];
|
|
38
|
+
};
|
|
39
|
+
export type BrowserDdcStatus = Omit<DdcStatus, 'gameDir' | 'projectDdcRoot' | 'layers'> & {
|
|
40
|
+
readonly layers: readonly BrowserDdcStatusLayer[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function serializeDdcStatus(status: DdcStatus): string {
|
|
44
|
+
return JSON.stringify(status);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function projectDdcStatusForBrowser(status: DdcStatus): BrowserDdcStatus {
|
|
48
|
+
return {
|
|
49
|
+
schemaVersion: DDC_STATUS_SCHEMA,
|
|
50
|
+
health: status.health,
|
|
51
|
+
layers: status.layers.map((layer) => ({
|
|
52
|
+
...layer,
|
|
53
|
+
actions: layer.actions.map(({ exactTarget: _exactTarget, ...action }) => action),
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const toBrowserDdcStatus = projectDdcStatusForBrowser;
|