@zhin.js/plugin-runtime 1.1.17 → 1.1.18
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 +15 -3
- package/lib/admission.js +10 -5
- package/lib/data-migration.d.ts +105 -0
- package/lib/data-migration.js +16 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/outbound-host.d.ts +32 -9
- package/lib/outbound-host.js +3 -0
- package/lib/plugin-console-rpc.d.ts +11 -1
- package/lib/plugin-events.d.ts +47 -0
- package/lib/plugin-events.js +30 -0
- package/lib/plugin.d.ts +6 -1
- package/lib/plugin.js +2 -0
- package/lib/relational-target-migration.d.ts +68 -0
- package/lib/relational-target-migration.js +571 -0
- package/lib/root-controller.d.ts +2 -2
- package/lib/root-controller.js +2 -0
- package/lib/snapshot.d.ts +22 -5
- package/lib/snapshot.js +60 -14
- package/lib/token.d.ts +5 -1
- package/lib/token.js +13 -0
- package/package.json +1 -1
package/lib/admission.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export declare class GenerationAdmissionGate {
|
|
2
2
|
#private;
|
|
3
3
|
get active(): boolean;
|
|
4
|
+
/** Root closure fact, bound at first publication and retained after generation retirement. */
|
|
5
|
+
get stopSignal(): AbortSignal | undefined;
|
|
4
6
|
acquire(): (() => void) | undefined;
|
|
5
7
|
/** Observe the first commit which publishes this generation. */
|
|
6
8
|
onActivate(listener: () => void): () => void;
|
|
7
9
|
/**
|
|
8
10
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
9
|
-
* synchronous and must not fail; it
|
|
10
|
-
*
|
|
11
|
+
* synchronous and must not fail. It closes new admission; it does not decide
|
|
12
|
+
* whether already-owned transports or work should end. Use stopSignal for Root shutdown.
|
|
11
13
|
*/
|
|
12
14
|
onDeactivate(listener: () => void): () => void;
|
|
13
15
|
enter<T>(operation: () => T): T | undefined;
|
|
@@ -26,4 +28,14 @@ export interface GenerationAdmissionSource {
|
|
|
26
28
|
}
|
|
27
29
|
export declare function bindGenerationAdmission<T>(value: T, gate: GenerationAdmissionGate): T;
|
|
28
30
|
export declare function collectGenerationAdmissions(values: Iterable<unknown>): ReadonlySet<GenerationAdmissionGate>;
|
|
29
|
-
|
|
31
|
+
/** One SnapshotStore-owned publication; all gates share that Root's explicit close fact. */
|
|
32
|
+
interface GenerationAdmissionTransition {
|
|
33
|
+
readonly previous: ReadonlySet<GenerationAdmissionGate>;
|
|
34
|
+
readonly next: ReadonlySet<GenerationAdmissionGate>;
|
|
35
|
+
readonly owner: object;
|
|
36
|
+
readonly publishNext: () => void;
|
|
37
|
+
readonly acquireNext?: () => () => void;
|
|
38
|
+
readonly stopSignal: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
export declare function replaceGenerationAdmissions({ previous, next, owner, publishNext, acquireNext, stopSignal }: GenerationAdmissionTransition): void;
|
|
41
|
+
export {};
|
package/lib/admission.js
CHANGED
|
@@ -5,13 +5,14 @@ let admissionLifecycle;
|
|
|
5
5
|
export class GenerationAdmissionGate {
|
|
6
6
|
#state = { active: false };
|
|
7
7
|
#owner;
|
|
8
|
+
#stopSignal;
|
|
8
9
|
#activationListeners = new Set();
|
|
9
10
|
#deactivationListeners = new Set();
|
|
10
11
|
static {
|
|
11
12
|
admissionLifecycle = {
|
|
12
13
|
assertClaimable: (gate, owner) => gate.#assertClaimable(owner),
|
|
13
14
|
assertOwner: (gate, owner) => gate.#assertOwner(owner),
|
|
14
|
-
claim: (gate, owner) => { gate.#owner = owner; },
|
|
15
|
+
claim: (gate, owner, stopSignal) => { gate.#owner = owner; gate.#stopSignal = stopSignal; },
|
|
15
16
|
activate: (gate, acquire) => gate.#activate(acquire),
|
|
16
17
|
deactivate: (gate) => gate.#deactivate(),
|
|
17
18
|
notifyActivation: (gate) => gate.#notifyActivation(),
|
|
@@ -21,6 +22,10 @@ export class GenerationAdmissionGate {
|
|
|
21
22
|
get active() {
|
|
22
23
|
return this.#state.active;
|
|
23
24
|
}
|
|
25
|
+
/** Root closure fact, bound at first publication and retained after generation retirement. */
|
|
26
|
+
get stopSignal() {
|
|
27
|
+
return this.#stopSignal;
|
|
28
|
+
}
|
|
24
29
|
acquire() {
|
|
25
30
|
return this.#state.active ? this.#state.acquire?.() : undefined;
|
|
26
31
|
}
|
|
@@ -35,8 +40,8 @@ export class GenerationAdmissionGate {
|
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
42
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
38
|
-
* synchronous and must not fail; it
|
|
39
|
-
*
|
|
43
|
+
* synchronous and must not fail. It closes new admission; it does not decide
|
|
44
|
+
* whether already-owned transports or work should end. Use stopSignal for Root shutdown.
|
|
40
45
|
*/
|
|
41
46
|
onDeactivate(listener) {
|
|
42
47
|
if (!this.active) {
|
|
@@ -136,7 +141,7 @@ export function collectGenerationAdmissions(values) {
|
|
|
136
141
|
}
|
|
137
142
|
return result;
|
|
138
143
|
}
|
|
139
|
-
export function replaceGenerationAdmissions(previous, next, owner, publishNext, acquireNext) {
|
|
144
|
+
export function replaceGenerationAdmissions({ previous, next, owner, publishNext, acquireNext, stopSignal }) {
|
|
140
145
|
// Validate the complete switch before changing ownership or visibility. A
|
|
141
146
|
// rejected cross-Root gate must leave both generations exactly untouched.
|
|
142
147
|
for (const gate of next)
|
|
@@ -148,7 +153,7 @@ export function replaceGenerationAdmissions(previous, next, owner, publishNext,
|
|
|
148
153
|
// potentially rejecting ownership check has completed before publication.
|
|
149
154
|
publishNext();
|
|
150
155
|
for (const gate of next)
|
|
151
|
-
admissionLifecycle.claim(gate, owner);
|
|
156
|
+
admissionLifecycle.claim(gate, owner, stopSignal);
|
|
152
157
|
for (const gate of previous) {
|
|
153
158
|
if (next.has(gate))
|
|
154
159
|
continue;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { DeliveryTarget } from './delivery-target.js';
|
|
2
|
+
import type { GenerationAdmissionSource } from './admission.js';
|
|
3
|
+
import type { JsonValue } from './persistent-state.js';
|
|
4
|
+
/** Read-only, owner-namespaced historical data; candidate IDs are opaque to the Host. */
|
|
5
|
+
export interface DataMigrationCandidate {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly domain: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly sourceVersion: string;
|
|
10
|
+
readonly recordCount: number;
|
|
11
|
+
readonly beforeDigest: string;
|
|
12
|
+
readonly status: 'unresolved' | 'canonical' | 'corrupt';
|
|
13
|
+
/** Display facts only; never a preselected account or recovered authorization. */
|
|
14
|
+
readonly summary: Readonly<Record<string, JsonValue>>;
|
|
15
|
+
/** False when the configured persistence cannot supply the required atomic unit. */
|
|
16
|
+
readonly executable: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DataMigrationInventory {
|
|
19
|
+
readonly revision: string;
|
|
20
|
+
readonly candidates: readonly DataMigrationCandidate[];
|
|
21
|
+
}
|
|
22
|
+
export interface DataMigrationRequest {
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export interface PrepareDataMigrationRequest extends DataMigrationRequest {
|
|
26
|
+
/** Host-generated operation UUID; reused to reconcile a lost response. */
|
|
27
|
+
readonly planId: string;
|
|
28
|
+
readonly candidateId: string;
|
|
29
|
+
readonly expectedDigest: string;
|
|
30
|
+
/** Explicit administrator selection, validated by the Host against the account directory. */
|
|
31
|
+
readonly target: DeliveryTarget;
|
|
32
|
+
}
|
|
33
|
+
/** Domain-prepared exact unit; the Host persists the complete plan before admitting any mutation. */
|
|
34
|
+
export interface DataMigrationPlan {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly candidateId: string;
|
|
37
|
+
readonly domain: string;
|
|
38
|
+
readonly sourceVersion: string;
|
|
39
|
+
readonly beforeDigest: string;
|
|
40
|
+
readonly target: DeliveryTarget;
|
|
41
|
+
readonly recordCount: number;
|
|
42
|
+
/** Full lossless before-images and exact domain writes, not a 64 KiB state-store value. */
|
|
43
|
+
readonly data: JsonValue;
|
|
44
|
+
readonly description: readonly string[];
|
|
45
|
+
/** These facts require fresh authorization and must never be activated by a data migration. */
|
|
46
|
+
readonly authorizationCandidates: readonly JsonValue[];
|
|
47
|
+
}
|
|
48
|
+
export interface ApplyDataMigrationRequest extends DataMigrationRequest {
|
|
49
|
+
readonly plan: DataMigrationPlan;
|
|
50
|
+
}
|
|
51
|
+
/** Confirmed by the same atomic domain unit as the writes; unknown commits are never blindly retried. */
|
|
52
|
+
export interface DataMigrationReceipt {
|
|
53
|
+
/** Distinguishes an original commit from its explicit reversal during reconciliation. */
|
|
54
|
+
readonly operation: 'apply' | 'restore';
|
|
55
|
+
readonly planId: string;
|
|
56
|
+
readonly candidateId: string;
|
|
57
|
+
readonly beforeDigest: string;
|
|
58
|
+
readonly afterDigest: string;
|
|
59
|
+
readonly recordCount: number;
|
|
60
|
+
readonly status: 'applied' | 'already_applied';
|
|
61
|
+
readonly maintenance: 'paused';
|
|
62
|
+
}
|
|
63
|
+
export interface ResumeDataMigrationRequest extends DataMigrationRequest {
|
|
64
|
+
readonly plan: DataMigrationPlan;
|
|
65
|
+
}
|
|
66
|
+
export interface RestoreDataMigrationRequest extends ResumeDataMigrationRequest {
|
|
67
|
+
readonly receipt: DataMigrationReceipt;
|
|
68
|
+
}
|
|
69
|
+
export interface InspectDataMigrationRequest extends DataMigrationRequest {
|
|
70
|
+
readonly plan: DataMigrationPlan;
|
|
71
|
+
}
|
|
72
|
+
/** Verified current rows plus the durable maintenance journal, never inferred from a missing receipt. */
|
|
73
|
+
export interface DataMigrationInspection {
|
|
74
|
+
readonly data: 'not_applied' | 'applied' | 'restored' | 'conflict' | 'unknown';
|
|
75
|
+
readonly maintenance: 'paused' | 'resumed' | 'unknown';
|
|
76
|
+
/** True only for paused, drained scopes with a verified current recovery baseline; never authorizes data writes. */
|
|
77
|
+
readonly resumable: boolean;
|
|
78
|
+
/** A committed write receipt is not evidence that maintenance has resumed. */
|
|
79
|
+
readonly receipt?: DataMigrationReceipt;
|
|
80
|
+
}
|
|
81
|
+
/** Plugin owns record semantics, maintenance admission/drain and the existing transactional store. */
|
|
82
|
+
export interface PluginDataMigrationProvider extends GenerationAdmissionSource {
|
|
83
|
+
readonly title: string;
|
|
84
|
+
preview(request?: DataMigrationRequest): Promise<DataMigrationInventory>;
|
|
85
|
+
prepare(request: PrepareDataMigrationRequest): Promise<DataMigrationPlan>;
|
|
86
|
+
/** Enter the selected scope's maintenance, drain work, recheck and commit; leave it paused. */
|
|
87
|
+
apply(request: ApplyDataMigrationRequest): Promise<DataMigrationReceipt>;
|
|
88
|
+
/** Pure authoritative inspection; an in-flight mutation or unprovable state remains unknown. */
|
|
89
|
+
inspect(request: InspectDataMigrationRequest): Promise<DataMigrationInspection>;
|
|
90
|
+
/** Explicit resumption never starts an account, replays delivery or extends a game deadline. */
|
|
91
|
+
resume(request: ResumeDataMigrationRequest): Promise<void>;
|
|
92
|
+
/** Restore only when the actual rows still match the confirmed after-image; remain paused. */
|
|
93
|
+
restore(request: RestoreDataMigrationRequest): Promise<DataMigrationReceipt>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* commit_unknown means the data commit result is unconfirmed. invalid_input/conflict/unsupported/
|
|
97
|
+
* not_found guarantee no data commit by this invocation, but do not imply maintenance was released.
|
|
98
|
+
* unavailable does not establish either outcome. Inspect before recovery; never blindly replay.
|
|
99
|
+
*/
|
|
100
|
+
export declare class DataMigrationError extends Error {
|
|
101
|
+
readonly code: 'invalid_input' | 'conflict' | 'unsupported' | 'unavailable' | 'not_found' | 'commit_unknown';
|
|
102
|
+
constructor(code: 'invalid_input' | 'conflict' | 'unsupported' | 'unavailable' | 'not_found' | 'commit_unknown', message: string);
|
|
103
|
+
}
|
|
104
|
+
/** Existing owner Resource discovery, not a process-global migration registry. */
|
|
105
|
+
export declare const pluginDataMigrationProviderToken: import("./token.js").Token<PluginDataMigrationProvider>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createToken } from './token.js';
|
|
2
|
+
/**
|
|
3
|
+
* commit_unknown means the data commit result is unconfirmed. invalid_input/conflict/unsupported/
|
|
4
|
+
* not_found guarantee no data commit by this invocation, but do not imply maintenance was released.
|
|
5
|
+
* unavailable does not establish either outcome. Inspect before recovery; never blindly replay.
|
|
6
|
+
*/
|
|
7
|
+
export class DataMigrationError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'DataMigrationError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Existing owner Resource discovery, not a process-global migration registry. */
|
|
16
|
+
export const pluginDataMigrationProviderToken = createToken('zhin.plugin-data-migration', 'Plugin-owned historical data migration, controlled by authenticated Host operations');
|
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
/** @internal Runtime 内部:CapabilitySlot(feature 与 projection 之间的载体)。 */
|
|
6
6
|
export * from './capability.js';
|
|
7
7
|
export * from './dispose.js';
|
|
8
|
+
/** @internal Shared accepted-owner frame; no additional admission or task registry. */
|
|
9
|
+
export { OwnerOperation } from './owner-operation.js';
|
|
10
|
+
/** @internal Runtime-owned delivery authority; author contexts expose subscriptions only. */
|
|
11
|
+
export { createPluginEventChannel, type PluginEventChannel } from './plugin-events.js';
|
|
8
12
|
export * from './identity.js';
|
|
9
13
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, type GenerationAdmissionBindable, type GenerationAdmissionSource, } from './admission.js';
|
|
10
14
|
export * from './handoff.js';
|
|
@@ -41,3 +45,5 @@ export * from './plugin-verification.js';
|
|
|
41
45
|
export * from './plugin-tasks.js';
|
|
42
46
|
export * from './task-storage.js';
|
|
43
47
|
export * from './plugin-console-rpc.js';
|
|
48
|
+
export * from './data-migration.js';
|
|
49
|
+
export * from './relational-target-migration.js';
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
/** @internal Runtime 内部:CapabilitySlot(feature 与 projection 之间的载体)。 */
|
|
6
6
|
export * from './capability.js';
|
|
7
7
|
export * from './dispose.js';
|
|
8
|
+
/** @internal Shared accepted-owner frame; no additional admission or task registry. */
|
|
9
|
+
export { OwnerOperation } from './owner-operation.js';
|
|
10
|
+
/** @internal Runtime-owned delivery authority; author contexts expose subscriptions only. */
|
|
11
|
+
export { createPluginEventChannel } from './plugin-events.js';
|
|
8
12
|
export * from './identity.js';
|
|
9
13
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, } from './admission.js';
|
|
10
14
|
export * from './handoff.js';
|
|
@@ -41,3 +45,5 @@ export * from './plugin-verification.js';
|
|
|
41
45
|
export * from './plugin-tasks.js';
|
|
42
46
|
export * from './task-storage.js';
|
|
43
47
|
export * from './plugin-console-rpc.js';
|
|
48
|
+
export * from './data-migration.js';
|
|
49
|
+
export * from './relational-target-migration.js';
|
package/lib/outbound-host.d.ts
CHANGED
|
@@ -30,22 +30,16 @@ export interface OutboundSendInput {
|
|
|
30
30
|
readonly content: string;
|
|
31
31
|
}
|
|
32
32
|
export interface OutboundReactionInput {
|
|
33
|
-
readonly adapter: string;
|
|
34
|
-
readonly endpointKey: string;
|
|
35
33
|
readonly message: OutboundMessage;
|
|
36
34
|
readonly emoji: string;
|
|
37
35
|
readonly sceneType?: string;
|
|
38
36
|
readonly channelId?: string;
|
|
39
37
|
}
|
|
40
38
|
export interface OutboundRemoveReactionInput {
|
|
41
|
-
readonly adapter: string;
|
|
42
|
-
readonly endpointKey: string;
|
|
43
39
|
readonly message: OutboundMessage;
|
|
44
40
|
readonly reactionId: string;
|
|
45
41
|
}
|
|
46
42
|
export interface OutboundRecallInput {
|
|
47
|
-
readonly adapter: string;
|
|
48
|
-
readonly endpointKey: string;
|
|
49
43
|
readonly message: OutboundMessage;
|
|
50
44
|
}
|
|
51
45
|
export type OutboundEndpointOperation = 'recall' | 'edit' | 'reaction' | 'typing';
|
|
@@ -56,11 +50,11 @@ export interface OutboundEndpointInput {
|
|
|
56
50
|
export interface OutboundEndpointCapabilities {
|
|
57
51
|
readonly operations: readonly OutboundEndpointOperation[];
|
|
58
52
|
}
|
|
59
|
-
export interface OutboundEditInput
|
|
53
|
+
export interface OutboundEditInput {
|
|
60
54
|
readonly message: OutboundMessage;
|
|
61
55
|
readonly content: unknown;
|
|
62
56
|
}
|
|
63
|
-
export interface OutboundTypingInput
|
|
57
|
+
export interface OutboundTypingInput {
|
|
64
58
|
readonly conversation: OutboundMessage['conversation'];
|
|
65
59
|
readonly active?: boolean;
|
|
66
60
|
}
|
|
@@ -74,7 +68,34 @@ export interface OutboundMessage {
|
|
|
74
68
|
}>;
|
|
75
69
|
readonly id: string;
|
|
76
70
|
}
|
|
71
|
+
/** Retain a real source Client while its inbound operation is active. Never serialize this input. */
|
|
72
|
+
export interface OutboundCaptureInput {
|
|
73
|
+
readonly target: DeliveryTarget;
|
|
74
|
+
readonly client: unknown;
|
|
75
|
+
readonly signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
/** Messaging effects bound to one source incarnation, independent of reusable account names. */
|
|
78
|
+
export interface CapturedOutboundOperation {
|
|
79
|
+
readonly target: DeliveryTarget;
|
|
80
|
+
readonly signal: AbortSignal;
|
|
81
|
+
send(content: unknown, options?: Readonly<{
|
|
82
|
+
locale?: Locale;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}>): Promise<string | null>;
|
|
85
|
+
recall?(message: OutboundMessage): Promise<void>;
|
|
86
|
+
edit?(message: OutboundMessage, content: unknown): Promise<string | null>;
|
|
87
|
+
addReaction?(message: OutboundMessage, emoji: string, hint?: Readonly<{
|
|
88
|
+
sceneType?: string;
|
|
89
|
+
channelId?: string;
|
|
90
|
+
}>): Promise<string | null>;
|
|
91
|
+
removeReaction?(message: OutboundMessage, reactionId: string): Promise<void>;
|
|
92
|
+
typing?(conversation: OutboundMessage['conversation'], active?: boolean): Promise<void>;
|
|
93
|
+
/** Release the captured source and Root leases on every completion path. */
|
|
94
|
+
release(): void;
|
|
95
|
+
}
|
|
77
96
|
export interface OutboundHost {
|
|
97
|
+
/** Creates source-bound effects; ordinary persisted sends continue using sendTarget. */
|
|
98
|
+
captureOperation?(input: OutboundCaptureInput): CapturedOutboundOperation;
|
|
78
99
|
/** Sends to persisted exact identities, rejecting stale addresses instead of aliasing another bot. */
|
|
79
100
|
sendTarget?(target: DeliveryTarget, content: unknown, options?: Readonly<{
|
|
80
101
|
locale?: Locale;
|
|
@@ -99,7 +120,8 @@ export interface OutboundHost {
|
|
|
99
120
|
typing?(input: OutboundTypingInput): Promise<void>;
|
|
100
121
|
}
|
|
101
122
|
/** Root-only process port: requester is supplied by the owner-bound facade. */
|
|
102
|
-
export interface OutboundRootHost extends Omit<OutboundHost, 'sendTarget'> {
|
|
123
|
+
export interface OutboundRootHost extends Omit<OutboundHost, 'sendTarget' | 'captureOperation'> {
|
|
124
|
+
captureOperation?(input: OutboundCaptureInput, requester: PluginId): CapturedOutboundOperation;
|
|
103
125
|
sendTarget?(target: DeliveryTarget, content: unknown, requester: PluginId, options?: Readonly<{
|
|
104
126
|
locale?: Locale;
|
|
105
127
|
}>): Promise<string | null>;
|
|
@@ -107,6 +129,7 @@ export interface OutboundRootHost extends Omit<OutboundHost, 'sendTarget'> {
|
|
|
107
129
|
/** Preserve the sending plugin as requester, independent of the destination endpoint owner. */
|
|
108
130
|
export declare class PluginOutboundHost implements OutboundHost {
|
|
109
131
|
#private;
|
|
132
|
+
readonly captureOperation?: OutboundHost['captureOperation'];
|
|
110
133
|
readonly sendTarget?: OutboundHost['sendTarget'];
|
|
111
134
|
readonly runWithView?: OutboundHost['runWithView'];
|
|
112
135
|
readonly capabilities?: OutboundHost['capabilities'];
|
package/lib/outbound-host.js
CHANGED
|
@@ -3,6 +3,7 @@ import { generationAdmissionBinder, generationAdmissionSource } from './admissio
|
|
|
3
3
|
import { OwnerOperation } from './owner-operation.js';
|
|
4
4
|
/** Preserve the sending plugin as requester, independent of the destination endpoint owner. */
|
|
5
5
|
export class PluginOutboundHost {
|
|
6
|
+
captureOperation;
|
|
6
7
|
sendTarget;
|
|
7
8
|
runWithView;
|
|
8
9
|
capabilities;
|
|
@@ -20,6 +21,8 @@ export class PluginOutboundHost {
|
|
|
20
21
|
this.#host = host;
|
|
21
22
|
this.#admission = admission;
|
|
22
23
|
this.#operation = new OwnerOperation(admission, 'Outbound');
|
|
24
|
+
const captureOperation = host.captureOperation?.bind(host);
|
|
25
|
+
this.captureOperation = captureOperation ? input => this.#run(() => captureOperation(input, this.#owner)) : undefined;
|
|
23
26
|
const sendTarget = host.sendTarget?.bind(host);
|
|
24
27
|
const runWithView = host.runWithView?.bind(host);
|
|
25
28
|
const capabilities = host.capabilities?.bind(host);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GenerationAdmissionSource } from './admission.js';
|
|
2
|
+
import type { PluginId, FeatureId } from './identity.js';
|
|
2
3
|
import type { JsonValue } from './persistent-state.js';
|
|
3
4
|
/**
|
|
4
5
|
* Plugin-registered Console RPC (zhinjs/zhin#970).
|
|
@@ -22,6 +23,15 @@ export type PluginConsoleRpcInput = Readonly<Record<string, JsonValue>>;
|
|
|
22
23
|
export interface PluginConsoleRpcCall {
|
|
23
24
|
/** Aborted when the Host request times out. Long work must observe it. */
|
|
24
25
|
readonly signal: AbortSignal;
|
|
26
|
+
/** Authenticated operation view; no snapshot, configuration, resource or private resolver access. */
|
|
27
|
+
readonly runtime?: Readonly<{
|
|
28
|
+
owner: PluginId;
|
|
29
|
+
generation: number;
|
|
30
|
+
operator: boolean;
|
|
31
|
+
project(feature: FeatureId): unknown;
|
|
32
|
+
}>;
|
|
33
|
+
/** Framework-only request-local notification before a validated durable mutation begins. */
|
|
34
|
+
commitStarted?(): void;
|
|
25
35
|
}
|
|
26
36
|
export interface PluginConsoleRpcMethod<TInput = PluginConsoleRpcInput> {
|
|
27
37
|
/** Local name of a Page owned by the same plugin (`pages/<page>/index.tsx`). Its access rules apply. */
|
|
@@ -77,7 +87,7 @@ export interface PluginConsoleRpc extends GenerationAdmissionSource {
|
|
|
77
87
|
}
|
|
78
88
|
/** Safe, user-facing failure. `message` is shown in the Console; never put raw errors in it. */
|
|
79
89
|
export declare class PluginConsoleRpcError extends Error {
|
|
80
|
-
readonly code: 'invalid_input' | 'conflict' | 'not_found' | 'unsupported' | 'unavailable';
|
|
90
|
+
readonly code: 'invalid_input' | 'conflict' | 'not_found' | 'unsupported' | 'unavailable' | 'forbidden' | 'commit_unknown';
|
|
81
91
|
constructor(code: PluginConsoleRpcError['code'], message: string);
|
|
82
92
|
}
|
|
83
93
|
export declare const pluginConsoleRpcToken: import("./token.js").Token<PluginConsoleRpc>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Dispose } from './dispose.js';
|
|
2
|
+
import type { CapabilityId, PluginId } from './identity.js';
|
|
3
|
+
/** Planned invalidation is distinct from the instances actually replaced at commit. */
|
|
4
|
+
export interface HmrReloadScope {
|
|
5
|
+
readonly fullRoot: boolean;
|
|
6
|
+
readonly plugins: readonly PluginId[];
|
|
7
|
+
readonly capabilities: readonly CapabilityId[];
|
|
8
|
+
}
|
|
9
|
+
export interface HmrEventDetails {
|
|
10
|
+
readonly trigger: 'manual' | 'source';
|
|
11
|
+
readonly changed: readonly string[];
|
|
12
|
+
readonly planned: HmrReloadScope;
|
|
13
|
+
readonly previousGeneration: number;
|
|
14
|
+
readonly activeGeneration: number;
|
|
15
|
+
readonly receiver: Readonly<{
|
|
16
|
+
owner: PluginId;
|
|
17
|
+
creationGeneration: number;
|
|
18
|
+
state: 'current' | 'replaced';
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export type HmrOutcome = 'committed' | 'unchanged' | 'rejected' | 'cancelled' | 'restart-required';
|
|
22
|
+
export interface HmrResumeDetails extends HmrEventDetails {
|
|
23
|
+
readonly outcome: HmrOutcome;
|
|
24
|
+
readonly reason?: 'root-stopping';
|
|
25
|
+
readonly replaced: readonly PluginId[];
|
|
26
|
+
}
|
|
27
|
+
/** A control-phase signal, not a command to cancel the plugin's accepted business work. */
|
|
28
|
+
export interface PluginLifecycleEvent<TDetails> {
|
|
29
|
+
readonly signal: AbortSignal;
|
|
30
|
+
readonly details: Readonly<TDetails>;
|
|
31
|
+
}
|
|
32
|
+
export interface PluginLifecycleEventMap {
|
|
33
|
+
readonly 'hmr.pause': PluginLifecycleEvent<HmrEventDetails>;
|
|
34
|
+
readonly 'hmr.resume': PluginLifecycleEvent<HmrResumeDetails>;
|
|
35
|
+
}
|
|
36
|
+
export interface PluginEventSubscriptions {
|
|
37
|
+
/** Subscription belongs to this concrete plugin instance, including when its Scope is reused. */
|
|
38
|
+
on<TName extends keyof PluginLifecycleEventMap>(name: TName, handler: (event: PluginLifecycleEventMap[TName]) => void | Promise<void>): Dispose;
|
|
39
|
+
}
|
|
40
|
+
/** @internal Runtime delivery authority; never exposed through a setup context or resource snapshot. */
|
|
41
|
+
export interface PluginEventChannel {
|
|
42
|
+
readonly events: PluginEventSubscriptions;
|
|
43
|
+
dispatch<TName extends keyof PluginLifecycleEventMap>(name: TName, event: PluginLifecycleEventMap[TName]): Promise<void>;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
/** @internal One instance-owned subscription record; no inherited or process-wide listener registry. */
|
|
47
|
+
export declare function createPluginEventChannel(): PluginEventChannel;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** @internal One instance-owned subscription record; no inherited or process-wide listener registry. */
|
|
2
|
+
export function createPluginEventChannel() {
|
|
3
|
+
const handlers = {
|
|
4
|
+
'hmr.pause': new Set(), 'hmr.resume': new Set(),
|
|
5
|
+
};
|
|
6
|
+
let disposed = false;
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
events: Object.freeze({
|
|
9
|
+
on(name, handler) {
|
|
10
|
+
if (disposed)
|
|
11
|
+
throw new Error('Plugin event subscriptions are disposed');
|
|
12
|
+
handlers[name].add(handler);
|
|
13
|
+
return () => { handlers[name].delete(handler); };
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
async dispatch(name, event) {
|
|
17
|
+
if (disposed)
|
|
18
|
+
throw new Error('Plugin event subscriptions are disposed');
|
|
19
|
+
const results = await Promise.allSettled([...handlers[name]].map(handler => Promise.resolve().then(() => handler(event))));
|
|
20
|
+
const errors = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
21
|
+
if (errors.length)
|
|
22
|
+
throw new AggregateError(errors, `Plugin ${name} handlers failed`);
|
|
23
|
+
},
|
|
24
|
+
dispose() {
|
|
25
|
+
disposed = true;
|
|
26
|
+
handlers['hmr.pause'].clear();
|
|
27
|
+
handlers['hmr.resume'].clear();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
package/lib/plugin.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Dispose, DisposeStack } from './dispose.js';
|
|
|
6
6
|
import type { GenerationHandoffRegistry } from './handoff.js';
|
|
7
7
|
import type { FeatureId, PluginId } from './identity.js';
|
|
8
8
|
import type { Scope, Token } from './token.js';
|
|
9
|
+
import type { PluginEventSubscriptions } from './plugin-events.js';
|
|
9
10
|
export interface PluginMetadata {
|
|
10
11
|
readonly displayName?: string;
|
|
11
12
|
readonly icon?: string;
|
|
@@ -27,6 +28,7 @@ export interface PluginInstanceView {
|
|
|
27
28
|
readonly role: 'root' | 'child';
|
|
28
29
|
}
|
|
29
30
|
export interface PluginSetupContext<TConfig = unknown> {
|
|
31
|
+
readonly events: PluginEventSubscriptions;
|
|
30
32
|
readonly signal: AbortSignal;
|
|
31
33
|
readonly plugin: PluginInstanceView;
|
|
32
34
|
readonly config: ConfigView<TConfig>;
|
|
@@ -40,6 +42,7 @@ export interface PluginSetupContext<TConfig = unknown> {
|
|
|
40
42
|
*/
|
|
41
43
|
addFeature<TDefinition>(feature: FeatureId | string, localName: string, definition: TDefinition): void;
|
|
42
44
|
}
|
|
45
|
+
export { type PluginEventSubscriptions, type PluginLifecycleEvent, type PluginLifecycleEventMap, type HmrEventDetails, type HmrResumeDetails, type HmrReloadScope, type HmrOutcome, } from './plugin-events.js';
|
|
43
46
|
export interface PluginDefinition<TConfig = unknown> {
|
|
44
47
|
readonly name: string;
|
|
45
48
|
readonly config?: PluginConfigSchema<TConfig>;
|
|
@@ -69,5 +72,7 @@ export declare function definePlugin<TConfig = unknown>(definition: PluginDefini
|
|
|
69
72
|
export { createDeliveryTarget, type DeliveryTarget } from './delivery-target.js';
|
|
70
73
|
export { PluginStateStore, type JsonValue, type StateScope, type StateRecord, type PersistentStatePort, type StateWriteOptions } from './persistent-state.js';
|
|
71
74
|
export { databaseHostToken, type DatabaseHostConsole, type DatabaseHostModel, type DatabaseHostTransaction, type DatabaseHostSelection, type DatabaseHostSelectResult, type DatabaseHostTable, type DatabaseHostType, PluginDatabaseHost, } from './database-host.js';
|
|
72
|
-
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';
|
|
75
|
+
export { outboundHostToken, type OutboundConversation, type OutboundEditInput, type OutboundEndpointCapabilities, type OutboundEndpointInput, type OutboundEndpointOperation, type OutboundHost, type OutboundCaptureInput, type CapturedOutboundOperation, type OutboundMessage, type OutboundReactionInput, type OutboundRecallInput, type OutboundRemoveReactionInput, type OutboundSendInput, type OutboundTypingInput, } from './outbound-host.js';
|
|
73
76
|
export { scheduleHostToken, PluginScheduleHost, type ScheduleJobRegistration, } from './schedule-host.js';
|
|
77
|
+
export { DataMigrationError, pluginDataMigrationProviderToken, type DataMigrationCandidate, type DataMigrationInventory, type DataMigrationRequest, type PrepareDataMigrationRequest, type DataMigrationPlan, type ApplyDataMigrationRequest, type DataMigrationReceipt, type ResumeDataMigrationRequest, type RestoreDataMigrationRequest, type InspectDataMigrationRequest, type DataMigrationInspection, type PluginDataMigrationProvider, } from './data-migration.js';
|
|
78
|
+
export { TargetMigrationScopes, targetMigrationDigest, orderedMigrationRows, createRelationalTargetMigration, type TargetMigrationRows, type TargetMigrationWrite, type RelationalTargetMigrationUnit, type RelationalTargetMigrationOptions, } from './relational-target-migration.js';
|
package/lib/plugin.js
CHANGED
|
@@ -36,3 +36,5 @@ export { PluginStateStore } from './persistent-state.js';
|
|
|
36
36
|
export { databaseHostToken, PluginDatabaseHost, } from './database-host.js';
|
|
37
37
|
export { outboundHostToken, } from './outbound-host.js';
|
|
38
38
|
export { scheduleHostToken, PluginScheduleHost, } from './schedule-host.js';
|
|
39
|
+
export { DataMigrationError, pluginDataMigrationProviderToken, } from './data-migration.js';
|
|
40
|
+
export { TargetMigrationScopes, targetMigrationDigest, orderedMigrationRows, createRelationalTargetMigration, } from './relational-target-migration.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DatabaseHostTransaction, PluginDatabaseHost } from './database-host.js';
|
|
2
|
+
import { type DeliveryTarget } from './delivery-target.js';
|
|
3
|
+
import { type DataMigrationCandidate, type PluginDataMigrationProvider } from './data-migration.js';
|
|
4
|
+
import type { JsonValue } from './persistent-state.js';
|
|
5
|
+
type Row = Record<string, unknown>;
|
|
6
|
+
/** One business-key row set; identical keyless rows remain an indivisible group. */
|
|
7
|
+
export interface TargetMigrationRows {
|
|
8
|
+
readonly table: string;
|
|
9
|
+
readonly query: Readonly<Row>;
|
|
10
|
+
readonly rows: readonly Row[];
|
|
11
|
+
}
|
|
12
|
+
export interface TargetMigrationWrite {
|
|
13
|
+
readonly before: TargetMigrationRows;
|
|
14
|
+
readonly after: TargetMigrationRows;
|
|
15
|
+
readonly patch: Readonly<Row>;
|
|
16
|
+
}
|
|
17
|
+
export interface RelationalTargetMigrationUnit {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly scope: string;
|
|
21
|
+
readonly status: DataMigrationCandidate['status'];
|
|
22
|
+
readonly records: readonly TargetMigrationRows[];
|
|
23
|
+
readonly summary: Readonly<Record<string, JsonValue>>;
|
|
24
|
+
readonly authorizationCandidates?: readonly JsonValue[];
|
|
25
|
+
}
|
|
26
|
+
export interface RelationalTargetMigrationOptions {
|
|
27
|
+
/** Owner-qualified Host facade; raw process tables never enter the provider. */
|
|
28
|
+
readonly database: PluginDatabaseHost;
|
|
29
|
+
readonly domain: string;
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly sourceVersion: string;
|
|
32
|
+
/** All tables that this domain is allowed to read or change. */
|
|
33
|
+
readonly tables: readonly string[];
|
|
34
|
+
readonly scopes: TargetMigrationScopes;
|
|
35
|
+
readUnits(reader: DatabaseHostTransaction): Promise<readonly RelationalTargetMigrationUnit[]>;
|
|
36
|
+
/** Rewrites routing only. Domain state, deadlines, IDs and revision fields are retained. */
|
|
37
|
+
rewrite(unit: RelationalTargetMigrationUnit, target: DeliveryTarget): {
|
|
38
|
+
readonly writes: readonly TargetMigrationWrite[];
|
|
39
|
+
readonly destinationScope: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Stable, lossless digest; this does not impose the small-object StateStore size limit. */
|
|
43
|
+
export declare function targetMigrationDigest(value: unknown): string;
|
|
44
|
+
export declare function orderedMigrationRows(rows: readonly Row[]): readonly Row[];
|
|
45
|
+
/** Per-instance scope admission. Existing work drains; unrelated scopes continue. */
|
|
46
|
+
export declare class TargetMigrationScopes {
|
|
47
|
+
#private;
|
|
48
|
+
constructor(input: Readonly<{
|
|
49
|
+
pending: (scope: string) => boolean;
|
|
50
|
+
}>);
|
|
51
|
+
bindInitialization(initialize: () => Promise<void>): void;
|
|
52
|
+
run<T>(input: Readonly<{
|
|
53
|
+
scope: string;
|
|
54
|
+
run: () => Promise<T>;
|
|
55
|
+
}>): Promise<T>;
|
|
56
|
+
pause(input: Readonly<{
|
|
57
|
+
planId: string;
|
|
58
|
+
scopes: readonly string[];
|
|
59
|
+
persist: () => Promise<void>;
|
|
60
|
+
}>): Promise<void>;
|
|
61
|
+
drained(scopes: readonly string[]): boolean;
|
|
62
|
+
maintenance(planId: string, scopes: readonly string[]): 'paused' | 'resumed' | 'unknown';
|
|
63
|
+
restorePause(planId: string, scopes: readonly string[]): void;
|
|
64
|
+
resume(planId: string, scopes: readonly string[]): void;
|
|
65
|
+
}
|
|
66
|
+
/** Shared only by the concrete game and subscription target migrations. No raw-SQL or generic schema discovery. */
|
|
67
|
+
export declare function createRelationalTargetMigration(options: RelationalTargetMigrationOptions): PluginDataMigrationProvider;
|
|
68
|
+
export {};
|