@zhin.js/plugin-runtime 1.1.6 → 1.1.8
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 +3 -0
- package/lib/admission.js +25 -0
- package/lib/database-host.d.ts +1 -0
- package/lib/database-host.js +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/outbound-host.d.ts +25 -0
- package/lib/outbound-host.js +1 -0
- package/lib/plugin.d.ts +26 -0
- package/lib/plugin.js +22 -0
- package/lib/root-controller.d.ts +14 -13
- package/lib/root-controller.js +12 -7
- package/lib/schedule-host.d.ts +1 -1
- package/lib/schedule-host.js +1 -1
- package/lib/snapshot.d.ts +15 -4
- package/lib/snapshot.js +14 -2
- package/lib/token.d.ts +1 -0
- package/lib/token.js +41 -8
- package/package.json +1 -1
package/lib/admission.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare class GenerationAdmissionGate {
|
|
|
7
7
|
constructor();
|
|
8
8
|
get active(): boolean;
|
|
9
9
|
acquire(): (() => void) | undefined;
|
|
10
|
+
/** Observe the first commit which publishes this generation. */
|
|
11
|
+
onActivate(listener: () => void): () => void;
|
|
10
12
|
/**
|
|
11
13
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
12
14
|
* synchronous and must not fail; it is intended for closing process-owned
|
|
@@ -19,6 +21,7 @@ export declare function createGenerationAdmissionGate(): GenerationAdmissionGate
|
|
|
19
21
|
/** Resource hook used by CapabilityContext to bind an ingress dependency. */
|
|
20
22
|
export declare const generationAdmissionBinder: unique symbol;
|
|
21
23
|
export interface GenerationAdmissionBindable<T> {
|
|
24
|
+
/** @internal Runtime lifecycle wiring; never called by plugin authors. */
|
|
22
25
|
[generationAdmissionBinder](gate: GenerationAdmissionGate): T;
|
|
23
26
|
}
|
|
24
27
|
/** Snapshot value hook used to declare lifecycle-owned ingress gates. */
|
package/lib/admission.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export class GenerationAdmissionGate {
|
|
7
7
|
constructor() {
|
|
8
8
|
admissionState.set(this, { active: false });
|
|
9
|
+
admissionActivationListeners.set(this, new Set());
|
|
9
10
|
admissionDeactivationListeners.set(this, new Set());
|
|
10
11
|
}
|
|
11
12
|
get active() {
|
|
@@ -15,6 +16,18 @@ export class GenerationAdmissionGate {
|
|
|
15
16
|
const state = admissionState.get(this);
|
|
16
17
|
return state?.active ? state.acquire?.() : undefined;
|
|
17
18
|
}
|
|
19
|
+
/** Observe the first commit which publishes this generation. */
|
|
20
|
+
onActivate(listener) {
|
|
21
|
+
const listeners = admissionActivationListeners.get(this);
|
|
22
|
+
if (!listeners)
|
|
23
|
+
throw new Error('Unknown generation admission gate');
|
|
24
|
+
if (this.active) {
|
|
25
|
+
listener();
|
|
26
|
+
return () => undefined;
|
|
27
|
+
}
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => { listeners.delete(listener); };
|
|
30
|
+
}
|
|
18
31
|
/**
|
|
19
32
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
20
33
|
* synchronous and must not fail; it is intended for closing process-owned
|
|
@@ -50,6 +63,7 @@ export class GenerationAdmissionGate {
|
|
|
50
63
|
}
|
|
51
64
|
const admissionState = new WeakMap();
|
|
52
65
|
const admissionOwners = new WeakMap();
|
|
66
|
+
const admissionActivationListeners = new WeakMap();
|
|
53
67
|
const admissionDeactivationListeners = new WeakMap();
|
|
54
68
|
export function createGenerationAdmissionGate() {
|
|
55
69
|
return new GenerationAdmissionGate();
|
|
@@ -111,6 +125,17 @@ export function replaceGenerationAdmissions(previous, next, owner, acquireNext)
|
|
|
111
125
|
}
|
|
112
126
|
for (const gate of next) {
|
|
113
127
|
admissionState.set(gate, { active: true, acquire: acquireNext });
|
|
128
|
+
const listeners = admissionActivationListeners.get(gate);
|
|
129
|
+
for (const listener of listeners ?? []) {
|
|
130
|
+
try {
|
|
131
|
+
listener();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Admission publication is an infallible pointer switch. Buffered
|
|
135
|
+
// ingress reports through its own dispatch path.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
listeners?.clear();
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
function assertClaimableAdmission(gate, owner) {
|
package/lib/database-host.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ export declare function unwrapPluginDatabaseHost(host: DatabaseHost | PluginData
|
|
|
128
128
|
* The owner encoding keeps `root/a` and `root/a/b` disjoint, so a parent
|
|
129
129
|
* facade cannot accidentally enumerate a descendant's tables.
|
|
130
130
|
*/
|
|
131
|
+
/** @public Plugin-facing database facade bound to the current owner. */
|
|
131
132
|
export declare const databaseHostToken: import("./token.js").Token<PluginDatabaseHost>;
|
|
132
133
|
/** Root-only process host for CLI composition and Console administration. */
|
|
133
134
|
export declare const databaseRootHostToken: import("./token.js").Token<DatabaseHost>;
|
package/lib/database-host.js
CHANGED
|
@@ -66,6 +66,7 @@ function assertLogicalResourceName(name) {
|
|
|
66
66
|
* The owner encoding keeps `root/a` and `root/a/b` disjoint, so a parent
|
|
67
67
|
* facade cannot accidentally enumerate a descendant's tables.
|
|
68
68
|
*/
|
|
69
|
+
/** @public Plugin-facing database facade bound to the current owner. */
|
|
69
70
|
export const databaseHostToken = createToken('zhin.database.host', 'Plugin Runtime scoped database host');
|
|
70
71
|
/** Root-only process host for CLI composition and Console administration. */
|
|
71
72
|
export const databaseRootHostToken = createToken('zhin.database.root-host', 'Plugin Runtime root database host');
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/outbound-host.d.ts
CHANGED
|
@@ -42,6 +42,22 @@ export interface OutboundRecallInput {
|
|
|
42
42
|
readonly endpointKey: string;
|
|
43
43
|
readonly message: OutboundMessage;
|
|
44
44
|
}
|
|
45
|
+
export type OutboundEndpointOperation = 'recall' | 'edit' | 'reaction' | 'typing';
|
|
46
|
+
export interface OutboundEndpointInput {
|
|
47
|
+
readonly adapter: string;
|
|
48
|
+
readonly endpointKey: string;
|
|
49
|
+
}
|
|
50
|
+
export interface OutboundEndpointCapabilities {
|
|
51
|
+
readonly operations: readonly OutboundEndpointOperation[];
|
|
52
|
+
}
|
|
53
|
+
export interface OutboundEditInput extends OutboundEndpointInput {
|
|
54
|
+
readonly message: OutboundMessage;
|
|
55
|
+
readonly content: unknown;
|
|
56
|
+
}
|
|
57
|
+
export interface OutboundTypingInput extends OutboundEndpointInput {
|
|
58
|
+
readonly conversation: OutboundMessage['conversation'];
|
|
59
|
+
readonly active?: boolean;
|
|
60
|
+
}
|
|
45
61
|
/** Structured message identity; structurally compatible with IM MessageRef. */
|
|
46
62
|
export interface OutboundMessage {
|
|
47
63
|
readonly conversation: OutboundConversation & Readonly<{
|
|
@@ -53,6 +69,10 @@ export interface OutboundMessage {
|
|
|
53
69
|
readonly id: string;
|
|
54
70
|
}
|
|
55
71
|
export interface OutboundHost {
|
|
72
|
+
/** Runs related operations against one IM Runtime snapshot captured at ingress. */
|
|
73
|
+
runWithView?<T>(operation: () => Promise<T>): Promise<T>;
|
|
74
|
+
/** Exact operations declared by one live Endpoint. */
|
|
75
|
+
capabilities?(input: OutboundEndpointInput): OutboundEndpointCapabilities | undefined;
|
|
56
76
|
/** Returns platform message id when available (activity-feedback needs it). */
|
|
57
77
|
send(input: OutboundSendInput): Promise<string | null>;
|
|
58
78
|
/** Optional: platform message reactions (icqq group emoji, etc.). */
|
|
@@ -60,5 +80,10 @@ export interface OutboundHost {
|
|
|
60
80
|
removeReaction?(input: OutboundRemoveReactionInput): Promise<void>;
|
|
61
81
|
/** Optional: recall/delete a status message (activity-feedback autoRemove). */
|
|
62
82
|
recall?(input: OutboundRecallInput): Promise<void>;
|
|
83
|
+
/** Optional: update a previously sent status message. */
|
|
84
|
+
edit?(input: OutboundEditInput): Promise<string | null>;
|
|
85
|
+
/** Optional: toggle the platform-native typing indicator. */
|
|
86
|
+
typing?(input: OutboundTypingInput): Promise<void>;
|
|
63
87
|
}
|
|
88
|
+
/** @public Plugin-facing cross-platform outbound messaging Host. */
|
|
64
89
|
export declare const outboundHostToken: import("./token.js").Token<OutboundHost>;
|
package/lib/outbound-host.js
CHANGED
package/lib/plugin.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical plugin definition consumed from `zhin.js`.
|
|
3
|
+
* @module zhin.js
|
|
4
|
+
*/
|
|
1
5
|
import type { Dispose, DisposeStack } from './dispose.js';
|
|
2
6
|
import type { GenerationHandoffRegistry } from './handoff.js';
|
|
3
7
|
import type { FeatureId, PluginId } from './identity.js';
|
|
@@ -27,6 +31,7 @@ export interface PluginSetupContext<TConfig = unknown> {
|
|
|
27
31
|
/**
|
|
28
32
|
* Registers an in-memory definition through the same validation and
|
|
29
33
|
* projection transaction used by convention-discovered capabilities.
|
|
34
|
+
* @internal Runtime bridge for feature installers; plugin authors use convention files.
|
|
30
35
|
*/
|
|
31
36
|
addFeature<TDefinition>(feature: FeatureId | string, localName: string, definition: TDefinition): void;
|
|
32
37
|
}
|
|
@@ -36,4 +41,25 @@ export interface PluginDefinition<TConfig = unknown> {
|
|
|
36
41
|
readonly requires?: readonly Token<unknown>[];
|
|
37
42
|
setup?(context: PluginSetupContext<TConfig>): void | Dispose | Promise<void | Dispose>;
|
|
38
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Define the canonical `plugin.ts` entry point.
|
|
46
|
+
* Setup runs against one candidate generation and may return its disposer.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { definePlugin } from 'zhin.js';
|
|
52
|
+
*
|
|
53
|
+
* // plugin.ts
|
|
54
|
+
* export default definePlugin({
|
|
55
|
+
* name: 'hello',
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* Put capabilities in convention files such as `commands/hello.ts`, each with
|
|
60
|
+
* one default export.
|
|
61
|
+
*/
|
|
39
62
|
export declare function definePlugin<TConfig = unknown>(definition: PluginDefinition<TConfig>): Readonly<PluginDefinition<TConfig>>;
|
|
63
|
+
export { databaseHostToken, type DatabaseHostConsole, type DatabaseHostModel, type DatabaseHostSelection, type DatabaseHostSelectResult, type DatabaseHostTable, type DatabaseHostType, type PluginDatabaseHost, } from './database-host.js';
|
|
64
|
+
export { outboundHostToken, type OutboundConversation, type OutboundEditInput, type OutboundEndpointCapabilities, type OutboundEndpointInput, type OutboundEndpointOperation, type OutboundHost, type OutboundMessage, type OutboundReactionInput, type OutboundRecallInput, type OutboundRemoveReactionInput, type OutboundSendInput, type OutboundTypingInput, } from './outbound-host.js';
|
|
65
|
+
export { scheduleHostToken, type PluginScheduleHost, type ScheduleJobRegistration, } from './schedule-host.js';
|
package/lib/plugin.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
const pluginNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
2
|
+
/**
|
|
3
|
+
* Define the canonical `plugin.ts` entry point.
|
|
4
|
+
* Setup runs against one candidate generation and may return its disposer.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { definePlugin } from 'zhin.js';
|
|
10
|
+
*
|
|
11
|
+
* // plugin.ts
|
|
12
|
+
* export default definePlugin({
|
|
13
|
+
* name: 'hello',
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Put capabilities in convention files such as `commands/hello.ts`, each with
|
|
18
|
+
* one default export.
|
|
19
|
+
*/
|
|
2
20
|
export function definePlugin(definition) {
|
|
3
21
|
if (!pluginNamePattern.test(definition.name)) {
|
|
4
22
|
throw new TypeError(`Invalid plugin name: ${definition.name}`);
|
|
@@ -9,3 +27,7 @@ export function definePlugin(definition) {
|
|
|
9
27
|
requires: Object.freeze([...(definition.requires ?? [])]),
|
|
10
28
|
});
|
|
11
29
|
}
|
|
30
|
+
// The application facade exposes these Host contracts beside definePlugin.
|
|
31
|
+
export { databaseHostToken, } from './database-host.js';
|
|
32
|
+
export { outboundHostToken, } from './outbound-host.js';
|
|
33
|
+
export { scheduleHostToken, } from './schedule-host.js';
|
package/lib/root-controller.d.ts
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import type { PluginId } from './identity.js';
|
|
2
|
-
import { type PreparedGeneration, type RuntimeSnapshot, type SnapshotReader, type SnapshotState } from './snapshot.js';
|
|
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, signal: AbortSignal) => PreparedGeneration | Promise<PreparedGeneration
|
|
5
|
-
export type PrepareTransaction = (current: RuntimeSnapshot, signal: AbortSignal) => 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
|
|
11
|
+
export type GenerationCommitListener<TState = undefined> = (event: GenerationCommitEvent<TState>) => void | Promise<void>;
|
|
12
12
|
export declare class RootIntegrityError extends Error {
|
|
13
13
|
constructor(message: string, options?: ErrorOptions);
|
|
14
14
|
}
|
|
15
|
-
export declare class RootController {
|
|
15
|
+
export declare class RootController<TState = undefined> {
|
|
16
16
|
#private;
|
|
17
17
|
private readonly onControlError;
|
|
18
18
|
readonly snapshots: SnapshotReader;
|
|
19
|
-
constructor(initial: SnapshotState, onControlError?: ControlErrorHandler);
|
|
19
|
+
constructor(initial: SnapshotState, onControlError?: ControlErrorHandler, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
|
|
20
20
|
get state(): RootState;
|
|
21
21
|
get generation(): number;
|
|
22
22
|
get snapshot(): RuntimeSnapshot;
|
|
23
|
+
get committed(): CommittedGeneration<TState>;
|
|
23
24
|
/** Observe successful generation commits, including the initial start. */
|
|
24
|
-
onGenerationCommit(listener: GenerationCommitListener): () => void;
|
|
25
|
-
start(prepare: PrepareGeneration): Promise<RuntimeSnapshot>;
|
|
26
|
-
transact(prepare: PrepareTransaction): Promise<RuntimeSnapshot>;
|
|
27
|
-
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>;
|
|
28
29
|
stop(): Promise<void>;
|
|
29
30
|
}
|
package/lib/root-controller.js
CHANGED
|
@@ -16,9 +16,9 @@ export class RootController {
|
|
|
16
16
|
#stopRequested = false;
|
|
17
17
|
#activeTransaction;
|
|
18
18
|
#generationCommitListeners = new Set();
|
|
19
|
-
constructor(initial, onControlError = () => undefined) {
|
|
19
|
+
constructor(initial, onControlError = () => undefined, ...state) {
|
|
20
20
|
this.onControlError = onControlError;
|
|
21
|
-
this.#snapshotStore = new SnapshotStore(initial, (error) => this.#failIntegrity(error));
|
|
21
|
+
this.#snapshotStore = new SnapshotStore(initial, (error) => this.#failIntegrity(error), ...state);
|
|
22
22
|
const store = this.#snapshotStore;
|
|
23
23
|
this.snapshots = Object.freeze({
|
|
24
24
|
acquire: () => {
|
|
@@ -39,6 +39,9 @@ export class RootController {
|
|
|
39
39
|
get snapshot() {
|
|
40
40
|
return this.#snapshotStore.current;
|
|
41
41
|
}
|
|
42
|
+
get committed() {
|
|
43
|
+
return this.#snapshotStore.committed;
|
|
44
|
+
}
|
|
42
45
|
/** Observe successful generation commits, including the initial start. */
|
|
43
46
|
onGenerationCommit(listener) {
|
|
44
47
|
this.#generationCommitListeners.add(listener);
|
|
@@ -141,21 +144,23 @@ export class RootController {
|
|
|
141
144
|
return result;
|
|
142
145
|
}
|
|
143
146
|
#commitGeneration(expectedGeneration, prepared) {
|
|
144
|
-
const previous = this.#snapshotStore.
|
|
145
|
-
const
|
|
146
|
-
const event = Object.freeze({ previous, current
|
|
147
|
+
const previous = this.#snapshotStore.committed;
|
|
148
|
+
const current = this.#snapshotStore.commit(expectedGeneration, prepared);
|
|
149
|
+
const event = Object.freeze({ previous, current });
|
|
147
150
|
const listeners = [...this.#generationCommitListeners];
|
|
148
151
|
queueMicrotask(() => {
|
|
149
152
|
for (const listener of listeners) {
|
|
150
153
|
try {
|
|
151
|
-
listener(event)
|
|
154
|
+
void Promise.resolve(listener(event)).catch((error) => {
|
|
155
|
+
this.#reportControlError(error);
|
|
156
|
+
});
|
|
152
157
|
}
|
|
153
158
|
catch (error) {
|
|
154
159
|
this.#reportControlError(error);
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
162
|
});
|
|
158
|
-
return snapshot;
|
|
163
|
+
return current.snapshot;
|
|
159
164
|
}
|
|
160
165
|
#reportControlError(error) {
|
|
161
166
|
try {
|
package/lib/schedule-host.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare function unqualifyPluginScheduleId(owner: PluginId, id: string):
|
|
|
37
37
|
export declare function createPluginScheduleHost(owner: PluginId, host: ScheduleHost): PluginScheduleHost;
|
|
38
38
|
/** See unwrapPluginDatabaseHost: preserves legacy custom RootResource installers. */
|
|
39
39
|
export declare function unwrapPluginScheduleHost(host: ScheduleHost | PluginScheduleHost): ScheduleHost;
|
|
40
|
-
/** Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
|
40
|
+
/** @public Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
|
41
41
|
export declare const scheduleHostToken: import("./token.js").Token<PluginScheduleHost>;
|
|
42
42
|
/** Root-only process scheduler for CLI composition and Console administration. */
|
|
43
43
|
export declare const scheduleRootHostToken: import("./token.js").Token<ScheduleHost>;
|
package/lib/schedule-host.js
CHANGED
|
@@ -46,7 +46,7 @@ function assertLogicalJobId(id) {
|
|
|
46
46
|
throw new TypeError(`Invalid plugin schedule id: ${id}`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
/** Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
|
49
|
+
/** @public Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
|
50
50
|
export const scheduleHostToken = createToken('zhin.schedule.host', 'Plugin Runtime scoped solar cron host');
|
|
51
51
|
/** Root-only process scheduler for CLI composition and Console administration. */
|
|
52
52
|
export const scheduleRootHostToken = createToken('zhin.schedule.root-host', 'Plugin Runtime root solar cron host');
|
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;
|
|
@@ -46,14 +55,16 @@ export interface SnapshotReader {
|
|
|
46
55
|
acquire(): SnapshotLease;
|
|
47
56
|
owns(lease: SnapshotLease): boolean;
|
|
48
57
|
}
|
|
49
|
-
export declare class SnapshotStore implements SnapshotReader {
|
|
58
|
+
export declare class SnapshotStore<TState = undefined> implements SnapshotReader {
|
|
50
59
|
#private;
|
|
51
60
|
private readonly onDisposalError;
|
|
52
|
-
constructor(initial: SnapshotState, onDisposalError?: (error: unknown) => void);
|
|
61
|
+
constructor(initial: SnapshotState, onDisposalError?: (error: unknown) => void, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
|
|
53
62
|
get current(): RuntimeSnapshot;
|
|
63
|
+
/** Complete active record; snapshot and state always belong to one generation. */
|
|
64
|
+
get committed(): CommittedGeneration<TState>;
|
|
54
65
|
acquire(): SnapshotLease;
|
|
55
66
|
owns(lease: SnapshotLease): boolean;
|
|
56
|
-
commit(expectedGeneration: number, prepared: PreparedGeneration):
|
|
67
|
+
commit(expectedGeneration: number, prepared: PreparedGeneration<TState>): CommittedGeneration<TState>;
|
|
57
68
|
close(): Promise<void>;
|
|
58
69
|
}
|
|
59
70
|
export declare function createSnapshotView(generation: number, state: SnapshotState): RuntimeSnapshot;
|
package/lib/snapshot.js
CHANGED
|
@@ -36,12 +36,16 @@ export class SnapshotStore {
|
|
|
36
36
|
#retired = new Set();
|
|
37
37
|
#leases = new WeakSet();
|
|
38
38
|
#closed = false;
|
|
39
|
-
constructor(initial, onDisposalError = () => undefined) {
|
|
39
|
+
constructor(initial, onDisposalError = () => undefined, ...state) {
|
|
40
40
|
this.onDisposalError = onDisposalError;
|
|
41
|
+
const initialState = state[0];
|
|
41
42
|
const snapshot = createSnapshotView(0, initial);
|
|
43
|
+
const committed = Object.freeze({ snapshot, state: initialState });
|
|
42
44
|
const admissions = collectSnapshotAdmissions(snapshot);
|
|
43
45
|
const record = {
|
|
44
46
|
snapshot,
|
|
47
|
+
state: initialState,
|
|
48
|
+
committed,
|
|
45
49
|
dispose: () => undefined,
|
|
46
50
|
admissions,
|
|
47
51
|
leases: 0,
|
|
@@ -53,6 +57,10 @@ export class SnapshotStore {
|
|
|
53
57
|
get current() {
|
|
54
58
|
return this.#current.snapshot;
|
|
55
59
|
}
|
|
60
|
+
/** Complete active record; snapshot and state always belong to one generation. */
|
|
61
|
+
get committed() {
|
|
62
|
+
return this.#current.committed;
|
|
63
|
+
}
|
|
56
64
|
acquire() {
|
|
57
65
|
if (this.#closed)
|
|
58
66
|
throw new Error('SnapshotStore is closed');
|
|
@@ -75,9 +83,13 @@ export class SnapshotStore {
|
|
|
75
83
|
}
|
|
76
84
|
const previous = this.#current;
|
|
77
85
|
const snapshot = createSnapshotView(expectedGeneration + 1, prepared.snapshot);
|
|
86
|
+
const state = prepared.state;
|
|
87
|
+
const committed = Object.freeze({ snapshot, state });
|
|
78
88
|
const admissions = collectSnapshotAdmissions(snapshot);
|
|
79
89
|
const next = {
|
|
80
90
|
snapshot,
|
|
91
|
+
state,
|
|
92
|
+
committed,
|
|
81
93
|
dispose: prepared.dispose,
|
|
82
94
|
admissions,
|
|
83
95
|
leases: 0,
|
|
@@ -90,7 +102,7 @@ export class SnapshotStore {
|
|
|
90
102
|
previous.retired = true;
|
|
91
103
|
this.#retired.add(previous);
|
|
92
104
|
this.#disposeIfReady(previous);
|
|
93
|
-
return this.#current.
|
|
105
|
+
return this.#current.committed;
|
|
94
106
|
}
|
|
95
107
|
async close() {
|
|
96
108
|
if (this.#closed)
|
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
|
}
|