@zhin.js/plugin-runtime 1.1.5 → 1.1.7
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/lib/admission.d.ts +31 -0
- package/lib/admission.js +133 -0
- package/lib/handoff.d.ts +7 -13
- package/lib/handoff.js +10 -34
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/outbound-host.d.ts +13 -3
- package/lib/plugin.d.ts +1 -0
- package/lib/root-controller.d.ts +19 -14
- package/lib/root-controller.js +82 -38
- package/lib/snapshot.d.ts +21 -4
- package/lib/snapshot.js +58 -14
- package/lib/token.d.ts +1 -0
- package/lib/token.js +41 -8
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingress gate owned by a lifecycle Resource and switched only by SnapshotStore.
|
|
3
|
+
* External callbacks may enter while the owning Resource is present in the
|
|
4
|
+
* committed snapshot; candidate and retired Resources fail closed.
|
|
5
|
+
*/
|
|
6
|
+
export declare class GenerationAdmissionGate {
|
|
7
|
+
constructor();
|
|
8
|
+
get active(): boolean;
|
|
9
|
+
acquire(): (() => void) | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Observe retirement of the generation that owns this gate. The callback is
|
|
12
|
+
* synchronous and must not fail; it is intended for closing process-owned
|
|
13
|
+
* ingress transports such as WebSocket connections.
|
|
14
|
+
*/
|
|
15
|
+
onDeactivate(listener: () => void): () => void;
|
|
16
|
+
enter<T>(operation: () => T): T | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function createGenerationAdmissionGate(): GenerationAdmissionGate;
|
|
19
|
+
/** Resource hook used by CapabilityContext to bind an ingress dependency. */
|
|
20
|
+
export declare const generationAdmissionBinder: unique symbol;
|
|
21
|
+
export interface GenerationAdmissionBindable<T> {
|
|
22
|
+
[generationAdmissionBinder](gate: GenerationAdmissionGate): T;
|
|
23
|
+
}
|
|
24
|
+
/** Snapshot value hook used to declare lifecycle-owned ingress gates. */
|
|
25
|
+
export declare const generationAdmissionSource: unique symbol;
|
|
26
|
+
export interface GenerationAdmissionSource {
|
|
27
|
+
readonly [generationAdmissionSource]: readonly GenerationAdmissionGate[];
|
|
28
|
+
}
|
|
29
|
+
export declare function bindGenerationAdmission<T>(value: T, gate: GenerationAdmissionGate): T;
|
|
30
|
+
export declare function collectGenerationAdmissions(values: Iterable<unknown>): ReadonlySet<GenerationAdmissionGate>;
|
|
31
|
+
export declare function replaceGenerationAdmissions(previous: ReadonlySet<GenerationAdmissionGate>, next: ReadonlySet<GenerationAdmissionGate>, owner: object, acquireNext?: () => () => void): void;
|
package/lib/admission.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingress gate owned by a lifecycle Resource and switched only by SnapshotStore.
|
|
3
|
+
* External callbacks may enter while the owning Resource is present in the
|
|
4
|
+
* committed snapshot; candidate and retired Resources fail closed.
|
|
5
|
+
*/
|
|
6
|
+
export class GenerationAdmissionGate {
|
|
7
|
+
constructor() {
|
|
8
|
+
admissionState.set(this, { active: false });
|
|
9
|
+
admissionDeactivationListeners.set(this, new Set());
|
|
10
|
+
}
|
|
11
|
+
get active() {
|
|
12
|
+
return admissionState.get(this)?.active === true;
|
|
13
|
+
}
|
|
14
|
+
acquire() {
|
|
15
|
+
const state = admissionState.get(this);
|
|
16
|
+
return state?.active ? state.acquire?.() : undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Observe retirement of the generation that owns this gate. The callback is
|
|
20
|
+
* synchronous and must not fail; it is intended for closing process-owned
|
|
21
|
+
* ingress transports such as WebSocket connections.
|
|
22
|
+
*/
|
|
23
|
+
onDeactivate(listener) {
|
|
24
|
+
const listeners = admissionDeactivationListeners.get(this);
|
|
25
|
+
if (!listeners)
|
|
26
|
+
throw new Error('Unknown generation admission gate');
|
|
27
|
+
if (!this.active) {
|
|
28
|
+
listener();
|
|
29
|
+
return () => undefined;
|
|
30
|
+
}
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => { listeners.delete(listener); };
|
|
33
|
+
}
|
|
34
|
+
enter(operation) {
|
|
35
|
+
const release = this.acquire();
|
|
36
|
+
if (!release)
|
|
37
|
+
return undefined;
|
|
38
|
+
try {
|
|
39
|
+
const result = operation();
|
|
40
|
+
if (isPromiseLike(result))
|
|
41
|
+
return Promise.resolve(result).finally(release);
|
|
42
|
+
release();
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
release();
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const admissionState = new WeakMap();
|
|
52
|
+
const admissionOwners = new WeakMap();
|
|
53
|
+
const admissionDeactivationListeners = new WeakMap();
|
|
54
|
+
export function createGenerationAdmissionGate() {
|
|
55
|
+
return new GenerationAdmissionGate();
|
|
56
|
+
}
|
|
57
|
+
/** Resource hook used by CapabilityContext to bind an ingress dependency. */
|
|
58
|
+
export const generationAdmissionBinder = Symbol('GenerationAdmissionBinder');
|
|
59
|
+
/** Snapshot value hook used to declare lifecycle-owned ingress gates. */
|
|
60
|
+
export const generationAdmissionSource = Symbol('GenerationAdmissionSource');
|
|
61
|
+
export function bindGenerationAdmission(value, gate) {
|
|
62
|
+
if (!isObject(value))
|
|
63
|
+
return value;
|
|
64
|
+
const binder = value[generationAdmissionBinder];
|
|
65
|
+
return typeof binder === 'function' ? binder.call(value, gate) : value;
|
|
66
|
+
}
|
|
67
|
+
export function collectGenerationAdmissions(values) {
|
|
68
|
+
const result = new Set();
|
|
69
|
+
for (const value of values) {
|
|
70
|
+
if (value instanceof GenerationAdmissionGate) {
|
|
71
|
+
result.add(value);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!isObject(value))
|
|
75
|
+
continue;
|
|
76
|
+
const gates = value[generationAdmissionSource];
|
|
77
|
+
if (!Array.isArray(gates))
|
|
78
|
+
continue;
|
|
79
|
+
for (const gate of gates) {
|
|
80
|
+
if (!(gate instanceof GenerationAdmissionGate)) {
|
|
81
|
+
throw new TypeError('Generation admission source returned an invalid gate');
|
|
82
|
+
}
|
|
83
|
+
result.add(gate);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
export function replaceGenerationAdmissions(previous, next, owner, acquireNext) {
|
|
89
|
+
// Validate the complete switch before changing ownership or visibility. A
|
|
90
|
+
// rejected cross-Root gate must leave both generations exactly untouched.
|
|
91
|
+
for (const gate of next)
|
|
92
|
+
assertClaimableAdmission(gate, owner);
|
|
93
|
+
for (const gate of previous) {
|
|
94
|
+
assertAdmissionOwner(gate, owner);
|
|
95
|
+
}
|
|
96
|
+
for (const gate of next)
|
|
97
|
+
admissionOwners.set(gate, owner);
|
|
98
|
+
for (const gate of previous) {
|
|
99
|
+
if (next.has(gate))
|
|
100
|
+
continue;
|
|
101
|
+
admissionState.set(gate, { active: false });
|
|
102
|
+
for (const listener of admissionDeactivationListeners.get(gate) ?? []) {
|
|
103
|
+
try {
|
|
104
|
+
listener();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Admission publication is an infallible pointer switch. Transport
|
|
108
|
+
// cleanup reports through its own lifecycle and cannot veto commit.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const gate of next) {
|
|
113
|
+
admissionState.set(gate, { active: true, acquire: acquireNext });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function assertClaimableAdmission(gate, owner) {
|
|
117
|
+
const current = admissionOwners.get(gate);
|
|
118
|
+
if (current && current !== owner) {
|
|
119
|
+
throw new Error('Generation admission gate belongs to another SnapshotStore');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertAdmissionOwner(gate, owner) {
|
|
123
|
+
if (admissionOwners.get(gate) !== owner) {
|
|
124
|
+
throw new Error('SnapshotStore does not own generation admission gate');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function isObject(value) {
|
|
128
|
+
return (typeof value === 'object' && value !== null) || typeof value === 'function';
|
|
129
|
+
}
|
|
130
|
+
function isPromiseLike(value) {
|
|
131
|
+
return isObject(value)
|
|
132
|
+
&& typeof value.then === 'function';
|
|
133
|
+
}
|
package/lib/handoff.d.ts
CHANGED
|
@@ -1,30 +1,24 @@
|
|
|
1
|
-
import type { RuntimeSnapshot } from './snapshot.js';
|
|
2
1
|
export interface GenerationHandoff {
|
|
3
|
-
|
|
4
|
-
activateNext(): void | Promise<void>;
|
|
2
|
+
activateNext(signal: AbortSignal): void | Promise<void>;
|
|
5
3
|
deactivateNext(): void | Promise<void>;
|
|
6
|
-
resumePrevious(): void | Promise<void>;
|
|
7
|
-
openNext(): void;
|
|
8
4
|
}
|
|
9
5
|
export interface GenerationHandoffParticipant {
|
|
10
|
-
|
|
11
|
-
activateNext?(): void | Promise<void>;
|
|
6
|
+
activateNext?(signal: AbortSignal): void | Promise<void>;
|
|
12
7
|
deactivateNext?(): void | Promise<void>;
|
|
13
|
-
resumePrevious?(): void | Promise<void>;
|
|
14
|
-
openNext?(): void;
|
|
15
8
|
}
|
|
16
9
|
export interface GenerationHandoffRegistry {
|
|
17
10
|
add(participant: GenerationHandoffParticipant): GenerationHandoffParticipant;
|
|
18
11
|
}
|
|
12
|
+
/** A transaction failed and its internal compensation also failed. */
|
|
13
|
+
export declare class GenerationCompensationError extends AggregateError {
|
|
14
|
+
constructor(errors: readonly unknown[], message: string, options?: ErrorOptions);
|
|
15
|
+
}
|
|
19
16
|
/** Composes owner-ordered Resource handoffs and compensates partial progress. */
|
|
20
17
|
export declare class GenerationHandoffStack implements GenerationHandoffRegistry, GenerationHandoff {
|
|
21
18
|
#private;
|
|
22
19
|
get size(): number;
|
|
23
20
|
add(participant: GenerationHandoffParticipant): GenerationHandoffParticipant;
|
|
24
21
|
seal(): GenerationHandoff | undefined;
|
|
25
|
-
|
|
26
|
-
activateNext(): Promise<void>;
|
|
22
|
+
activateNext(signal: AbortSignal): Promise<void>;
|
|
27
23
|
deactivateNext(): Promise<void>;
|
|
28
|
-
resumePrevious(): Promise<void>;
|
|
29
|
-
openNext(): void;
|
|
30
24
|
}
|
package/lib/handoff.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { DisposeStack } from './dispose.js';
|
|
2
|
+
/** A transaction failed and its internal compensation also failed. */
|
|
3
|
+
export class GenerationCompensationError extends AggregateError {
|
|
4
|
+
constructor(errors, message, options) {
|
|
5
|
+
super(errors, message, options);
|
|
6
|
+
this.name = 'GenerationCompensationError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
/** Composes owner-ordered Resource handoffs and compensates partial progress. */
|
|
3
10
|
export class GenerationHandoffStack {
|
|
4
11
|
#participants = [];
|
|
5
|
-
#quiesced = [];
|
|
6
12
|
#activated = [];
|
|
7
13
|
#sealed = false;
|
|
8
14
|
get size() {
|
|
@@ -18,27 +24,13 @@ export class GenerationHandoffStack {
|
|
|
18
24
|
this.#sealed = true;
|
|
19
25
|
return this.size > 0 ? this : undefined;
|
|
20
26
|
}
|
|
21
|
-
async
|
|
22
|
-
this.#assertSealed();
|
|
23
|
-
try {
|
|
24
|
-
for (const participant of [...this.#participants].reverse()) {
|
|
25
|
-
if (!participant.quiescePrevious)
|
|
26
|
-
continue;
|
|
27
|
-
await participant.quiescePrevious(previous);
|
|
28
|
-
this.#quiesced.push(participant);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
await compensate(error, () => this.resumePrevious(), 'Quiesce and resume both failed');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async activateNext() {
|
|
27
|
+
async activateNext(signal) {
|
|
36
28
|
this.#assertSealed();
|
|
37
29
|
try {
|
|
38
30
|
for (const participant of this.#participants) {
|
|
39
31
|
if (!participant.activateNext)
|
|
40
32
|
continue;
|
|
41
|
-
await participant.activateNext();
|
|
33
|
+
await participant.activateNext(signal);
|
|
42
34
|
this.#activated.push(participant);
|
|
43
35
|
}
|
|
44
36
|
}
|
|
@@ -49,22 +41,6 @@ export class GenerationHandoffStack {
|
|
|
49
41
|
async deactivateNext() {
|
|
50
42
|
await unwind(this.#activated, (participant) => participant.deactivateNext?.(), 'One or more next-generation Resources failed to deactivate');
|
|
51
43
|
}
|
|
52
|
-
async resumePrevious() {
|
|
53
|
-
await unwind(this.#quiesced, (participant) => participant.resumePrevious?.(), 'One or more previous-generation Resources failed to resume');
|
|
54
|
-
}
|
|
55
|
-
openNext() {
|
|
56
|
-
const errors = [];
|
|
57
|
-
for (const participant of this.#participants) {
|
|
58
|
-
try {
|
|
59
|
-
participant.openNext?.();
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
errors.push(error);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (errors.length > 0)
|
|
66
|
-
throw new AggregateError(errors, 'One or more Resources failed to open');
|
|
67
|
-
}
|
|
68
44
|
#assertSealed() {
|
|
69
45
|
if (!this.#sealed)
|
|
70
46
|
throw new Error('GenerationHandoffStack is not sealed');
|
|
@@ -75,7 +51,7 @@ async function compensate(primary, rollback, message) {
|
|
|
75
51
|
await rollback();
|
|
76
52
|
}
|
|
77
53
|
catch (rollbackError) {
|
|
78
|
-
throw new
|
|
54
|
+
throw new GenerationCompensationError([primary, rollbackError], message, { cause: rollbackError });
|
|
79
55
|
}
|
|
80
56
|
throw primary;
|
|
81
57
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './capability.js';
|
|
3
3
|
export * from './dispose.js';
|
|
4
4
|
export * from './identity.js';
|
|
5
|
+
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, type GenerationAdmissionBindable, type GenerationAdmissionSource, } from './admission.js';
|
|
5
6
|
export * from './generation-store.js';
|
|
6
7
|
export * from './handoff.js';
|
|
7
8
|
/** @public 用户侧创作面:`definePlugin`(`plugin.ts` 约定入口,承诺 semver)。 */
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './capability.js';
|
|
3
3
|
export * from './dispose.js';
|
|
4
4
|
export * from './identity.js';
|
|
5
|
+
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, } from './admission.js';
|
|
5
6
|
export * from './generation-store.js';
|
|
6
7
|
export * from './handoff.js';
|
|
7
8
|
/** @public 用户侧创作面:`definePlugin`(`plugin.ts` 约定入口,承诺 semver)。 */
|
package/lib/outbound-host.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface OutboundSendInput {
|
|
|
26
26
|
export interface OutboundReactionInput {
|
|
27
27
|
readonly adapter: string;
|
|
28
28
|
readonly endpointKey: string;
|
|
29
|
-
readonly
|
|
29
|
+
readonly message: OutboundMessage;
|
|
30
30
|
readonly emoji: string;
|
|
31
31
|
readonly sceneType?: string;
|
|
32
32
|
readonly channelId?: string;
|
|
@@ -34,13 +34,23 @@ export interface OutboundReactionInput {
|
|
|
34
34
|
export interface OutboundRemoveReactionInput {
|
|
35
35
|
readonly adapter: string;
|
|
36
36
|
readonly endpointKey: string;
|
|
37
|
-
readonly
|
|
37
|
+
readonly message: OutboundMessage;
|
|
38
38
|
readonly reactionId: string;
|
|
39
39
|
}
|
|
40
40
|
export interface OutboundRecallInput {
|
|
41
41
|
readonly adapter: string;
|
|
42
42
|
readonly endpointKey: string;
|
|
43
|
-
readonly
|
|
43
|
+
readonly message: OutboundMessage;
|
|
44
|
+
}
|
|
45
|
+
/** Structured message identity; structurally compatible with IM MessageRef. */
|
|
46
|
+
export interface OutboundMessage {
|
|
47
|
+
readonly conversation: OutboundConversation & Readonly<{
|
|
48
|
+
readonly endpoint: Readonly<{
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly adapter: string;
|
|
51
|
+
}>;
|
|
52
|
+
}>;
|
|
53
|
+
readonly id: string;
|
|
44
54
|
}
|
|
45
55
|
export interface OutboundHost {
|
|
46
56
|
/** Returns platform message id when available (activity-feedback needs it). */
|
package/lib/plugin.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface PluginInstanceView {
|
|
|
18
18
|
readonly role: 'root' | 'child';
|
|
19
19
|
}
|
|
20
20
|
export interface PluginSetupContext<TConfig = unknown> {
|
|
21
|
+
readonly signal: AbortSignal;
|
|
21
22
|
readonly plugin: PluginInstanceView;
|
|
22
23
|
readonly config: ConfigView<TConfig>;
|
|
23
24
|
readonly resources: Scope;
|
package/lib/root-controller.d.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import type { PluginId } from './identity.js';
|
|
2
|
-
import {
|
|
2
|
+
import { type CommittedGeneration, type PreparedGeneration, type RuntimeSnapshot, type SnapshotReader, type SnapshotState } from './snapshot.js';
|
|
3
3
|
export type RootState = 'idle' | 'running' | 'stopping' | 'stopped' | 'failed';
|
|
4
|
-
export type PrepareGeneration = (current: RuntimeSnapshot) => PreparedGeneration | Promise<PreparedGeneration
|
|
5
|
-
export type PrepareTransaction = (current: RuntimeSnapshot) => PreparedGeneration | undefined | Promise<PreparedGeneration | undefined>;
|
|
4
|
+
export type PrepareGeneration<TState = undefined> = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration<TState> | Promise<PreparedGeneration<TState>>;
|
|
5
|
+
export type PrepareTransaction<TState = undefined> = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration<TState> | undefined | Promise<PreparedGeneration<TState> | undefined>;
|
|
6
6
|
export type ControlErrorHandler = (error: unknown) => void;
|
|
7
|
-
export interface GenerationCommitEvent {
|
|
8
|
-
readonly previous:
|
|
9
|
-
readonly current:
|
|
7
|
+
export interface GenerationCommitEvent<TState = undefined> {
|
|
8
|
+
readonly previous: CommittedGeneration<TState>;
|
|
9
|
+
readonly current: CommittedGeneration<TState>;
|
|
10
10
|
}
|
|
11
|
-
export type GenerationCommitListener = (event: GenerationCommitEvent) => void
|
|
12
|
-
export declare class
|
|
11
|
+
export type GenerationCommitListener<TState = undefined> = (event: GenerationCommitEvent<TState>) => void | Promise<void>;
|
|
12
|
+
export declare class RootIntegrityError extends Error {
|
|
13
|
+
constructor(message: string, options?: ErrorOptions);
|
|
14
|
+
}
|
|
15
|
+
export declare class RootController<TState = undefined> {
|
|
13
16
|
#private;
|
|
14
17
|
private readonly onControlError;
|
|
15
|
-
readonly snapshots:
|
|
16
|
-
constructor(initial: SnapshotState, onControlError?: ControlErrorHandler);
|
|
18
|
+
readonly snapshots: SnapshotReader;
|
|
19
|
+
constructor(initial: SnapshotState, onControlError?: ControlErrorHandler, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
|
|
17
20
|
get state(): RootState;
|
|
18
21
|
get generation(): number;
|
|
22
|
+
get snapshot(): RuntimeSnapshot;
|
|
23
|
+
get committed(): CommittedGeneration<TState>;
|
|
19
24
|
/** Observe successful generation commits, including the initial start. */
|
|
20
|
-
onGenerationCommit(listener: GenerationCommitListener): () => void;
|
|
21
|
-
start(prepare: PrepareGeneration): Promise<RuntimeSnapshot>;
|
|
22
|
-
transact(prepare: PrepareTransaction): Promise<RuntimeSnapshot>;
|
|
23
|
-
reload(_target: PluginId | string, prepare: PrepareTransaction): Promise<RuntimeSnapshot>;
|
|
25
|
+
onGenerationCommit(listener: GenerationCommitListener<TState>): () => void;
|
|
26
|
+
start(prepare: PrepareGeneration<TState>): Promise<RuntimeSnapshot>;
|
|
27
|
+
transact(prepare: PrepareTransaction<TState>): Promise<RuntimeSnapshot>;
|
|
28
|
+
reload(_target: PluginId | string, prepare: PrepareTransaction<TState>): Promise<RuntimeSnapshot>;
|
|
24
29
|
stop(): Promise<void>;
|
|
25
30
|
}
|
package/lib/root-controller.js
CHANGED
|
@@ -1,19 +1,46 @@
|
|
|
1
|
+
import { GenerationCompensationError } from './handoff.js';
|
|
1
2
|
import { SnapshotStore, } from './snapshot.js';
|
|
3
|
+
export class RootIntegrityError extends Error {
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = 'RootIntegrityError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
export class RootController {
|
|
3
10
|
onControlError;
|
|
4
11
|
snapshots;
|
|
12
|
+
#snapshotStore;
|
|
5
13
|
#state = 'idle';
|
|
6
14
|
#tail = Promise.resolve();
|
|
15
|
+
#stopResult;
|
|
16
|
+
#stopRequested = false;
|
|
17
|
+
#activeTransaction;
|
|
7
18
|
#generationCommitListeners = new Set();
|
|
8
|
-
constructor(initial, onControlError = () => undefined) {
|
|
19
|
+
constructor(initial, onControlError = () => undefined, ...state) {
|
|
9
20
|
this.onControlError = onControlError;
|
|
10
|
-
this
|
|
21
|
+
this.#snapshotStore = new SnapshotStore(initial, (error) => this.#failIntegrity(error), ...state);
|
|
22
|
+
const store = this.#snapshotStore;
|
|
23
|
+
this.snapshots = Object.freeze({
|
|
24
|
+
acquire: () => {
|
|
25
|
+
if (this.#state !== 'running' || this.#stopRequested) {
|
|
26
|
+
throw new RootIntegrityError(`Root is not accepting generation operations (${this.#state})`);
|
|
27
|
+
}
|
|
28
|
+
return store.acquire();
|
|
29
|
+
},
|
|
30
|
+
owns: (lease) => store.owns(lease),
|
|
31
|
+
});
|
|
11
32
|
}
|
|
12
33
|
get state() {
|
|
13
34
|
return this.#state;
|
|
14
35
|
}
|
|
15
36
|
get generation() {
|
|
16
|
-
return this.
|
|
37
|
+
return this.#snapshotStore.current.generation;
|
|
38
|
+
}
|
|
39
|
+
get snapshot() {
|
|
40
|
+
return this.#snapshotStore.current;
|
|
41
|
+
}
|
|
42
|
+
get committed() {
|
|
43
|
+
return this.#snapshotStore.committed;
|
|
17
44
|
}
|
|
18
45
|
/** Observe successful generation commits, including the initial start. */
|
|
19
46
|
onGenerationCommit(listener) {
|
|
@@ -29,12 +56,15 @@ export class RootController {
|
|
|
29
56
|
}
|
|
30
57
|
let prepared;
|
|
31
58
|
let activated = false;
|
|
59
|
+
const transaction = new AbortController();
|
|
60
|
+
this.#activeTransaction = transaction;
|
|
32
61
|
try {
|
|
33
|
-
prepared = await prepare(this.
|
|
62
|
+
prepared = await prepare(this.#snapshotStore.current, transaction.signal);
|
|
34
63
|
if (prepared.handoff) {
|
|
35
64
|
activated = true;
|
|
36
|
-
await prepared.handoff.activateNext();
|
|
65
|
+
await prepared.handoff.activateNext(transaction.signal);
|
|
37
66
|
}
|
|
67
|
+
this.#assertPublishable('idle');
|
|
38
68
|
this.#state = 'running';
|
|
39
69
|
const snapshot = this.#commitGeneration(0, prepared);
|
|
40
70
|
prepared = undefined;
|
|
@@ -44,6 +74,10 @@ export class RootController {
|
|
|
44
74
|
this.#state = 'failed';
|
|
45
75
|
return this.#rollback(prepared, { activated }, error);
|
|
46
76
|
}
|
|
77
|
+
finally {
|
|
78
|
+
if (this.#activeTransaction === transaction)
|
|
79
|
+
this.#activeTransaction = undefined;
|
|
80
|
+
}
|
|
47
81
|
});
|
|
48
82
|
}
|
|
49
83
|
transact(prepare) {
|
|
@@ -53,26 +87,28 @@ export class RootController {
|
|
|
53
87
|
}
|
|
54
88
|
const previous = this.snapshots.acquire();
|
|
55
89
|
let prepared;
|
|
56
|
-
let quiesced = false;
|
|
57
90
|
let activated = false;
|
|
91
|
+
const transaction = new AbortController();
|
|
92
|
+
this.#activeTransaction = transaction;
|
|
58
93
|
try {
|
|
59
|
-
prepared = await prepare(previous.value);
|
|
94
|
+
prepared = await prepare(previous.value, transaction.signal);
|
|
60
95
|
if (!prepared)
|
|
61
96
|
return previous.value;
|
|
62
97
|
if (prepared.handoff) {
|
|
63
|
-
quiesced = true;
|
|
64
|
-
await prepared.handoff.quiescePrevious(previous.value);
|
|
65
98
|
activated = true;
|
|
66
|
-
await prepared.handoff.activateNext();
|
|
99
|
+
await prepared.handoff.activateNext(transaction.signal);
|
|
67
100
|
}
|
|
101
|
+
this.#assertPublishable('running');
|
|
68
102
|
const snapshot = this.#commitGeneration(previous.value.generation, prepared);
|
|
69
103
|
prepared = undefined;
|
|
70
104
|
return snapshot;
|
|
71
105
|
}
|
|
72
106
|
catch (error) {
|
|
73
|
-
return this.#rollback(prepared, {
|
|
107
|
+
return this.#rollback(prepared, { activated }, error);
|
|
74
108
|
}
|
|
75
109
|
finally {
|
|
110
|
+
if (this.#activeTransaction === transaction)
|
|
111
|
+
this.#activeTransaction = undefined;
|
|
76
112
|
previous.release();
|
|
77
113
|
}
|
|
78
114
|
});
|
|
@@ -81,16 +117,26 @@ export class RootController {
|
|
|
81
117
|
return this.transact(prepare);
|
|
82
118
|
}
|
|
83
119
|
stop() {
|
|
84
|
-
|
|
120
|
+
if (this.#stopResult)
|
|
121
|
+
return this.#stopResult;
|
|
122
|
+
this.#stopRequested = true;
|
|
123
|
+
this.#activeTransaction?.abort(new Error('Root lifecycle is stopping'));
|
|
124
|
+
const result = this.#enqueue(async () => {
|
|
85
125
|
if (this.#state === 'stopped')
|
|
86
126
|
return;
|
|
87
127
|
if (this.#state !== 'running' && this.#state !== 'failed') {
|
|
88
128
|
throw new Error(`Cannot stop RootController from ${this.#state}`);
|
|
89
129
|
}
|
|
90
130
|
this.#state = 'stopping';
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
try {
|
|
132
|
+
await this.#snapshotStore.close();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
this.#state = 'stopped';
|
|
136
|
+
}
|
|
93
137
|
});
|
|
138
|
+
this.#stopResult = result;
|
|
139
|
+
return result;
|
|
94
140
|
}
|
|
95
141
|
#enqueue(operation) {
|
|
96
142
|
const result = this.#tail.then(operation, operation);
|
|
@@ -98,30 +144,23 @@ export class RootController {
|
|
|
98
144
|
return result;
|
|
99
145
|
}
|
|
100
146
|
#commitGeneration(expectedGeneration, prepared) {
|
|
101
|
-
const previous = this.
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const event = Object.freeze({ previous, current: snapshot });
|
|
147
|
+
const previous = this.#snapshotStore.committed;
|
|
148
|
+
const current = this.#snapshotStore.commit(expectedGeneration, prepared);
|
|
149
|
+
const event = Object.freeze({ previous, current });
|
|
105
150
|
const listeners = [...this.#generationCommitListeners];
|
|
106
151
|
queueMicrotask(() => {
|
|
107
152
|
for (const listener of listeners) {
|
|
108
153
|
try {
|
|
109
|
-
listener(event)
|
|
154
|
+
void Promise.resolve(listener(event)).catch((error) => {
|
|
155
|
+
this.#reportControlError(error);
|
|
156
|
+
});
|
|
110
157
|
}
|
|
111
158
|
catch (error) {
|
|
112
159
|
this.#reportControlError(error);
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
162
|
});
|
|
116
|
-
return snapshot;
|
|
117
|
-
}
|
|
118
|
-
#openNext(handoff) {
|
|
119
|
-
try {
|
|
120
|
-
handoff?.openNext();
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
this.#reportControlError(error);
|
|
124
|
-
}
|
|
163
|
+
return current.snapshot;
|
|
125
164
|
}
|
|
126
165
|
#reportControlError(error) {
|
|
127
166
|
try {
|
|
@@ -141,14 +180,6 @@ export class RootController {
|
|
|
141
180
|
errors.push(error);
|
|
142
181
|
}
|
|
143
182
|
}
|
|
144
|
-
if (prepared && state.quiesced) {
|
|
145
|
-
try {
|
|
146
|
-
await prepared.handoff?.resumePrevious();
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
errors.push(error);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
183
|
if (prepared) {
|
|
153
184
|
try {
|
|
154
185
|
await prepared.dispose();
|
|
@@ -157,9 +188,22 @@ export class RootController {
|
|
|
157
188
|
errors.push(error);
|
|
158
189
|
}
|
|
159
190
|
}
|
|
160
|
-
if (errors.length > 1) {
|
|
161
|
-
|
|
191
|
+
if (errors.length > 1 || transactionError instanceof GenerationCompensationError) {
|
|
192
|
+
const aggregate = new AggregateError(errors, 'Generation transaction and rollback both failed');
|
|
193
|
+
this.#failIntegrity(aggregate);
|
|
194
|
+
throw new RootIntegrityError('Root integrity failed while rolling back a generation transaction', { cause: aggregate });
|
|
162
195
|
}
|
|
163
196
|
throw transactionError;
|
|
164
197
|
}
|
|
198
|
+
#failIntegrity(error) {
|
|
199
|
+
if (this.#state === 'stopped' || this.#state === 'stopping')
|
|
200
|
+
return;
|
|
201
|
+
this.#state = 'failed';
|
|
202
|
+
this.#reportControlError(error);
|
|
203
|
+
}
|
|
204
|
+
#assertPublishable(expected) {
|
|
205
|
+
if (this.#state !== expected || this.#stopRequested) {
|
|
206
|
+
throw new RootIntegrityError(`Root cannot publish a generation after admission entered ${this.#stopRequested ? 'stopping' : this.#state}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
165
209
|
}
|
package/lib/snapshot.d.ts
CHANGED
|
@@ -22,11 +22,20 @@ export interface RuntimeSnapshot {
|
|
|
22
22
|
readonly projections: ReadonlyMap<FeatureId, unknown>;
|
|
23
23
|
}
|
|
24
24
|
export type SnapshotState = Omit<RuntimeSnapshot, 'generation'>;
|
|
25
|
-
|
|
25
|
+
interface PreparedGenerationBase {
|
|
26
26
|
readonly snapshot: SnapshotState;
|
|
27
27
|
readonly dispose: Dispose;
|
|
28
28
|
readonly handoff?: GenerationHandoff;
|
|
29
29
|
}
|
|
30
|
+
export type PreparedGeneration<TState = undefined> = PreparedGenerationBase & ([TState] extends [undefined] ? {
|
|
31
|
+
readonly state?: undefined;
|
|
32
|
+
} : {
|
|
33
|
+
readonly state: TState;
|
|
34
|
+
});
|
|
35
|
+
export interface CommittedGeneration<TState = undefined> {
|
|
36
|
+
readonly snapshot: RuntimeSnapshot;
|
|
37
|
+
readonly state: TState;
|
|
38
|
+
}
|
|
30
39
|
export declare class GenerationConflictError extends Error {
|
|
31
40
|
readonly expected: number;
|
|
32
41
|
readonly actual: number;
|
|
@@ -41,13 +50,21 @@ export declare class SnapshotLease {
|
|
|
41
50
|
get active(): boolean;
|
|
42
51
|
release(): void;
|
|
43
52
|
}
|
|
44
|
-
|
|
53
|
+
/** Read-only operation seam exposed by a Root lifecycle. */
|
|
54
|
+
export interface SnapshotReader {
|
|
55
|
+
acquire(): SnapshotLease;
|
|
56
|
+
owns(lease: SnapshotLease): boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare class SnapshotStore<TState = undefined> implements SnapshotReader {
|
|
45
59
|
#private;
|
|
46
|
-
|
|
60
|
+
private readonly onDisposalError;
|
|
61
|
+
constructor(initial: SnapshotState, onDisposalError?: (error: unknown) => void, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
|
|
47
62
|
get current(): RuntimeSnapshot;
|
|
63
|
+
/** Complete active record; snapshot and state always belong to one generation. */
|
|
64
|
+
get committed(): CommittedGeneration<TState>;
|
|
48
65
|
acquire(): SnapshotLease;
|
|
49
66
|
owns(lease: SnapshotLease): boolean;
|
|
50
|
-
commit(expectedGeneration: number, prepared: PreparedGeneration):
|
|
67
|
+
commit(expectedGeneration: number, prepared: PreparedGeneration<TState>): CommittedGeneration<TState>;
|
|
51
68
|
close(): Promise<void>;
|
|
52
69
|
}
|
|
53
70
|
export declare function createSnapshotView(generation: number, state: SnapshotState): RuntimeSnapshot;
|
package/lib/snapshot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectGenerationAdmissions, replaceGenerationAdmissions, } from './admission.js';
|
|
1
2
|
export class GenerationConflictError extends Error {
|
|
2
3
|
expected;
|
|
3
4
|
actual;
|
|
@@ -30,30 +31,41 @@ export class SnapshotLease {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
export class SnapshotStore {
|
|
34
|
+
onDisposalError;
|
|
33
35
|
#current;
|
|
34
36
|
#retired = new Set();
|
|
35
37
|
#leases = new WeakSet();
|
|
36
38
|
#closed = false;
|
|
37
|
-
constructor(initial) {
|
|
38
|
-
this
|
|
39
|
-
|
|
39
|
+
constructor(initial, onDisposalError = () => undefined, ...state) {
|
|
40
|
+
this.onDisposalError = onDisposalError;
|
|
41
|
+
const initialState = state[0];
|
|
42
|
+
const snapshot = createSnapshotView(0, initial);
|
|
43
|
+
const committed = Object.freeze({ snapshot, state: initialState });
|
|
44
|
+
const admissions = collectSnapshotAdmissions(snapshot);
|
|
45
|
+
const record = {
|
|
46
|
+
snapshot,
|
|
47
|
+
state: initialState,
|
|
48
|
+
committed,
|
|
40
49
|
dispose: () => undefined,
|
|
50
|
+
admissions,
|
|
41
51
|
leases: 0,
|
|
42
52
|
retired: false,
|
|
43
53
|
};
|
|
54
|
+
replaceGenerationAdmissions(new Set(), admissions, this, () => this.#retain(record));
|
|
55
|
+
this.#current = record;
|
|
44
56
|
}
|
|
45
57
|
get current() {
|
|
46
58
|
return this.#current.snapshot;
|
|
47
59
|
}
|
|
60
|
+
/** Complete active record; snapshot and state always belong to one generation. */
|
|
61
|
+
get committed() {
|
|
62
|
+
return this.#current.committed;
|
|
63
|
+
}
|
|
48
64
|
acquire() {
|
|
49
65
|
if (this.#closed)
|
|
50
66
|
throw new Error('SnapshotStore is closed');
|
|
51
67
|
const record = this.#current;
|
|
52
|
-
record.
|
|
53
|
-
const lease = new SnapshotLease(record.snapshot, () => {
|
|
54
|
-
record.leases -= 1;
|
|
55
|
-
this.#disposeIfReady(record);
|
|
56
|
-
}, snapshotLeaseAuthority);
|
|
68
|
+
const lease = new SnapshotLease(record.snapshot, this.#retain(record), snapshotLeaseAuthority);
|
|
57
69
|
this.#leases.add(lease);
|
|
58
70
|
return lease;
|
|
59
71
|
}
|
|
@@ -70,23 +82,33 @@ export class SnapshotStore {
|
|
|
70
82
|
throw new GenerationConflictError(expectedGeneration, this.#current.snapshot.generation);
|
|
71
83
|
}
|
|
72
84
|
const previous = this.#current;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const snapshot = createSnapshotView(expectedGeneration + 1, prepared.snapshot);
|
|
86
|
+
const state = prepared.state;
|
|
87
|
+
const committed = Object.freeze({ snapshot, state });
|
|
88
|
+
const admissions = collectSnapshotAdmissions(snapshot);
|
|
89
|
+
const next = {
|
|
90
|
+
snapshot,
|
|
91
|
+
state,
|
|
92
|
+
committed,
|
|
77
93
|
dispose: prepared.dispose,
|
|
94
|
+
admissions,
|
|
78
95
|
leases: 0,
|
|
79
96
|
retired: false,
|
|
80
97
|
};
|
|
98
|
+
replaceGenerationAdmissions(previous.admissions, admissions, this, () => this.#retain(next));
|
|
99
|
+
// Commit only swaps the active pointer. The previous record may still be
|
|
100
|
+
// serving in-flight work, so disposal is deferred to its final lease.
|
|
101
|
+
this.#current = next;
|
|
81
102
|
previous.retired = true;
|
|
82
103
|
this.#retired.add(previous);
|
|
83
104
|
this.#disposeIfReady(previous);
|
|
84
|
-
return this.#current.
|
|
105
|
+
return this.#current.committed;
|
|
85
106
|
}
|
|
86
107
|
async close() {
|
|
87
108
|
if (this.#closed)
|
|
88
109
|
return;
|
|
89
110
|
this.#closed = true;
|
|
111
|
+
replaceGenerationAdmissions(this.#current.admissions, new Set(), this);
|
|
90
112
|
this.#current.retired = true;
|
|
91
113
|
this.#retired.add(this.#current);
|
|
92
114
|
this.#disposeIfReady(this.#current);
|
|
@@ -100,9 +122,25 @@ export class SnapshotStore {
|
|
|
100
122
|
return;
|
|
101
123
|
if (!record.disposing) {
|
|
102
124
|
record.disposing = Promise.resolve().then(record.dispose);
|
|
103
|
-
void record.disposing.then(() => record.drain?.resolve(), (error) =>
|
|
125
|
+
void record.disposing.then(() => record.drain?.resolve(), (error) => {
|
|
126
|
+
record.drain?.reject(error);
|
|
127
|
+
this.onDisposalError(error);
|
|
128
|
+
});
|
|
104
129
|
}
|
|
105
130
|
}
|
|
131
|
+
#retain(record) {
|
|
132
|
+
record.leases += 1;
|
|
133
|
+
let released = false;
|
|
134
|
+
return () => {
|
|
135
|
+
if (released)
|
|
136
|
+
return;
|
|
137
|
+
released = true;
|
|
138
|
+
record.leases -= 1;
|
|
139
|
+
if (record.leases < 0)
|
|
140
|
+
throw new Error('Snapshot lease underflow');
|
|
141
|
+
this.#disposeIfReady(record);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
106
144
|
#waitForDisposal(record) {
|
|
107
145
|
if (record.disposing)
|
|
108
146
|
return record.disposing;
|
|
@@ -118,6 +156,12 @@ export class SnapshotStore {
|
|
|
118
156
|
return record.drain.promise;
|
|
119
157
|
}
|
|
120
158
|
}
|
|
159
|
+
function collectSnapshotAdmissions(snapshot) {
|
|
160
|
+
return collectGenerationAdmissions([
|
|
161
|
+
...snapshot.projections.values(),
|
|
162
|
+
...[...snapshot.resources.values()].flatMap((resources) => [...resources.values()]),
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
121
165
|
export function createSnapshotView(generation, state) {
|
|
122
166
|
// Object.freeze(Map) does not disable Map#set. ReadonlyMapView removes the
|
|
123
167
|
// mutator interface at runtime, including from provider projection hooks.
|
package/lib/token.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface Token<T> {
|
|
|
6
6
|
readonly _type?: () => T;
|
|
7
7
|
}
|
|
8
8
|
export declare function createToken<T>(id: string, description?: string): Token<T>;
|
|
9
|
+
export declare function createRootPrivateToken<T>(id: string, description?: string): Token<T>;
|
|
9
10
|
export declare class Scope {
|
|
10
11
|
#private;
|
|
11
12
|
readonly owner: PluginId;
|
package/lib/token.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { DisposeStack } from './dispose.js';
|
|
2
2
|
import { tokenId } from './identity.js';
|
|
3
|
+
const rootPrivateTokens = new WeakSet();
|
|
3
4
|
export function createToken(id, description) {
|
|
4
5
|
return Object.freeze({ id: tokenId(id), description });
|
|
5
6
|
}
|
|
7
|
+
export function createRootPrivateToken(id, description) {
|
|
8
|
+
const token = { id: tokenId(id), description };
|
|
9
|
+
rootPrivateTokens.add(token);
|
|
10
|
+
return Object.freeze(token);
|
|
11
|
+
}
|
|
6
12
|
export class Scope {
|
|
7
13
|
owner;
|
|
8
14
|
parent;
|
|
@@ -20,20 +26,36 @@ export class Scope {
|
|
|
20
26
|
if (this.#bindings.has(token.id)) {
|
|
21
27
|
throw new Error(`Duplicate resource ${token.id} in ${this.owner}`);
|
|
22
28
|
}
|
|
23
|
-
this.#bindings.set(token.id, Object.freeze({
|
|
29
|
+
this.#bindings.set(token.id, Object.freeze({
|
|
30
|
+
owner: this.owner,
|
|
31
|
+
value,
|
|
32
|
+
rootPrivate: rootPrivateTokens.has(token),
|
|
33
|
+
token,
|
|
34
|
+
}));
|
|
24
35
|
if (dispose)
|
|
25
36
|
this.disposers.add(dispose);
|
|
26
37
|
}
|
|
27
38
|
use(token) {
|
|
28
39
|
const binding = this.#bindings.get(token.id);
|
|
29
|
-
if (binding)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
if (binding) {
|
|
41
|
+
if (!binding.rootPrivate || binding.token === token)
|
|
42
|
+
return binding.value;
|
|
43
|
+
throw new Error(`Missing resource ${token.id} for ${this.owner}`);
|
|
44
|
+
}
|
|
45
|
+
const inherited = this.parent
|
|
46
|
+
? this.parent.#resolveForDescendant(token.id)
|
|
47
|
+
: undefined;
|
|
48
|
+
if (inherited?.status === 'found')
|
|
49
|
+
return inherited.binding.value;
|
|
33
50
|
throw new Error(`Missing resource ${token.id} for ${this.owner}`);
|
|
34
51
|
}
|
|
35
52
|
has(token) {
|
|
36
|
-
|
|
53
|
+
const binding = this.#bindings.get(token.id);
|
|
54
|
+
if (binding)
|
|
55
|
+
return !binding.rootPrivate || binding.token === token;
|
|
56
|
+
return this.parent
|
|
57
|
+
? this.parent.#resolveForDescendant(token.id)?.status === 'found'
|
|
58
|
+
: false;
|
|
37
59
|
}
|
|
38
60
|
seal() {
|
|
39
61
|
this.#sealed = true;
|
|
@@ -45,8 +67,19 @@ export class Scope {
|
|
|
45
67
|
const result = this.parent
|
|
46
68
|
? new Map(this.parent.snapshot())
|
|
47
69
|
: new Map();
|
|
48
|
-
for (const [id, binding] of this.#bindings)
|
|
49
|
-
|
|
70
|
+
for (const [id, binding] of this.#bindings) {
|
|
71
|
+
if (!binding.rootPrivate)
|
|
72
|
+
result.set(id, binding.value);
|
|
73
|
+
}
|
|
50
74
|
return result;
|
|
51
75
|
}
|
|
76
|
+
#resolveForDescendant(id) {
|
|
77
|
+
const binding = this.#bindings.get(id);
|
|
78
|
+
if (binding) {
|
|
79
|
+
return binding.rootPrivate
|
|
80
|
+
? Object.freeze({ status: 'blocked' })
|
|
81
|
+
: Object.freeze({ status: 'found', binding });
|
|
82
|
+
}
|
|
83
|
+
return this.parent ? this.parent.#resolveForDescendant(id) : undefined;
|
|
84
|
+
}
|
|
52
85
|
}
|