@cortexkit/aft-bridge 0.49.3 → 0.49.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/dist/cache-paths.d.ts +26 -0
- package/dist/cache-paths.d.ts.map +1 -0
- package/dist/cache-paths.js +37 -0
- package/dist/cache-paths.js.map +1 -0
- package/dist/downloader.d.ts +2 -3
- package/dist/downloader.d.ts.map +1 -1
- package/dist/downloader.js +5 -13
- package/dist/downloader.js.map +1 -1
- package/dist/durable-log.d.ts +3 -1
- package/dist/durable-log.d.ts.map +1 -1
- package/dist/durable-log.js +4 -2
- package/dist/durable-log.js.map +1 -1
- package/dist/error-contract.d.ts +7 -0
- package/dist/error-contract.d.ts.map +1 -1
- package/dist/error-contract.js +35 -0
- package/dist/error-contract.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/lifecycle-registry.d.ts +196 -0
- package/dist/lifecycle-registry.d.ts.map +1 -0
- package/dist/lifecycle-registry.js +471 -0
- package/dist/lifecycle-registry.js.map +1 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +4 -11
- package/dist/resolver.js.map +1 -1
- package/dist/revivable-transport.d.ts +10 -0
- package/dist/revivable-transport.d.ts.map +1 -1
- package/dist/revivable-transport.js +23 -0
- package/dist/revivable-transport.js.map +1 -1
- package/dist/subc-transport.d.ts +202 -42
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +810 -271
- package/dist/subc-transport.js.map +1 -1
- package/dist/transport-factory.d.ts +19 -0
- package/dist/transport-factory.d.ts.map +1 -1
- package/dist/transport-factory.js +71 -2
- package/dist/transport-factory.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** A canonical project root. The registry never derives identity by parsing it. */
|
|
2
|
+
declare const canonicalRootPathBrand: unique symbol;
|
|
3
|
+
export type CanonicalRootPath = string & {
|
|
4
|
+
readonly [canonicalRootPathBrand]: "CanonicalRootPath";
|
|
5
|
+
};
|
|
6
|
+
/** A pool-lifetime root generation. Generations are strictly increasing per root. */
|
|
7
|
+
declare const rootGenerationBrand: unique symbol;
|
|
8
|
+
export type RootGeneration = number & {
|
|
9
|
+
readonly [rootGenerationBrand]: "RootGeneration";
|
|
10
|
+
};
|
|
11
|
+
/** A realm-local concrete pool identity. IDs are never reused by a registry. */
|
|
12
|
+
declare const concretePoolIdBrand: unique symbol;
|
|
13
|
+
export type ConcretePoolId = string & {
|
|
14
|
+
readonly [concretePoolIdBrand]: "ConcretePoolId";
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Immutable provenance for one registration handle. The object identity is the
|
|
18
|
+
* deregistration authority; the serial fields make the provenance inspectable
|
|
19
|
+
* without treating a pool object as a registration token.
|
|
20
|
+
*/
|
|
21
|
+
export interface RegistrationIdentity {
|
|
22
|
+
readonly concretePoolId: ConcretePoolId;
|
|
23
|
+
readonly registrationSequence: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function asCanonicalRootPath(root: string): CanonicalRootPath;
|
|
26
|
+
export declare function asRootGeneration(generation: number): RootGeneration;
|
|
27
|
+
export declare function asConcretePoolId(id: string): ConcretePoolId;
|
|
28
|
+
export type LifecycleCloseCause = "sweep" | "explicit";
|
|
29
|
+
export type LifecycleRootState = "live" | "reaping" | "tombstoned";
|
|
30
|
+
/** Counts returned by a concrete pool after it has detached its indexed records. */
|
|
31
|
+
export interface LifecycleTeardownResult {
|
|
32
|
+
readonly tornDownSessionCount?: number;
|
|
33
|
+
readonly tornDownFacadeCount?: number;
|
|
34
|
+
}
|
|
35
|
+
/** The concrete-pool portion of the lifecycle contract. */
|
|
36
|
+
export interface LifecyclePool {
|
|
37
|
+
closeProjectRoot(root: CanonicalRootPath, generation: RootGeneration): unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface LifecyclePoolRegistrationOptions {
|
|
40
|
+
readonly reapingEnabled: boolean;
|
|
41
|
+
readonly evictOuterFacade: (root: CanonicalRootPath, generation: RootGeneration) => void;
|
|
42
|
+
}
|
|
43
|
+
export interface LifecyclePoolRegistration {
|
|
44
|
+
readonly concretePoolId: ConcretePoolId;
|
|
45
|
+
readonly reapingEnabled: boolean;
|
|
46
|
+
readonly registrationIdentity: RegistrationIdentity;
|
|
47
|
+
/** Short alias for callers that refer to the provenance token as identity. */
|
|
48
|
+
readonly identity: RegistrationIdentity;
|
|
49
|
+
deregister(): void;
|
|
50
|
+
}
|
|
51
|
+
export type LifecycleStatResult = boolean | {
|
|
52
|
+
readonly exists?: boolean;
|
|
53
|
+
} | unknown;
|
|
54
|
+
export type LifecycleStat = (root: CanonicalRootPath, concretePoolId: ConcretePoolId, generation: RootGeneration) => LifecycleStatResult | Promise<LifecycleStatResult>;
|
|
55
|
+
export type LifecycleDemandCheck = (root: CanonicalRootPath, concretePoolId: ConcretePoolId) => boolean | {
|
|
56
|
+
readonly exists?: boolean;
|
|
57
|
+
} | Promise<boolean | {
|
|
58
|
+
readonly exists?: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
export interface LifecycleTimerSeam {
|
|
61
|
+
setInterval(callback: () => void, delayMs: number): unknown;
|
|
62
|
+
clearInterval(handle: unknown): void;
|
|
63
|
+
}
|
|
64
|
+
export interface RootReapedLifecycleEvent {
|
|
65
|
+
readonly type: "subc_root_reaped";
|
|
66
|
+
readonly realm: string;
|
|
67
|
+
readonly concretePoolId: ConcretePoolId;
|
|
68
|
+
readonly canonicalRoot: CanonicalRootPath;
|
|
69
|
+
readonly generation: RootGeneration;
|
|
70
|
+
readonly consecutiveAbsenceCount: number;
|
|
71
|
+
readonly tornDownSessionCount: number;
|
|
72
|
+
readonly tornDownFacadeCount: number;
|
|
73
|
+
readonly cause: LifecycleCloseCause;
|
|
74
|
+
}
|
|
75
|
+
export interface RootGenerationRejectedLifecycleEvent {
|
|
76
|
+
readonly type: "subc_root_generation_rejected";
|
|
77
|
+
readonly realm: string;
|
|
78
|
+
readonly concretePoolId: ConcretePoolId;
|
|
79
|
+
readonly canonicalRoot: CanonicalRootPath;
|
|
80
|
+
readonly expectedGeneration: RootGeneration;
|
|
81
|
+
readonly currentGeneration?: RootGeneration;
|
|
82
|
+
readonly boundary: string;
|
|
83
|
+
}
|
|
84
|
+
export type LifecycleEvent = RootReapedLifecycleEvent | RootGenerationRejectedLifecycleEvent;
|
|
85
|
+
export interface LifecycleRegistryOptions {
|
|
86
|
+
/** Label attached to realm-local metrics and lifecycle events. */
|
|
87
|
+
readonly realm?: string;
|
|
88
|
+
/** Production cadence. Tests normally replace the timer and use a short value. */
|
|
89
|
+
readonly intervalMs?: number;
|
|
90
|
+
readonly timer?: LifecycleTimerSeam;
|
|
91
|
+
readonly setInterval?: LifecycleTimerSeam["setInterval"];
|
|
92
|
+
readonly clearInterval?: LifecycleTimerSeam["clearInterval"];
|
|
93
|
+
/** A stat that resolves for an existing root and rejects with ENOENT when absent. */
|
|
94
|
+
readonly stat?: LifecycleStat;
|
|
95
|
+
/** Positive demand is the only signal that permits an initial or successor root. */
|
|
96
|
+
readonly demandCheck?: LifecycleDemandCheck;
|
|
97
|
+
readonly onEvent?: (event: LifecycleEvent) => void;
|
|
98
|
+
}
|
|
99
|
+
export interface LifecycleRootSnapshot {
|
|
100
|
+
readonly concretePoolId: ConcretePoolId;
|
|
101
|
+
readonly registrationIdentity: RegistrationIdentity;
|
|
102
|
+
readonly canonicalRoot: CanonicalRootPath;
|
|
103
|
+
readonly generation: RootGeneration;
|
|
104
|
+
readonly state: LifecycleRootState;
|
|
105
|
+
readonly consecutiveAbsences: number;
|
|
106
|
+
}
|
|
107
|
+
export interface LifecycleRegistrationSnapshot {
|
|
108
|
+
readonly concretePoolId: ConcretePoolId;
|
|
109
|
+
readonly reapingEnabled: boolean;
|
|
110
|
+
readonly roots: readonly LifecycleRootSnapshot[];
|
|
111
|
+
}
|
|
112
|
+
export interface LifecycleRegistrySnapshot {
|
|
113
|
+
readonly realm: string;
|
|
114
|
+
readonly intervalMs: number;
|
|
115
|
+
readonly timerActive: boolean;
|
|
116
|
+
readonly registrations: readonly LifecycleRegistrationSnapshot[];
|
|
117
|
+
}
|
|
118
|
+
export declare const DEFAULT_LIFECYCLE_INTERVAL_MS: number;
|
|
119
|
+
/**
|
|
120
|
+
* One lifecycle registry per JavaScript module realm.
|
|
121
|
+
*
|
|
122
|
+
* The registry deliberately knows only the concrete pool's root-close seam. It
|
|
123
|
+
* owns root generations, absence observations, and transition ownership, while
|
|
124
|
+
* the concrete pool remains responsible for indexed session and route cleanup.
|
|
125
|
+
*/
|
|
126
|
+
export declare class LifecycleRegistry {
|
|
127
|
+
readonly realm: string;
|
|
128
|
+
readonly intervalMs: number;
|
|
129
|
+
private readonly registrations;
|
|
130
|
+
private readonly registrationByPool;
|
|
131
|
+
private readonly nextGenerationByRoot;
|
|
132
|
+
private nextConcretePoolNumber;
|
|
133
|
+
private nextRegistrationSequence;
|
|
134
|
+
private readonly timer;
|
|
135
|
+
private readonly statRoot;
|
|
136
|
+
private readonly demand;
|
|
137
|
+
private readonly onEvent?;
|
|
138
|
+
private intervalHandle;
|
|
139
|
+
private sweepInFlight;
|
|
140
|
+
constructor(options?: LifecycleRegistryOptions);
|
|
141
|
+
/** Register an unregistered concrete pool and permanently bind its reapingEnabled setting. */
|
|
142
|
+
registerLifecyclePool(pool: LifecyclePool, options: LifecyclePoolRegistrationOptions): LifecyclePoolRegistration;
|
|
143
|
+
/** Functional form used by construction sites that keep a registry instance. */
|
|
144
|
+
registerPool(pool: LifecyclePool, options: LifecyclePoolRegistrationOptions): LifecyclePoolRegistration;
|
|
145
|
+
/** Register a root before its facade is exposed. Repeated live registration is idempotent. */
|
|
146
|
+
registerRoot(poolId: ConcretePoolId, root: CanonicalRootPath): RootGeneration;
|
|
147
|
+
/**
|
|
148
|
+
* Get an existing live generation without creating one. This is the only
|
|
149
|
+
* non-creating lookup used by status and background paths.
|
|
150
|
+
*/
|
|
151
|
+
currentGeneration(poolId: ConcretePoolId, root: CanonicalRootPath): RootGeneration | undefined;
|
|
152
|
+
isTombstoned(poolId: ConcretePoolId, root: CanonicalRootPath, generation: RootGeneration): boolean;
|
|
153
|
+
isCurrentRegistration(identity: RegistrationIdentity): boolean;
|
|
154
|
+
isCurrentLiveGeneration(poolId: ConcretePoolId, root: CanonicalRootPath, generation: RootGeneration): boolean;
|
|
155
|
+
getRootSnapshot(poolId: ConcretePoolId, root: CanonicalRootPath): LifecycleRootSnapshot | undefined;
|
|
156
|
+
/** Return only roots visible to lifecycle operations; tombstones are retained until close settles. */
|
|
157
|
+
rootSnapshots(poolId?: ConcretePoolId): LifecycleRootSnapshot[];
|
|
158
|
+
snapshot(): LifecycleRegistrySnapshot;
|
|
159
|
+
/** Alias for callers that use status terminology. */
|
|
160
|
+
status(): LifecycleRegistrySnapshot;
|
|
161
|
+
/**
|
|
162
|
+
* Demand gate for initial and successor creation. A live root is returned
|
|
163
|
+
* without repeating the filesystem check; absent roots require a fresh positive
|
|
164
|
+
* result and then receive exactly one greater generation.
|
|
165
|
+
*/
|
|
166
|
+
ensureRootForDemand(poolId: ConcretePoolId, root: CanonicalRootPath): Promise<RootGeneration | undefined>;
|
|
167
|
+
demandRoot(poolId: ConcretePoolId, root: CanonicalRootPath): Promise<RootGeneration | undefined>;
|
|
168
|
+
getBridgeDemand(poolId: ConcretePoolId, root: CanonicalRootPath): Promise<RootGeneration | undefined>;
|
|
169
|
+
/**
|
|
170
|
+
* Coordinate one generation's close. The live-to-reaping transition, tombstone,
|
|
171
|
+
* outer eviction, and concrete close invocation all happen before the returned
|
|
172
|
+
* promise can yield. Concurrent requests coalesce on the same close promise.
|
|
173
|
+
*/
|
|
174
|
+
requestProjectRootClose(poolId: ConcretePoolId, root: CanonicalRootPath, expectedGeneration: RootGeneration, cause?: LifecycleCloseCause): Promise<void>;
|
|
175
|
+
/** Coalesce concurrent ticks and observe only enabled, live registrations. */
|
|
176
|
+
sweep(): Promise<void>;
|
|
177
|
+
/** Alias used by timer seams and tests. */
|
|
178
|
+
sweepNow(): Promise<void>;
|
|
179
|
+
/** Emit a generation-rejection event when a lifecycle boundary refuses the expected generation. */
|
|
180
|
+
recordGenerationRejection(poolId: ConcretePoolId, root: CanonicalRootPath, expectedGeneration: RootGeneration, boundary: string): void;
|
|
181
|
+
rejectGeneration(poolId: ConcretePoolId, root: CanonicalRootPath, expectedGeneration: RootGeneration, boundary: string): void;
|
|
182
|
+
private runSweep;
|
|
183
|
+
private currentRegistration;
|
|
184
|
+
private currentRoot;
|
|
185
|
+
private snapshotRoot;
|
|
186
|
+
private finishRootClose;
|
|
187
|
+
private deregisterInternal;
|
|
188
|
+
private refreshTimer;
|
|
189
|
+
private emit;
|
|
190
|
+
}
|
|
191
|
+
/** Create a lifecycle registry for callers that do not inject one; use getLifecycleRegistry() for the shared default. */
|
|
192
|
+
export declare function createLifecycleRegistry(options?: LifecycleRegistryOptions): LifecycleRegistry;
|
|
193
|
+
export declare function getLifecycleRegistry(): LifecycleRegistry;
|
|
194
|
+
export { LifecycleRegistry as SubcLifecycleRegistry };
|
|
195
|
+
export declare function registerLifecyclePool(pool: LifecyclePool, options: LifecyclePoolRegistrationOptions): LifecyclePoolRegistration;
|
|
196
|
+
//# sourceMappingURL=lifecycle-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-registry.d.ts","sourceRoot":"","sources":["../src/lifecycle-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
export function asCanonicalRootPath(root) {
|
|
3
|
+
return root;
|
|
4
|
+
}
|
|
5
|
+
export function asRootGeneration(generation) {
|
|
6
|
+
if (!Number.isSafeInteger(generation) || generation < 1) {
|
|
7
|
+
throw new RangeError("root generations must be positive safe integers");
|
|
8
|
+
}
|
|
9
|
+
return generation;
|
|
10
|
+
}
|
|
11
|
+
export function asConcretePoolId(id) {
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
export const DEFAULT_LIFECYCLE_INTERVAL_MS = 5 * 60 * 1000;
|
|
15
|
+
const DEFAULT_REALM = "default";
|
|
16
|
+
function rootKey(root) {
|
|
17
|
+
return root;
|
|
18
|
+
}
|
|
19
|
+
function resultMeansPresent(result) {
|
|
20
|
+
if (typeof result === "boolean")
|
|
21
|
+
return result;
|
|
22
|
+
if (typeof result === "object" && result !== null && "exists" in result) {
|
|
23
|
+
return result.exists !== false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function isEnoent(error) {
|
|
28
|
+
return (typeof error === "object" &&
|
|
29
|
+
error !== null &&
|
|
30
|
+
"code" in error &&
|
|
31
|
+
error.code === "ENOENT");
|
|
32
|
+
}
|
|
33
|
+
function closeCounts(result) {
|
|
34
|
+
const teardown = typeof result === "object" && result !== null ? result : undefined;
|
|
35
|
+
const sessions = teardown?.tornDownSessionCount;
|
|
36
|
+
const facades = teardown?.tornDownFacadeCount;
|
|
37
|
+
return {
|
|
38
|
+
sessions: typeof sessions === "number" && Number.isFinite(sessions) ? Math.max(0, sessions) : 0,
|
|
39
|
+
facades: typeof facades === "number" && Number.isFinite(facades) ? Math.max(0, facades) : 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function defaultTimer() {
|
|
43
|
+
return {
|
|
44
|
+
setInterval(callback, delayMs) {
|
|
45
|
+
const handle = setInterval(callback, delayMs);
|
|
46
|
+
const maybeUnref = handle;
|
|
47
|
+
maybeUnref.unref?.();
|
|
48
|
+
return handle;
|
|
49
|
+
},
|
|
50
|
+
clearInterval(handle) {
|
|
51
|
+
clearInterval(handle);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One lifecycle registry per JavaScript module realm.
|
|
57
|
+
*
|
|
58
|
+
* The registry deliberately knows only the concrete pool's root-close seam. It
|
|
59
|
+
* owns root generations, absence observations, and transition ownership, while
|
|
60
|
+
* the concrete pool remains responsible for indexed session and route cleanup.
|
|
61
|
+
*/
|
|
62
|
+
export class LifecycleRegistry {
|
|
63
|
+
realm;
|
|
64
|
+
intervalMs;
|
|
65
|
+
registrations = new Map();
|
|
66
|
+
registrationByPool = new WeakMap();
|
|
67
|
+
nextGenerationByRoot = new Map();
|
|
68
|
+
nextConcretePoolNumber = 1;
|
|
69
|
+
nextRegistrationSequence = 1;
|
|
70
|
+
timer;
|
|
71
|
+
statRoot;
|
|
72
|
+
demand;
|
|
73
|
+
onEvent;
|
|
74
|
+
intervalHandle = null;
|
|
75
|
+
sweepInFlight = null;
|
|
76
|
+
constructor(options = {}) {
|
|
77
|
+
this.realm = options.realm ?? DEFAULT_REALM;
|
|
78
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_LIFECYCLE_INTERVAL_MS;
|
|
79
|
+
if (!Number.isFinite(this.intervalMs) || this.intervalMs <= 0) {
|
|
80
|
+
throw new RangeError("lifecycle interval must be a positive finite number");
|
|
81
|
+
}
|
|
82
|
+
const nativeTimer = defaultTimer();
|
|
83
|
+
this.timer =
|
|
84
|
+
options.timer ??
|
|
85
|
+
(options.setInterval || options.clearInterval
|
|
86
|
+
? {
|
|
87
|
+
setInterval: options.setInterval ?? nativeTimer.setInterval,
|
|
88
|
+
clearInterval: options.clearInterval ?? nativeTimer.clearInterval,
|
|
89
|
+
}
|
|
90
|
+
: nativeTimer);
|
|
91
|
+
this.statRoot = options.stat ?? ((root) => lstat(root));
|
|
92
|
+
this.demand = options.demandCheck ?? (() => false);
|
|
93
|
+
this.onEvent = options.onEvent;
|
|
94
|
+
}
|
|
95
|
+
/** Register an unregistered concrete pool and permanently bind its reapingEnabled setting. */
|
|
96
|
+
registerLifecyclePool(pool, options) {
|
|
97
|
+
if (!pool || typeof pool.closeProjectRoot !== "function") {
|
|
98
|
+
throw new TypeError("a lifecycle pool must provide closeProjectRoot");
|
|
99
|
+
}
|
|
100
|
+
if (typeof options?.evictOuterFacade !== "function") {
|
|
101
|
+
throw new TypeError("lifecycle registration requires evictOuterFacade");
|
|
102
|
+
}
|
|
103
|
+
if (typeof options.reapingEnabled !== "boolean") {
|
|
104
|
+
throw new TypeError("reapingEnabled must be a boolean");
|
|
105
|
+
}
|
|
106
|
+
const previous = this.registrationByPool.get(pool);
|
|
107
|
+
if (previous?.active)
|
|
108
|
+
this.deregisterInternal(previous);
|
|
109
|
+
const concretePoolId = asConcretePoolId(`pool-${this.nextConcretePoolNumber++}`);
|
|
110
|
+
const registrationIdentity = Object.freeze({
|
|
111
|
+
concretePoolId,
|
|
112
|
+
registrationSequence: this.nextRegistrationSequence++,
|
|
113
|
+
});
|
|
114
|
+
const internal = {
|
|
115
|
+
pool,
|
|
116
|
+
concretePoolId,
|
|
117
|
+
registrationIdentity,
|
|
118
|
+
reapingEnabled: options.reapingEnabled,
|
|
119
|
+
evictOuterFacade: options.evictOuterFacade,
|
|
120
|
+
roots: new Map(),
|
|
121
|
+
handle: undefined,
|
|
122
|
+
active: true,
|
|
123
|
+
};
|
|
124
|
+
const handle = {
|
|
125
|
+
registrationIdentity,
|
|
126
|
+
identity: registrationIdentity,
|
|
127
|
+
get concretePoolId() {
|
|
128
|
+
return concretePoolId;
|
|
129
|
+
},
|
|
130
|
+
get reapingEnabled() {
|
|
131
|
+
return internal.reapingEnabled;
|
|
132
|
+
},
|
|
133
|
+
deregister: () => {
|
|
134
|
+
this.deregisterInternal(internal);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
internal.handle = handle;
|
|
138
|
+
this.registrations.set(concretePoolId, internal);
|
|
139
|
+
this.registrationByPool.set(pool, internal);
|
|
140
|
+
this.refreshTimer();
|
|
141
|
+
return handle;
|
|
142
|
+
}
|
|
143
|
+
/** Functional form used by construction sites that keep a registry instance. */
|
|
144
|
+
registerPool(pool, options) {
|
|
145
|
+
return this.registerLifecyclePool(pool, options);
|
|
146
|
+
}
|
|
147
|
+
/** Register a root before its facade is exposed. Repeated live registration is idempotent. */
|
|
148
|
+
registerRoot(poolId, root) {
|
|
149
|
+
const registration = this.currentRegistration(poolId);
|
|
150
|
+
if (!registration)
|
|
151
|
+
throw new Error(`unknown lifecycle pool ${poolId}`);
|
|
152
|
+
const key = rootKey(root);
|
|
153
|
+
const existing = registration.roots.get(key);
|
|
154
|
+
if (existing?.state === "live")
|
|
155
|
+
return existing.generation;
|
|
156
|
+
if (existing && !existing.synchronousPhaseComplete) {
|
|
157
|
+
// This is only reachable through re-entrant code in the synchronous close
|
|
158
|
+
// phase. Exposing the old generation would permit resurrection, so callers
|
|
159
|
+
// must wait until that phase has finished and ask again.
|
|
160
|
+
throw new Error("root registration raced its synchronous close phase");
|
|
161
|
+
}
|
|
162
|
+
const next = (this.nextGenerationByRoot.get(`${poolId}\u0000${key}`) ?? 0) + 1;
|
|
163
|
+
this.nextGenerationByRoot.set(`${poolId}\u0000${key}`, next);
|
|
164
|
+
const entry = {
|
|
165
|
+
registration,
|
|
166
|
+
canonicalRoot: root,
|
|
167
|
+
generation: asRootGeneration(next),
|
|
168
|
+
state: "live",
|
|
169
|
+
consecutiveAbsences: 0,
|
|
170
|
+
tombstoned: false,
|
|
171
|
+
synchronousPhaseComplete: true,
|
|
172
|
+
closePromise: null,
|
|
173
|
+
eventEmitted: false,
|
|
174
|
+
cause: null,
|
|
175
|
+
};
|
|
176
|
+
registration.roots.set(key, entry);
|
|
177
|
+
return entry.generation;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get an existing live generation without creating one. This is the only
|
|
181
|
+
* non-creating lookup used by status and background paths.
|
|
182
|
+
*/
|
|
183
|
+
currentGeneration(poolId, root) {
|
|
184
|
+
return this.currentRoot(poolId, root)?.generation;
|
|
185
|
+
}
|
|
186
|
+
isTombstoned(poolId, root, generation) {
|
|
187
|
+
const entry = this.currentRoot(poolId, root);
|
|
188
|
+
return entry?.generation === generation && entry.tombstoned;
|
|
189
|
+
}
|
|
190
|
+
isCurrentRegistration(identity) {
|
|
191
|
+
const registration = this.registrations.get(identity.concretePoolId);
|
|
192
|
+
return registration?.active === true && registration.registrationIdentity === identity;
|
|
193
|
+
}
|
|
194
|
+
isCurrentLiveGeneration(poolId, root, generation) {
|
|
195
|
+
const entry = this.currentRoot(poolId, root);
|
|
196
|
+
return entry?.generation === generation && entry.state === "live";
|
|
197
|
+
}
|
|
198
|
+
getRootSnapshot(poolId, root) {
|
|
199
|
+
const entry = this.currentRoot(poolId, root);
|
|
200
|
+
return entry ? this.snapshotRoot(entry) : undefined;
|
|
201
|
+
}
|
|
202
|
+
/** Return only roots visible to lifecycle operations; tombstones are retained until close settles. */
|
|
203
|
+
rootSnapshots(poolId) {
|
|
204
|
+
const entries = poolId
|
|
205
|
+
? [this.currentRegistration(poolId)].filter((registration) => registration != null)
|
|
206
|
+
: [...this.registrations.values()];
|
|
207
|
+
return entries.flatMap((registration) => [...registration.roots.values()].map((entry) => this.snapshotRoot(entry)));
|
|
208
|
+
}
|
|
209
|
+
snapshot() {
|
|
210
|
+
return {
|
|
211
|
+
realm: this.realm,
|
|
212
|
+
intervalMs: this.intervalMs,
|
|
213
|
+
timerActive: this.intervalHandle !== null,
|
|
214
|
+
registrations: [...this.registrations.values()].map((registration) => ({
|
|
215
|
+
concretePoolId: registration.concretePoolId,
|
|
216
|
+
reapingEnabled: registration.reapingEnabled,
|
|
217
|
+
roots: [...registration.roots.values()].map((entry) => this.snapshotRoot(entry)),
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Alias for callers that use status terminology. */
|
|
222
|
+
status() {
|
|
223
|
+
return this.snapshot();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Demand gate for initial and successor creation. A live root is returned
|
|
227
|
+
* without repeating the filesystem check; absent roots require a fresh positive
|
|
228
|
+
* result and then receive exactly one greater generation.
|
|
229
|
+
*/
|
|
230
|
+
async ensureRootForDemand(poolId, root) {
|
|
231
|
+
const registration = this.currentRegistration(poolId);
|
|
232
|
+
if (!registration)
|
|
233
|
+
return undefined;
|
|
234
|
+
const existing = registration.roots.get(rootKey(root));
|
|
235
|
+
if (existing?.state === "live")
|
|
236
|
+
return existing.generation;
|
|
237
|
+
if (existing && !existing.synchronousPhaseComplete)
|
|
238
|
+
return undefined;
|
|
239
|
+
let result;
|
|
240
|
+
try {
|
|
241
|
+
result = await this.demand(root, poolId);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (!resultMeansPresent(result))
|
|
247
|
+
return undefined;
|
|
248
|
+
const stillCurrent = this.currentRegistration(poolId);
|
|
249
|
+
if (stillCurrent !== registration)
|
|
250
|
+
return undefined;
|
|
251
|
+
const afterCheck = registration.roots.get(rootKey(root));
|
|
252
|
+
if (afterCheck?.state === "live")
|
|
253
|
+
return afterCheck.generation;
|
|
254
|
+
if (afterCheck && !afterCheck.synchronousPhaseComplete)
|
|
255
|
+
return undefined;
|
|
256
|
+
return this.registerRoot(poolId, root);
|
|
257
|
+
}
|
|
258
|
+
demandRoot(poolId, root) {
|
|
259
|
+
return this.ensureRootForDemand(poolId, root);
|
|
260
|
+
}
|
|
261
|
+
getBridgeDemand(poolId, root) {
|
|
262
|
+
return this.ensureRootForDemand(poolId, root);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Coordinate one generation's close. The live-to-reaping transition, tombstone,
|
|
266
|
+
* outer eviction, and concrete close invocation all happen before the returned
|
|
267
|
+
* promise can yield. Concurrent requests coalesce on the same close promise.
|
|
268
|
+
*/
|
|
269
|
+
requestProjectRootClose(poolId, root, expectedGeneration, cause = "explicit") {
|
|
270
|
+
const registration = this.currentRegistration(poolId);
|
|
271
|
+
if (!registration)
|
|
272
|
+
return Promise.resolve();
|
|
273
|
+
const entry = registration.roots.get(rootKey(root));
|
|
274
|
+
if (!entry || entry.generation !== expectedGeneration)
|
|
275
|
+
return Promise.resolve();
|
|
276
|
+
if (entry.state === "reaping")
|
|
277
|
+
return entry.closePromise ?? Promise.resolve();
|
|
278
|
+
if (entry.state === "tombstoned")
|
|
279
|
+
return entry.closePromise ?? Promise.resolve();
|
|
280
|
+
entry.state = "reaping";
|
|
281
|
+
entry.tombstoned = true;
|
|
282
|
+
entry.cause = cause;
|
|
283
|
+
let closeResult;
|
|
284
|
+
try {
|
|
285
|
+
registration.evictOuterFacade(entry.canonicalRoot, entry.generation);
|
|
286
|
+
closeResult = registration.pool.closeProjectRoot(entry.canonicalRoot, entry.generation);
|
|
287
|
+
entry.synchronousPhaseComplete = true;
|
|
288
|
+
entry.state = "tombstoned";
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
entry.synchronousPhaseComplete = true;
|
|
292
|
+
entry.state = "tombstoned";
|
|
293
|
+
const failed = Promise.reject(error);
|
|
294
|
+
entry.closePromise = failed.then(() => undefined, (failure) => {
|
|
295
|
+
this.finishRootClose(entry, undefined);
|
|
296
|
+
throw failure;
|
|
297
|
+
});
|
|
298
|
+
return entry.closePromise;
|
|
299
|
+
}
|
|
300
|
+
entry.closePromise = Promise.resolve(closeResult).then((result) => {
|
|
301
|
+
const counts = closeCounts(result);
|
|
302
|
+
this.finishRootClose(entry, counts);
|
|
303
|
+
}, (error) => {
|
|
304
|
+
this.finishRootClose(entry, undefined);
|
|
305
|
+
throw error;
|
|
306
|
+
});
|
|
307
|
+
return entry.closePromise;
|
|
308
|
+
}
|
|
309
|
+
/** Coalesce concurrent ticks and observe only enabled, live registrations. */
|
|
310
|
+
sweep() {
|
|
311
|
+
if (this.sweepInFlight)
|
|
312
|
+
return this.sweepInFlight;
|
|
313
|
+
const sweep = this.runSweep();
|
|
314
|
+
this.sweepInFlight = sweep;
|
|
315
|
+
sweep.then(() => {
|
|
316
|
+
if (this.sweepInFlight === sweep)
|
|
317
|
+
this.sweepInFlight = null;
|
|
318
|
+
}, () => {
|
|
319
|
+
if (this.sweepInFlight === sweep)
|
|
320
|
+
this.sweepInFlight = null;
|
|
321
|
+
});
|
|
322
|
+
return sweep;
|
|
323
|
+
}
|
|
324
|
+
/** Alias used by timer seams and tests. */
|
|
325
|
+
sweepNow() {
|
|
326
|
+
return this.sweep();
|
|
327
|
+
}
|
|
328
|
+
/** Emit a generation-rejection event when a lifecycle boundary refuses the expected generation. */
|
|
329
|
+
recordGenerationRejection(poolId, root, expectedGeneration, boundary) {
|
|
330
|
+
const currentGeneration = this.currentGeneration(poolId, root);
|
|
331
|
+
this.emit({
|
|
332
|
+
type: "subc_root_generation_rejected",
|
|
333
|
+
realm: this.realm,
|
|
334
|
+
concretePoolId: poolId,
|
|
335
|
+
canonicalRoot: root,
|
|
336
|
+
expectedGeneration,
|
|
337
|
+
currentGeneration,
|
|
338
|
+
boundary,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
rejectGeneration(poolId, root, expectedGeneration, boundary) {
|
|
342
|
+
this.recordGenerationRejection(poolId, root, expectedGeneration, boundary);
|
|
343
|
+
}
|
|
344
|
+
async runSweep() {
|
|
345
|
+
const snapshots = [...this.registrations.values()]
|
|
346
|
+
.filter((registration) => registration.active && registration.reapingEnabled)
|
|
347
|
+
.flatMap((registration) => [...registration.roots.values()]
|
|
348
|
+
.filter((entry) => entry.state === "live")
|
|
349
|
+
.map((entry) => ({
|
|
350
|
+
concretePoolId: registration.concretePoolId,
|
|
351
|
+
canonicalRoot: entry.canonicalRoot,
|
|
352
|
+
generation: entry.generation,
|
|
353
|
+
})));
|
|
354
|
+
const closes = [];
|
|
355
|
+
await Promise.all(snapshots.map(async (snapshot) => {
|
|
356
|
+
let present = false;
|
|
357
|
+
try {
|
|
358
|
+
const result = await this.statRoot(snapshot.canonicalRoot, snapshot.concretePoolId, snapshot.generation);
|
|
359
|
+
present = resultMeansPresent(result);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (!isEnoent(error))
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const entry = this.currentRoot(snapshot.concretePoolId, snapshot.canonicalRoot);
|
|
366
|
+
if (!entry || entry.generation !== snapshot.generation || entry.state !== "live")
|
|
367
|
+
return;
|
|
368
|
+
if (present) {
|
|
369
|
+
entry.consecutiveAbsences = 0;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
entry.consecutiveAbsences += 1;
|
|
373
|
+
if (entry.consecutiveAbsences >= 2) {
|
|
374
|
+
closes.push(this.requestProjectRootClose(snapshot.concretePoolId, snapshot.canonicalRoot, snapshot.generation, "sweep"));
|
|
375
|
+
}
|
|
376
|
+
}));
|
|
377
|
+
await Promise.all(closes);
|
|
378
|
+
}
|
|
379
|
+
currentRegistration(poolId) {
|
|
380
|
+
const registration = this.registrations.get(poolId);
|
|
381
|
+
return registration?.active ? registration : undefined;
|
|
382
|
+
}
|
|
383
|
+
currentRoot(poolId, root) {
|
|
384
|
+
return this.currentRegistration(poolId)?.roots.get(rootKey(root));
|
|
385
|
+
}
|
|
386
|
+
snapshotRoot(entry) {
|
|
387
|
+
return {
|
|
388
|
+
concretePoolId: entry.registration.concretePoolId,
|
|
389
|
+
registrationIdentity: entry.registration.registrationIdentity,
|
|
390
|
+
canonicalRoot: entry.canonicalRoot,
|
|
391
|
+
generation: entry.generation,
|
|
392
|
+
state: entry.state,
|
|
393
|
+
consecutiveAbsences: entry.consecutiveAbsences,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
finishRootClose(entry, counts) {
|
|
397
|
+
if (counts && !entry.eventEmitted) {
|
|
398
|
+
entry.eventEmitted = true;
|
|
399
|
+
this.emit({
|
|
400
|
+
type: "subc_root_reaped",
|
|
401
|
+
realm: this.realm,
|
|
402
|
+
concretePoolId: entry.registration.concretePoolId,
|
|
403
|
+
canonicalRoot: entry.canonicalRoot,
|
|
404
|
+
generation: entry.generation,
|
|
405
|
+
consecutiveAbsenceCount: entry.consecutiveAbsences,
|
|
406
|
+
tornDownSessionCount: counts.sessions,
|
|
407
|
+
tornDownFacadeCount: counts.facades,
|
|
408
|
+
cause: entry.cause ?? "explicit",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const registration = entry.registration;
|
|
412
|
+
if (registration.roots.get(rootKey(entry.canonicalRoot)) === entry) {
|
|
413
|
+
registration.roots.delete(rootKey(entry.canonicalRoot));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
deregisterInternal(registration) {
|
|
417
|
+
if (!registration.active ||
|
|
418
|
+
this.registrations.get(registration.concretePoolId) !== registration) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
for (const entry of [...registration.roots.values()]) {
|
|
422
|
+
if (entry.state === "live") {
|
|
423
|
+
const close = this.requestProjectRootClose(registration.concretePoolId, entry.canonicalRoot, entry.generation, "explicit");
|
|
424
|
+
void close.catch(() => undefined);
|
|
425
|
+
}
|
|
426
|
+
registration.roots.delete(rootKey(entry.canonicalRoot));
|
|
427
|
+
}
|
|
428
|
+
registration.active = false;
|
|
429
|
+
this.registrations.delete(registration.concretePoolId);
|
|
430
|
+
if (this.registrationByPool.get(registration.pool) === registration) {
|
|
431
|
+
this.registrationByPool.delete(registration.pool);
|
|
432
|
+
}
|
|
433
|
+
this.refreshTimer();
|
|
434
|
+
}
|
|
435
|
+
refreshTimer() {
|
|
436
|
+
const shouldRun = [...this.registrations.values()].some((registration) => registration.active && registration.reapingEnabled);
|
|
437
|
+
if (shouldRun && this.intervalHandle === null) {
|
|
438
|
+
this.intervalHandle = this.timer.setInterval(() => {
|
|
439
|
+
void this.sweep().catch(() => undefined);
|
|
440
|
+
}, this.intervalMs);
|
|
441
|
+
const maybeUnref = this.intervalHandle;
|
|
442
|
+
maybeUnref?.unref?.();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!shouldRun && this.intervalHandle !== null) {
|
|
446
|
+
this.timer.clearInterval(this.intervalHandle);
|
|
447
|
+
this.intervalHandle = null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
emit(event) {
|
|
451
|
+
try {
|
|
452
|
+
this.onEvent?.(event);
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// Observability must not change lifecycle ownership or teardown behavior.
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const defaultLifecycleRegistry = new LifecycleRegistry();
|
|
460
|
+
/** Create a lifecycle registry for callers that do not inject one; use getLifecycleRegistry() for the shared default. */
|
|
461
|
+
export function createLifecycleRegistry(options = {}) {
|
|
462
|
+
return new LifecycleRegistry(options);
|
|
463
|
+
}
|
|
464
|
+
export function getLifecycleRegistry() {
|
|
465
|
+
return defaultLifecycleRegistry;
|
|
466
|
+
}
|
|
467
|
+
export { LifecycleRegistry as SubcLifecycleRegistry };
|
|
468
|
+
export function registerLifecyclePool(pool, options) {
|
|
469
|
+
return defaultLifecycleRegistry.registerLifecyclePool(pool, options);
|
|
470
|
+
}
|
|
471
|
+
//# sourceMappingURL=lifecycle-registry.js.map
|