@forgeax/engine-assets-runtime 0.1.2
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 +213 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/asset-graph-red.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-kind.test.d.ts +2 -0
- package/dist/__tests__/asset-kind.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-core.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-session-red.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-mime-owner.test.d.ts +2 -0
- package/dist/__tests__/decode-image-mime-owner.test.d.ts.map +1 -0
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +1 -0
- package/dist/asset-kind.d.ts +4 -0
- package/dist/asset-kind.d.ts.map +1 -0
- package/dist/catalog-source.d.ts +23 -0
- package/dist/catalog-source.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1240 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal/artifact-cache.d.ts +17 -0
- package/dist/internal/artifact-cache.d.ts.map +1 -0
- package/dist/internal/asset-graph.d.ts +70 -0
- package/dist/internal/asset-graph.d.ts.map +1 -0
- package/dist/internal/catalog-session.d.ts +60 -0
- package/dist/internal/catalog-session.d.ts.map +1 -0
- package/dist/internal/decoder-registry.d.ts +19 -0
- package/dist/internal/decoder-registry.d.ts.map +1 -0
- package/dist/internal/immutable-payload.d.ts +9 -0
- package/dist/internal/immutable-payload.d.ts.map +1 -0
- package/dist/internal/load-asset.d.ts +31 -0
- package/dist/internal/load-asset.d.ts.map +1 -0
- package/dist/internal/pack-reader.d.ts +18 -0
- package/dist/internal/pack-reader.d.ts.map +1 -0
- package/dist/internal/validate-runtime-row.d.ts +7 -0
- package/dist/internal/validate-runtime-row.d.ts.map +1 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.mjs +15 -0
- package/dist/internal.mjs.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/asset-graph-red.integration.test.ts +113 -0
- package/src/__tests__/asset-kind.test.ts +14 -0
- package/src/__tests__/asset-registry-core.integration.test.ts +161 -0
- package/src/__tests__/asset-registry-public-api.test-d.ts +31 -0
- package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +79 -0
- package/src/__tests__/asset-runtime-snapshot.unit.test.ts +23 -0
- package/src/__tests__/catalog-session-red.unit.test.ts +276 -0
- package/src/__tests__/decode-image-mime-owner.test.ts +41 -0
- package/src/__tests__/registry-lifecycle-red.integration.test.ts +80 -0
- package/src/asset-kind.ts +6 -0
- package/src/catalog-source.ts +145 -0
- package/src/index.ts +22 -0
- package/src/internal/artifact-cache.ts +65 -0
- package/src/internal/asset-graph.ts +420 -0
- package/src/internal/catalog-session.ts +345 -0
- package/src/internal/decoder-registry.ts +175 -0
- package/src/internal/immutable-payload.ts +20 -0
- package/src/internal/load-asset.ts +254 -0
- package/src/internal/pack-reader.ts +183 -0
- package/src/internal/validate-runtime-row.ts +51 -0
- package/src/internal.ts +4 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { AssetLoadError, Result } from '@forgeax/engine-types';
|
|
2
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
3
|
+
import { freezeRuntimePayload } from './immutable-payload.js';
|
|
4
|
+
|
|
5
|
+
export interface AssetGraphValue<P> {
|
|
6
|
+
readonly value: P;
|
|
7
|
+
readonly refs: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AssetGraphOptions<
|
|
11
|
+
P extends { readonly value: unknown; readonly refs: readonly string[] },
|
|
12
|
+
> {
|
|
13
|
+
readonly read: (guid: string, signal: AbortSignal) => Promise<Result<P, AssetLoadError>>;
|
|
14
|
+
readonly maxConcurrentReads?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AssetGraphCounters {
|
|
18
|
+
readonly loads: number;
|
|
19
|
+
readonly cacheHits: number;
|
|
20
|
+
readonly readErrors: number;
|
|
21
|
+
readonly noChange: number;
|
|
22
|
+
readonly listenerFailures: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AssetGraphSnapshot {
|
|
26
|
+
readonly epoch: number;
|
|
27
|
+
readonly ready: readonly string[];
|
|
28
|
+
readonly pending: number;
|
|
29
|
+
readonly resources: number;
|
|
30
|
+
readonly sccs: readonly (readonly string[])[];
|
|
31
|
+
readonly counters: AssetGraphCounters;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type GraphResult<P> = Result<P, AssetLoadError>;
|
|
35
|
+
type GraphListener = (snapshot: AssetGraphSnapshot) => void;
|
|
36
|
+
|
|
37
|
+
function cancelled(guid: string): AssetLoadError {
|
|
38
|
+
return {
|
|
39
|
+
code: 'asset-load-cancelled',
|
|
40
|
+
expected: 'the request AbortSignal to remain live until asset closure completes',
|
|
41
|
+
hint: 'retry with a live AbortSignal when the request is still needed',
|
|
42
|
+
detail: { guid },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disposed(scopeId = 'asset-runtime'): AssetLoadError {
|
|
47
|
+
return {
|
|
48
|
+
code: 'asset-runtime-disposed',
|
|
49
|
+
expected: 'an active asset runtime scope',
|
|
50
|
+
hint: 'obtain a new Registry from the current realm',
|
|
51
|
+
detail: { scopeId },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function superseded(guid: string, generation: number): AssetLoadError {
|
|
56
|
+
return {
|
|
57
|
+
code: 'asset-superseded',
|
|
58
|
+
expected: 'the load ticket to remain current until promotion',
|
|
59
|
+
hint: 'load the current publication after the Catalog change',
|
|
60
|
+
detail: { guid, generation },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function thrown(guid: string): AssetLoadError {
|
|
65
|
+
return {
|
|
66
|
+
code: 'asset-decode-failed',
|
|
67
|
+
expected: 'the graph reader to return a Result',
|
|
68
|
+
hint: 'repair the owner reader and retry the current publication',
|
|
69
|
+
detail: { guid, kind: 'asset-graph-reader' },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function freezeSnapshot(snapshot: AssetGraphSnapshot): AssetGraphSnapshot {
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
...snapshot,
|
|
76
|
+
ready: Object.freeze([...snapshot.ready]),
|
|
77
|
+
sccs: Object.freeze(snapshot.sccs.map((scc) => Object.freeze([...scc]))),
|
|
78
|
+
counters: Object.freeze({ ...snapshot.counters }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class AssetGraph<P extends { readonly value: unknown; readonly refs: readonly string[] }> {
|
|
83
|
+
private readonly read: AssetGraphOptions<P>['read'];
|
|
84
|
+
private readonly limit: number;
|
|
85
|
+
private readonly ready = new Map<string, P>();
|
|
86
|
+
private readonly forward = new Map<string, Set<string>>();
|
|
87
|
+
private readonly reverse = new Map<string, Set<string>>();
|
|
88
|
+
private readonly reads = new Map<string, Promise<GraphResult<P>>>();
|
|
89
|
+
private readonly requests = new Map<string, Promise<GraphResult<P>>>();
|
|
90
|
+
private readonly listeners = new Set<GraphListener>();
|
|
91
|
+
private readonly sccs: (readonly string[])[] = [];
|
|
92
|
+
private activeReads = 0;
|
|
93
|
+
private readWaiters: (() => void)[] = [];
|
|
94
|
+
private disposed = false;
|
|
95
|
+
private epoch = 0;
|
|
96
|
+
private counters: AssetGraphCounters = {
|
|
97
|
+
loads: 0,
|
|
98
|
+
cacheHits: 0,
|
|
99
|
+
readErrors: 0,
|
|
100
|
+
noChange: 0,
|
|
101
|
+
listenerFailures: 0,
|
|
102
|
+
};
|
|
103
|
+
private currentSnapshot: AssetGraphSnapshot;
|
|
104
|
+
|
|
105
|
+
constructor(options: AssetGraphOptions<P>) {
|
|
106
|
+
this.read = options.read;
|
|
107
|
+
this.limit = Math.max(1, Math.floor(options.maxConcurrentReads ?? 8));
|
|
108
|
+
this.currentSnapshot = freezeSnapshot({
|
|
109
|
+
epoch: 0,
|
|
110
|
+
ready: [],
|
|
111
|
+
pending: 0,
|
|
112
|
+
resources: 0,
|
|
113
|
+
sccs: [],
|
|
114
|
+
counters: this.counters,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
load(guid: string, signal = new AbortController().signal): Promise<GraphResult<P>> {
|
|
119
|
+
const canonicalGuid = guid.toLowerCase();
|
|
120
|
+
if (this.disposed) return Promise.resolve(err(disposed()));
|
|
121
|
+
if (signal.aborted) return Promise.resolve(err(cancelled(canonicalGuid)));
|
|
122
|
+
const cached = this.ready.get(canonicalGuid);
|
|
123
|
+
if (cached !== undefined) {
|
|
124
|
+
this.counters = {
|
|
125
|
+
...this.counters,
|
|
126
|
+
cacheHits: this.counters.cacheHits + 1,
|
|
127
|
+
noChange: this.counters.noChange + 1,
|
|
128
|
+
};
|
|
129
|
+
this.publish();
|
|
130
|
+
return Promise.resolve(ok(cached));
|
|
131
|
+
}
|
|
132
|
+
const existing = this.requests.get(canonicalGuid);
|
|
133
|
+
if (existing !== undefined) {
|
|
134
|
+
this.counters = { ...this.counters, cacheHits: this.counters.cacheHits + 1 };
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
this.counters = { ...this.counters, loads: this.counters.loads + 1 };
|
|
138
|
+
const ticketEpoch = this.epoch;
|
|
139
|
+
const request = this.loadClosure(canonicalGuid, signal, ticketEpoch).finally(() => {
|
|
140
|
+
if (this.requests.get(canonicalGuid) === request) this.requests.delete(canonicalGuid);
|
|
141
|
+
this.publish();
|
|
142
|
+
});
|
|
143
|
+
this.requests.set(canonicalGuid, request);
|
|
144
|
+
this.publish();
|
|
145
|
+
return request;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
invalidate(guid: string): readonly string[] {
|
|
149
|
+
const affected = this.collectAffected([guid.toLowerCase()]);
|
|
150
|
+
this.drop(affected);
|
|
151
|
+
this.epoch += 1;
|
|
152
|
+
this.publish();
|
|
153
|
+
return [...affected].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
invalidateForCatalogChange(guids?: readonly string[]): void {
|
|
157
|
+
if (this.disposed) return;
|
|
158
|
+
const affected =
|
|
159
|
+
guids === undefined || guids.length === 0
|
|
160
|
+
? new Set([...this.ready.keys(), ...this.requests.keys(), ...this.reads.keys()])
|
|
161
|
+
: this.collectAffected(guids.map((item) => item.toLowerCase()));
|
|
162
|
+
this.drop(affected);
|
|
163
|
+
this.epoch += 1;
|
|
164
|
+
this.publish();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
subscribe(listener: GraphListener): () => void {
|
|
168
|
+
this.listeners.add(listener);
|
|
169
|
+
return () => this.listeners.delete(listener);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
snapshot(): AssetGraphSnapshot {
|
|
173
|
+
return this.currentSnapshot;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
lookup(guid: string): P['value'] | undefined {
|
|
177
|
+
return this.ready.get(guid.toLowerCase())?.value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
guidOf(value: P['value']): string | undefined {
|
|
181
|
+
for (const [guid, entry] of this.ready) {
|
|
182
|
+
if (entry.value === value) return guid;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
dispose(): void {
|
|
188
|
+
if (this.disposed) return;
|
|
189
|
+
this.disposed = true;
|
|
190
|
+
this.epoch += 1;
|
|
191
|
+
this.ready.clear();
|
|
192
|
+
this.forward.clear();
|
|
193
|
+
this.reverse.clear();
|
|
194
|
+
this.reads.clear();
|
|
195
|
+
const waiters = this.readWaiters;
|
|
196
|
+
this.readWaiters = [];
|
|
197
|
+
for (const resolve of waiters) resolve();
|
|
198
|
+
this.publish();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async loadClosure(
|
|
202
|
+
root: string,
|
|
203
|
+
signal: AbortSignal,
|
|
204
|
+
ticketEpoch: number,
|
|
205
|
+
): Promise<GraphResult<P>> {
|
|
206
|
+
const values = new Map<string, P>();
|
|
207
|
+
const visiting = new Set<string>();
|
|
208
|
+
const visited = new Set<string>();
|
|
209
|
+
const group = new Set<string>();
|
|
210
|
+
const result = await this.visit(root, signal, visiting, visited, values, group);
|
|
211
|
+
if (!result.ok) return result;
|
|
212
|
+
if (this.disposed) return err(disposed());
|
|
213
|
+
if (signal.aborted) return err(cancelled(root));
|
|
214
|
+
if (ticketEpoch !== this.epoch) return err(superseded(root, ticketEpoch));
|
|
215
|
+
for (const [guid, value] of values) this.promote(guid, value);
|
|
216
|
+
this.recordScc();
|
|
217
|
+
return ok(values.get(root) as P);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async visit(
|
|
221
|
+
guid: string,
|
|
222
|
+
signal: AbortSignal,
|
|
223
|
+
visiting: Set<string>,
|
|
224
|
+
visited: Set<string>,
|
|
225
|
+
values: Map<string, P>,
|
|
226
|
+
group: Set<string>,
|
|
227
|
+
): Promise<GraphResult<P>> {
|
|
228
|
+
if (visiting.has(guid)) {
|
|
229
|
+
group.add(guid);
|
|
230
|
+
return ok({ value: undefined as unknown as P, refs: [] } as unknown as P);
|
|
231
|
+
}
|
|
232
|
+
if (visited.has(guid)) return ok(values.get(guid) as P);
|
|
233
|
+
const cached = this.ready.get(guid);
|
|
234
|
+
if (cached !== undefined) {
|
|
235
|
+
values.set(guid, cached);
|
|
236
|
+
visited.add(guid);
|
|
237
|
+
return ok(cached);
|
|
238
|
+
}
|
|
239
|
+
visiting.add(guid);
|
|
240
|
+
const read = await this.readOnce(guid, signal);
|
|
241
|
+
if (!read.ok) {
|
|
242
|
+
visiting.delete(guid);
|
|
243
|
+
return read;
|
|
244
|
+
}
|
|
245
|
+
values.set(guid, read.value);
|
|
246
|
+
group.add(guid);
|
|
247
|
+
this.link(guid, read.value.refs);
|
|
248
|
+
for (const ref of read.value.refs) {
|
|
249
|
+
const child = await this.visit(ref, signal, visiting, visited, values, group);
|
|
250
|
+
if (!child.ok) {
|
|
251
|
+
visiting.delete(guid);
|
|
252
|
+
return err({
|
|
253
|
+
code: 'asset-dependency-failed',
|
|
254
|
+
expected: 'every referenced asset to load successfully',
|
|
255
|
+
hint: 'repair the dependency publication and retry the root asset',
|
|
256
|
+
detail: { guid, dependencyGuid: ref },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
visiting.delete(guid);
|
|
261
|
+
visited.add(guid);
|
|
262
|
+
return ok(read.value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private readOnce(guid: string, signal: AbortSignal): Promise<GraphResult<P>> {
|
|
266
|
+
const existing = this.reads.get(guid);
|
|
267
|
+
if (existing !== undefined) return existing;
|
|
268
|
+
const request = this.withPermit(async () => {
|
|
269
|
+
if (this.disposed) return err(disposed());
|
|
270
|
+
if (signal.aborted) return err(cancelled(guid));
|
|
271
|
+
try {
|
|
272
|
+
const result = await this.read(guid, signal);
|
|
273
|
+
if (signal.aborted) return err(cancelled(guid));
|
|
274
|
+
if (!result.ok)
|
|
275
|
+
this.counters = { ...this.counters, readErrors: this.counters.readErrors + 1 };
|
|
276
|
+
return result;
|
|
277
|
+
} catch {
|
|
278
|
+
this.counters = { ...this.counters, readErrors: this.counters.readErrors + 1 };
|
|
279
|
+
return err(thrown(guid));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.reads.set(guid, request);
|
|
283
|
+
void request.finally(() => {
|
|
284
|
+
if (this.reads.get(guid) === request) this.reads.delete(guid);
|
|
285
|
+
});
|
|
286
|
+
return request;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async withPermit<T>(operation: () => Promise<T>): Promise<T> {
|
|
290
|
+
if (this.activeReads >= this.limit) {
|
|
291
|
+
await new Promise<void>((resolve) => this.readWaiters.push(resolve));
|
|
292
|
+
}
|
|
293
|
+
this.activeReads += 1;
|
|
294
|
+
try {
|
|
295
|
+
return await operation();
|
|
296
|
+
} finally {
|
|
297
|
+
this.activeReads -= 1;
|
|
298
|
+
this.readWaiters.shift()?.();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private link(guid: string, refs: readonly string[]): void {
|
|
303
|
+
const previous = this.forward.get(guid) ?? new Set<string>();
|
|
304
|
+
for (const ref of previous) this.reverse.get(ref)?.delete(guid);
|
|
305
|
+
const next = new Set(refs.map((ref) => ref.toLowerCase()));
|
|
306
|
+
this.forward.set(guid, next);
|
|
307
|
+
for (const ref of next) {
|
|
308
|
+
const dependents = this.reverse.get(ref) ?? new Set<string>();
|
|
309
|
+
dependents.add(guid);
|
|
310
|
+
this.reverse.set(ref, dependents);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private collectAffected(guids: readonly string[]): Set<string> {
|
|
315
|
+
const affected = new Set<string>();
|
|
316
|
+
const queue = [...guids];
|
|
317
|
+
while (queue.length > 0) {
|
|
318
|
+
const current = queue.shift();
|
|
319
|
+
if (current === undefined || affected.has(current)) continue;
|
|
320
|
+
affected.add(current);
|
|
321
|
+
for (const dependent of this.reverse.get(current) ?? []) queue.push(dependent);
|
|
322
|
+
}
|
|
323
|
+
return affected;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private drop(affected: ReadonlySet<string>): void {
|
|
327
|
+
for (const item of affected) {
|
|
328
|
+
for (const ref of this.forward.get(item) ?? []) this.reverse.get(ref)?.delete(item);
|
|
329
|
+
for (const dependent of this.reverse.get(item) ?? []) {
|
|
330
|
+
this.forward.get(dependent)?.delete(item);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const item of affected) {
|
|
334
|
+
this.ready.delete(item);
|
|
335
|
+
this.requests.delete(item);
|
|
336
|
+
this.reads.delete(item);
|
|
337
|
+
this.forward.delete(item);
|
|
338
|
+
this.reverse.delete(item);
|
|
339
|
+
}
|
|
340
|
+
this.sccs.splice(0, this.sccs.length);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private promote(guid: string, value: P): void {
|
|
344
|
+
if (value === undefined) return;
|
|
345
|
+
this.ready.set(
|
|
346
|
+
guid,
|
|
347
|
+
Object.freeze({
|
|
348
|
+
...value,
|
|
349
|
+
value: freezeRuntimePayload(value.value),
|
|
350
|
+
refs: Object.freeze([...value.refs]),
|
|
351
|
+
}) as P,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private recordScc(): void {
|
|
356
|
+
const indexByGuid = new Map<string, number>();
|
|
357
|
+
const lowByGuid = new Map<string, number>();
|
|
358
|
+
const stack: string[] = [];
|
|
359
|
+
const onStack = new Set<string>();
|
|
360
|
+
let nextIndex = 0;
|
|
361
|
+
const components: string[][] = [];
|
|
362
|
+
const visit = (guid: string): void => {
|
|
363
|
+
indexByGuid.set(guid, nextIndex);
|
|
364
|
+
lowByGuid.set(guid, nextIndex);
|
|
365
|
+
nextIndex += 1;
|
|
366
|
+
stack.push(guid);
|
|
367
|
+
onStack.add(guid);
|
|
368
|
+
for (const ref of this.forward.get(guid) ?? []) {
|
|
369
|
+
if (!indexByGuid.has(ref)) {
|
|
370
|
+
visit(ref);
|
|
371
|
+
lowByGuid.set(guid, Math.min(lowByGuid.get(guid) ?? 0, lowByGuid.get(ref) ?? 0));
|
|
372
|
+
} else if (onStack.has(ref)) {
|
|
373
|
+
lowByGuid.set(guid, Math.min(lowByGuid.get(guid) ?? 0, indexByGuid.get(ref) ?? 0));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (lowByGuid.get(guid) !== indexByGuid.get(guid)) return;
|
|
377
|
+
const component: string[] = [];
|
|
378
|
+
let member: string | undefined;
|
|
379
|
+
do {
|
|
380
|
+
member = stack.pop();
|
|
381
|
+
if (member === undefined) break;
|
|
382
|
+
onStack.delete(member);
|
|
383
|
+
component.push(member);
|
|
384
|
+
} while (member !== guid);
|
|
385
|
+
if (component.length > 1 || this.forward.get(guid)?.has(guid) === true)
|
|
386
|
+
components.push(component.sort());
|
|
387
|
+
};
|
|
388
|
+
for (const guid of this.forward.keys()) if (!indexByGuid.has(guid)) visit(guid);
|
|
389
|
+
this.sccs.splice(
|
|
390
|
+
0,
|
|
391
|
+
this.sccs.length,
|
|
392
|
+
...components.map((component) => Object.freeze(component)),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private publish(): void {
|
|
397
|
+
this.currentSnapshot = freezeSnapshot({
|
|
398
|
+
epoch: this.epoch,
|
|
399
|
+
ready: [...this.ready.keys()].sort(),
|
|
400
|
+
pending: this.requests.size + this.reads.size,
|
|
401
|
+
resources: this.ready.size,
|
|
402
|
+
sccs: this.sccs,
|
|
403
|
+
counters: this.counters,
|
|
404
|
+
});
|
|
405
|
+
for (const listener of [...this.listeners]) {
|
|
406
|
+
try {
|
|
407
|
+
listener(this.currentSnapshot);
|
|
408
|
+
} catch {
|
|
409
|
+
this.counters = {
|
|
410
|
+
...this.counters,
|
|
411
|
+
listenerFailures: Math.min(1024, this.counters.listenerFailures + 1),
|
|
412
|
+
};
|
|
413
|
+
this.currentSnapshot = freezeSnapshot({
|
|
414
|
+
...this.currentSnapshot,
|
|
415
|
+
counters: this.counters,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|