@zhin.js/plugin-runtime 1.1.9 → 1.1.10
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 +1 -6
- package/lib/admission.js +62 -62
- package/lib/component-host.d.ts +1 -1
- package/lib/database-host.d.ts +15 -23
- package/lib/database-host.js +31 -54
- package/lib/inbox.d.ts +1 -1
- package/lib/inbox.js +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +4 -1
- package/lib/owner-scoped-resource-host.d.ts +9 -0
- package/lib/owner-scoped-resource-host.js +27 -0
- package/lib/plugin-configuration.d.ts +15 -0
- package/lib/plugin-configuration.js +26 -0
- package/lib/plugin.d.ts +3 -3
- package/lib/plugin.js +3 -3
- package/lib/root-config-document.d.ts +33 -0
- package/lib/root-config-document.js +116 -0
- package/lib/root-config-file.d.ts +9 -0
- package/lib/root-config-file.js +29 -0
- package/lib/schedule-host.d.ts +6 -8
- package/lib/schedule-host.js +23 -43
- package/lib/system-log.d.ts +1 -2
- package/lib/system-log.js +1 -2
- package/package.json +1 -1
- package/lib/generation-store.d.ts +0 -32
- package/lib/generation-store.js +0 -38
package/lib/admission.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
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
1
|
export declare class GenerationAdmissionGate {
|
|
7
|
-
|
|
2
|
+
#private;
|
|
8
3
|
get active(): boolean;
|
|
9
4
|
acquire(): (() => void) | undefined;
|
|
10
5
|
/** Observe the first commit which publishes this generation. */
|
package/lib/admission.js
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
1
|
+
// Assigned once by the class static block. The closures are a package-private
|
|
2
|
+
// friend seam: lifecycle publication can mutate #private state without adding
|
|
3
|
+
// Runtime-only methods to the public gate contract.
|
|
4
|
+
let admissionLifecycle;
|
|
6
5
|
export class GenerationAdmissionGate {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
#state = { active: false };
|
|
7
|
+
#owner;
|
|
8
|
+
#activationListeners = new Set();
|
|
9
|
+
#deactivationListeners = new Set();
|
|
10
|
+
static {
|
|
11
|
+
admissionLifecycle = {
|
|
12
|
+
assertClaimable: (gate, owner) => gate.#assertClaimable(owner),
|
|
13
|
+
assertOwner: (gate, owner) => gate.#assertOwner(owner),
|
|
14
|
+
claim: (gate, owner) => { gate.#owner = owner; },
|
|
15
|
+
activate: (gate, acquire) => gate.#activate(acquire),
|
|
16
|
+
deactivate: (gate) => gate.#deactivate(),
|
|
17
|
+
};
|
|
11
18
|
}
|
|
12
19
|
get active() {
|
|
13
|
-
return
|
|
20
|
+
return this.#state.active;
|
|
14
21
|
}
|
|
15
22
|
acquire() {
|
|
16
|
-
|
|
17
|
-
return state?.active ? state.acquire?.() : undefined;
|
|
23
|
+
return this.#state.active ? this.#state.acquire?.() : undefined;
|
|
18
24
|
}
|
|
19
25
|
/** Observe the first commit which publishes this generation. */
|
|
20
26
|
onActivate(listener) {
|
|
21
|
-
const listeners = admissionActivationListeners.get(this);
|
|
22
|
-
if (!listeners)
|
|
23
|
-
throw new Error('Unknown generation admission gate');
|
|
24
27
|
if (this.active) {
|
|
25
28
|
listener();
|
|
26
29
|
return () => undefined;
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
return () => {
|
|
31
|
+
this.#activationListeners.add(listener);
|
|
32
|
+
return () => { this.#activationListeners.delete(listener); };
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
@@ -34,15 +37,12 @@ export class GenerationAdmissionGate {
|
|
|
34
37
|
* ingress transports such as WebSocket connections.
|
|
35
38
|
*/
|
|
36
39
|
onDeactivate(listener) {
|
|
37
|
-
const listeners = admissionDeactivationListeners.get(this);
|
|
38
|
-
if (!listeners)
|
|
39
|
-
throw new Error('Unknown generation admission gate');
|
|
40
40
|
if (!this.active) {
|
|
41
41
|
listener();
|
|
42
42
|
return () => undefined;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
return () => {
|
|
44
|
+
this.#deactivationListeners.add(listener);
|
|
45
|
+
return () => { this.#deactivationListeners.delete(listener); };
|
|
46
46
|
}
|
|
47
47
|
enter(operation) {
|
|
48
48
|
const release = this.acquire();
|
|
@@ -60,11 +60,42 @@ export class GenerationAdmissionGate {
|
|
|
60
60
|
throw error;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
#assertClaimable(owner) {
|
|
64
|
+
if (this.#owner && this.#owner !== owner) {
|
|
65
|
+
throw new Error('Generation admission gate belongs to another SnapshotStore');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
#assertOwner(owner) {
|
|
69
|
+
if (this.#owner !== owner) {
|
|
70
|
+
throw new Error('SnapshotStore does not own generation admission gate');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#activate(acquire) {
|
|
74
|
+
this.#state = { active: true, acquire };
|
|
75
|
+
for (const listener of this.#activationListeners) {
|
|
76
|
+
try {
|
|
77
|
+
listener();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Admission publication is an infallible pointer switch. Buffered
|
|
81
|
+
// ingress reports through its own dispatch path.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.#activationListeners.clear();
|
|
85
|
+
}
|
|
86
|
+
#deactivate() {
|
|
87
|
+
this.#state = { active: false };
|
|
88
|
+
for (const listener of this.#deactivationListeners) {
|
|
89
|
+
try {
|
|
90
|
+
listener();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Admission publication is an infallible pointer switch. Transport
|
|
94
|
+
// cleanup reports through its own lifecycle and cannot veto commit.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
63
98
|
}
|
|
64
|
-
const admissionState = new WeakMap();
|
|
65
|
-
const admissionOwners = new WeakMap();
|
|
66
|
-
const admissionActivationListeners = new WeakMap();
|
|
67
|
-
const admissionDeactivationListeners = new WeakMap();
|
|
68
99
|
export function createGenerationAdmissionGate() {
|
|
69
100
|
return new GenerationAdmissionGate();
|
|
70
101
|
}
|
|
@@ -103,50 +134,19 @@ export function replaceGenerationAdmissions(previous, next, owner, acquireNext)
|
|
|
103
134
|
// Validate the complete switch before changing ownership or visibility. A
|
|
104
135
|
// rejected cross-Root gate must leave both generations exactly untouched.
|
|
105
136
|
for (const gate of next)
|
|
106
|
-
|
|
137
|
+
admissionLifecycle.assertClaimable(gate, owner);
|
|
107
138
|
for (const gate of previous) {
|
|
108
|
-
|
|
139
|
+
admissionLifecycle.assertOwner(gate, owner);
|
|
109
140
|
}
|
|
110
141
|
for (const gate of next)
|
|
111
|
-
|
|
142
|
+
admissionLifecycle.claim(gate, owner);
|
|
112
143
|
for (const gate of previous) {
|
|
113
144
|
if (next.has(gate))
|
|
114
145
|
continue;
|
|
115
|
-
|
|
116
|
-
for (const listener of admissionDeactivationListeners.get(gate) ?? []) {
|
|
117
|
-
try {
|
|
118
|
-
listener();
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// Admission publication is an infallible pointer switch. Transport
|
|
122
|
-
// cleanup reports through its own lifecycle and cannot veto commit.
|
|
123
|
-
}
|
|
124
|
-
}
|
|
146
|
+
admissionLifecycle.deactivate(gate);
|
|
125
147
|
}
|
|
126
148
|
for (const gate of next) {
|
|
127
|
-
|
|
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();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function assertClaimableAdmission(gate, owner) {
|
|
142
|
-
const current = admissionOwners.get(gate);
|
|
143
|
-
if (current && current !== owner) {
|
|
144
|
-
throw new Error('Generation admission gate belongs to another SnapshotStore');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function assertAdmissionOwner(gate, owner) {
|
|
148
|
-
if (admissionOwners.get(gate) !== owner) {
|
|
149
|
-
throw new Error('SnapshotStore does not own generation admission gate');
|
|
149
|
+
admissionLifecycle.activate(gate, acquireNext);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
function isObject(value) {
|
package/lib/component-host.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface TemplateContext {
|
|
|
39
39
|
/**
|
|
40
40
|
* Thin Host Resource for Plugin Runtime outbound template compilation.
|
|
41
41
|
* Evaluates `${expr}` template expressions in a sandboxed VM context
|
|
42
|
-
*
|
|
42
|
+
* The Host keeps render authority inside the active Runtime operation.
|
|
43
43
|
*
|
|
44
44
|
* Implemented by the CLI Host; absent when not wired — consumers must
|
|
45
45
|
* pass text through unchanged.
|
package/lib/database-host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PluginId } from './identity.js';
|
|
2
|
+
import { OwnerScopedResourceHost } from './owner-scoped-resource-host.js';
|
|
2
3
|
export interface DatabaseHostSelection {
|
|
3
4
|
where(query: Record<string, unknown>): DatabaseHostSelection;
|
|
4
5
|
orderBy(field: string, direction?: 'ASC' | 'DESC'): DatabaseHostSelection;
|
|
@@ -96,32 +97,23 @@ export interface DatabaseHost {
|
|
|
96
97
|
* the Runtime maps them to private physical table names before they reach the
|
|
97
98
|
* process-wide DatabaseHost.
|
|
98
99
|
*/
|
|
99
|
-
export interface PluginDatabaseHost {
|
|
100
|
-
readonly owner: PluginId;
|
|
101
|
-
readonly dialect: string;
|
|
102
|
-
readonly started: boolean;
|
|
103
|
-
define(name: string, definition: Record<string, unknown>): void;
|
|
104
|
-
tables(): readonly string[];
|
|
105
|
-
models: {
|
|
106
|
-
get(name: string): DatabaseHostModel | undefined;
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
100
|
/**
|
|
110
101
|
* Maps a plugin's logical resource name to its process-wide physical name.
|
|
111
|
-
*
|
|
112
|
-
* database migration merely to adopt scoped child plugins.
|
|
102
|
+
* Every owner, including the root plugin, uses the same private namespace.
|
|
113
103
|
*/
|
|
114
104
|
export declare function qualifyPluginResourceName(owner: PluginId, name: string): string;
|
|
115
|
-
/**
|
|
116
|
-
export declare
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
105
|
+
/** Owner-bound persistence facade without Console administration or raw DB access. */
|
|
106
|
+
export declare class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
107
|
+
private readonly host;
|
|
108
|
+
readonly models: {
|
|
109
|
+
get(name: string): DatabaseHostModel | undefined;
|
|
110
|
+
};
|
|
111
|
+
constructor(owner: PluginId, host: DatabaseHost);
|
|
112
|
+
get dialect(): string;
|
|
113
|
+
get started(): boolean;
|
|
114
|
+
define(name: string, definition: Record<string, unknown>): void;
|
|
115
|
+
tables(): readonly string[];
|
|
116
|
+
}
|
|
125
117
|
/**
|
|
126
118
|
* Plugin-facing scoped persistence token. Runtime replaces this binding for
|
|
127
119
|
* every Plugin Scope; plugin code must never receive the process-wide host.
|
package/lib/database-host.js
CHANGED
|
@@ -1,63 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OwnerScopedResourceHost, qualifyOwnedResourceName, } from './owner-scoped-resource-host.js';
|
|
2
2
|
import { createToken } from './token.js';
|
|
3
|
-
const resourcePrefix = '__zhin_plugin__';
|
|
4
|
-
const resourceSeparator = '__';
|
|
5
|
-
const roots = new WeakMap();
|
|
6
3
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Persistence surface visible to a plugin. Names are logical to the owner;
|
|
5
|
+
* the Runtime maps them to private physical table names before they reach the
|
|
6
|
+
* process-wide DatabaseHost.
|
|
10
7
|
*/
|
|
11
|
-
export function qualifyPluginResourceName(owner, name) {
|
|
12
|
-
assertLogicalResourceName(name);
|
|
13
|
-
if (owner === rootPluginId())
|
|
14
|
-
return name;
|
|
15
|
-
return `${resourcePrefix}${pluginOwnerResourceKey(owner)}${resourceSeparator}${name}`;
|
|
16
|
-
}
|
|
17
|
-
/** Reverse `qualifyPluginResourceName` only when the name belongs to owner. */
|
|
18
|
-
export function unqualifyPluginResourceName(owner, name) {
|
|
19
|
-
if (owner === rootPluginId())
|
|
20
|
-
return name.startsWith(resourcePrefix) ? undefined : name;
|
|
21
|
-
const prefix = `${resourcePrefix}${pluginOwnerResourceKey(owner)}${resourceSeparator}`;
|
|
22
|
-
return name.startsWith(prefix) ? name.slice(prefix.length) : undefined;
|
|
23
|
-
}
|
|
24
|
-
/** Creates a tenant facade without exposing Console administration or raw DB access. */
|
|
25
|
-
export function createPluginDatabaseHost(owner, host) {
|
|
26
|
-
const facade = Object.freeze({
|
|
27
|
-
owner,
|
|
28
|
-
get dialect() { return host.dialect; },
|
|
29
|
-
get started() { return host.started; },
|
|
30
|
-
define(name, definition) {
|
|
31
|
-
host.define(qualifyPluginResourceName(owner, name), definition);
|
|
32
|
-
},
|
|
33
|
-
tables() {
|
|
34
|
-
return Object.freeze(host.tables().flatMap((name) => {
|
|
35
|
-
const logical = unqualifyPluginResourceName(owner, name);
|
|
36
|
-
return logical === undefined ? [] : [logical];
|
|
37
|
-
}));
|
|
38
|
-
},
|
|
39
|
-
models: Object.freeze({
|
|
40
|
-
get(name) {
|
|
41
|
-
return host.models.get(qualifyPluginResourceName(owner, name));
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
});
|
|
45
|
-
roots.set(facade, host);
|
|
46
|
-
return facade;
|
|
47
|
-
}
|
|
48
8
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* token working while child scopes begin receiving tenant boundaries.
|
|
9
|
+
* Maps a plugin's logical resource name to its process-wide physical name.
|
|
10
|
+
* Every owner, including the root plugin, uses the same private namespace.
|
|
52
11
|
*/
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
return host;
|
|
56
|
-
return roots.get(host);
|
|
12
|
+
export function qualifyPluginResourceName(owner, name) {
|
|
13
|
+
return qualifyOwnedResourceName(owner, name);
|
|
57
14
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
15
|
+
/** Owner-bound persistence facade without Console administration or raw DB access. */
|
|
16
|
+
export class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
17
|
+
host;
|
|
18
|
+
models;
|
|
19
|
+
constructor(owner, host) {
|
|
20
|
+
super(owner);
|
|
21
|
+
this.host = host;
|
|
22
|
+
this.models = Object.freeze({ get: (name) => this.host.models.get(this.qualify(name)) });
|
|
23
|
+
}
|
|
24
|
+
get dialect() {
|
|
25
|
+
return this.host.dialect;
|
|
26
|
+
}
|
|
27
|
+
get started() {
|
|
28
|
+
return this.host.started;
|
|
29
|
+
}
|
|
30
|
+
define(name, definition) {
|
|
31
|
+
this.host.define(this.qualify(name), definition);
|
|
32
|
+
}
|
|
33
|
+
tables() {
|
|
34
|
+
return Object.freeze(this.host.tables().flatMap((name) => {
|
|
35
|
+
const logical = this.unqualify(name);
|
|
36
|
+
return logical === undefined ? [] : [logical];
|
|
37
|
+
}));
|
|
61
38
|
}
|
|
62
39
|
}
|
|
63
40
|
/**
|
package/lib/inbox.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { DatabaseHost } from './database-host.js';
|
|
|
5
5
|
* 列结构对齐 packages/im/zhin/src/setup/register-inbox.ts(legacy 读路径 SSOT),
|
|
6
6
|
* request/notice 额外补 consumed/consumed_at 列(对齐 legacy host-api
|
|
7
7
|
* endpoint-persistence 的 console_bot_requests/notices consumed 标记方式),
|
|
8
|
-
* 供 console-rpc-extended 的
|
|
8
|
+
* 供 console-rpc-extended 的 request.consumed / notice.consumed 写路径使用。
|
|
9
9
|
*/
|
|
10
10
|
export declare const INBOX_TABLE_MESSAGE = "unified_inbox_message";
|
|
11
11
|
export declare const INBOX_TABLE_REQUEST = "unified_inbox_request";
|
package/lib/inbox.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createToken } from './token.js';
|
|
|
5
5
|
* 列结构对齐 packages/im/zhin/src/setup/register-inbox.ts(legacy 读路径 SSOT),
|
|
6
6
|
* request/notice 额外补 consumed/consumed_at 列(对齐 legacy host-api
|
|
7
7
|
* endpoint-persistence 的 console_bot_requests/notices consumed 标记方式),
|
|
8
|
-
* 供 console-rpc-extended 的
|
|
8
|
+
* 供 console-rpc-extended 的 request.consumed / notice.consumed 写路径使用。
|
|
9
9
|
*/
|
|
10
10
|
export const INBOX_TABLE_MESSAGE = 'unified_inbox_message';
|
|
11
11
|
export const INBOX_TABLE_REQUEST = 'unified_inbox_request';
|
package/lib/index.d.ts
CHANGED
|
@@ -7,10 +7,13 @@ export * from './capability.js';
|
|
|
7
7
|
export * from './dispose.js';
|
|
8
8
|
export * from './identity.js';
|
|
9
9
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, type GenerationAdmissionBindable, type GenerationAdmissionSource, } from './admission.js';
|
|
10
|
-
export * from './generation-store.js';
|
|
11
10
|
export * from './handoff.js';
|
|
12
11
|
/** @public 用户侧创作面:`definePlugin`(`plugin.ts` 约定入口,承诺 semver)。 */
|
|
13
12
|
export * from './plugin.js';
|
|
13
|
+
/** @public Root configuration contract shared by Runtime and composition tooling. */
|
|
14
|
+
export * from './plugin-configuration.js';
|
|
15
|
+
export * from './root-config-file.js';
|
|
16
|
+
export * from './root-config-document.js';
|
|
14
17
|
/** @internal Runtime 内部:RootRuntime / RootController。 */
|
|
15
18
|
export * from './root-controller.js';
|
|
16
19
|
export * from './shared-lifetime.js';
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,13 @@ export * from './capability.js';
|
|
|
7
7
|
export * from './dispose.js';
|
|
8
8
|
export * from './identity.js';
|
|
9
9
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, } from './admission.js';
|
|
10
|
-
export * from './generation-store.js';
|
|
11
10
|
export * from './handoff.js';
|
|
12
11
|
/** @public 用户侧创作面:`definePlugin`(`plugin.ts` 约定入口,承诺 semver)。 */
|
|
13
12
|
export * from './plugin.js';
|
|
13
|
+
/** @public Root configuration contract shared by Runtime and composition tooling. */
|
|
14
|
+
export * from './plugin-configuration.js';
|
|
15
|
+
export * from './root-config-file.js';
|
|
16
|
+
export * from './root-config-document.js';
|
|
14
17
|
/** @internal Runtime 内部:RootRuntime / RootController。 */
|
|
15
18
|
export * from './root-controller.js';
|
|
16
19
|
export * from './shared-lifetime.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PluginId } from './identity.js';
|
|
2
|
+
/** Shared owner identity and physical-name invariant for process-wide resources. */
|
|
3
|
+
export declare abstract class OwnerScopedResourceHost {
|
|
4
|
+
readonly owner: PluginId;
|
|
5
|
+
constructor(owner: PluginId);
|
|
6
|
+
protected qualify(name: string): string;
|
|
7
|
+
protected unqualify(name: string): string | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function qualifyOwnedResourceName(owner: PluginId, name: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pluginOwnerResourceKey } from './identity.js';
|
|
2
|
+
const resourcePrefix = '__zhin_plugin__';
|
|
3
|
+
const resourceSeparator = '__';
|
|
4
|
+
/** Shared owner identity and physical-name invariant for process-wide resources. */
|
|
5
|
+
export class OwnerScopedResourceHost {
|
|
6
|
+
owner;
|
|
7
|
+
constructor(owner) {
|
|
8
|
+
this.owner = owner;
|
|
9
|
+
}
|
|
10
|
+
qualify(name) {
|
|
11
|
+
assertLogicalName(name);
|
|
12
|
+
return qualifyOwnedResourceName(this.owner, name);
|
|
13
|
+
}
|
|
14
|
+
unqualify(name) {
|
|
15
|
+
const prefix = `${resourcePrefix}${pluginOwnerResourceKey(this.owner)}${resourceSeparator}`;
|
|
16
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function qualifyOwnedResourceName(owner, name) {
|
|
20
|
+
assertLogicalName(name);
|
|
21
|
+
return `${resourcePrefix}${pluginOwnerResourceKey(owner)}${resourceSeparator}${name}`;
|
|
22
|
+
}
|
|
23
|
+
function assertLogicalName(name) {
|
|
24
|
+
if (!name || name.startsWith(resourcePrefix)) {
|
|
25
|
+
throw new TypeError(`Invalid plugin resource name: ${name}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Canonical `zhin.config.*#plugins` shape keyed by Plugin instanceKey. */
|
|
2
|
+
export type PluginConfigurationMap = Readonly<Record<string, unknown>>;
|
|
3
|
+
/** Raised when a Root config still uses a non-canonical Plugin configuration shape. */
|
|
4
|
+
export declare class PluginConfigurationShapeError extends TypeError {
|
|
5
|
+
readonly source: string;
|
|
6
|
+
constructor(source?: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reads the canonical Plugin instance configuration map.
|
|
10
|
+
*
|
|
11
|
+
* An omitted `plugins` key means that the Root has no configured child instances.
|
|
12
|
+
* Every present value must be an object map; legacy package-name arrays belong only
|
|
13
|
+
* to the explicit migration pipeline.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readPluginConfigurationMap(document: Readonly<Record<string, unknown>>, source?: string): PluginConfigurationMap;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const EMPTY_PLUGIN_CONFIGURATION = Object.freeze({});
|
|
2
|
+
/** Raised when a Root config still uses a non-canonical Plugin configuration shape. */
|
|
3
|
+
export class PluginConfigurationShapeError extends TypeError {
|
|
4
|
+
source;
|
|
5
|
+
constructor(source = 'Root config') {
|
|
6
|
+
super(`${source}.plugins must be an object keyed by Plugin instanceKey`);
|
|
7
|
+
this.source = source;
|
|
8
|
+
this.name = 'PluginConfigurationShapeError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads the canonical Plugin instance configuration map.
|
|
13
|
+
*
|
|
14
|
+
* An omitted `plugins` key means that the Root has no configured child instances.
|
|
15
|
+
* Every present value must be an object map; legacy package-name arrays belong only
|
|
16
|
+
* to the explicit migration pipeline.
|
|
17
|
+
*/
|
|
18
|
+
export function readPluginConfigurationMap(document, source = 'Root config') {
|
|
19
|
+
const value = document.plugins;
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return EMPTY_PLUGIN_CONFIGURATION;
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
23
|
+
throw new PluginConfigurationShapeError(source);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
package/lib/plugin.d.ts
CHANGED
|
@@ -56,10 +56,10 @@ export interface PluginDefinition<TConfig = unknown> {
|
|
|
56
56
|
* });
|
|
57
57
|
* ```
|
|
58
58
|
*
|
|
59
|
-
* Put capabilities in convention files such as `commands/hello.ts`, each with
|
|
59
|
+
* Put capabilities in convention files such as `commands/hello/index.ts`, each with
|
|
60
60
|
* one default export.
|
|
61
61
|
*/
|
|
62
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,
|
|
63
|
+
export { databaseHostToken, type DatabaseHostConsole, type DatabaseHostModel, type DatabaseHostSelection, type DatabaseHostSelectResult, type DatabaseHostTable, type DatabaseHostType, PluginDatabaseHost, } from './database-host.js';
|
|
64
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,
|
|
65
|
+
export { scheduleHostToken, PluginScheduleHost, type ScheduleJobRegistration, } from './schedule-host.js';
|
package/lib/plugin.js
CHANGED
|
@@ -14,7 +14,7 @@ const pluginNamePattern = /^[a-z][a-z0-9-]*$/;
|
|
|
14
14
|
* });
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
17
|
-
* Put capabilities in convention files such as `commands/hello.ts`, each with
|
|
17
|
+
* Put capabilities in convention files such as `commands/hello/index.ts`, each with
|
|
18
18
|
* one default export.
|
|
19
19
|
*/
|
|
20
20
|
export function definePlugin(definition) {
|
|
@@ -28,6 +28,6 @@ export function definePlugin(definition) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
// The application facade exposes these Host contracts beside definePlugin.
|
|
31
|
-
export { databaseHostToken, } from './database-host.js';
|
|
31
|
+
export { databaseHostToken, PluginDatabaseHost, } from './database-host.js';
|
|
32
32
|
export { outboundHostToken, } from './outbound-host.js';
|
|
33
|
-
export { scheduleHostToken, } from './schedule-host.js';
|
|
33
|
+
export { scheduleHostToken, PluginScheduleHost, } from './schedule-host.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type RuntimeConfigDocument = Readonly<Record<string, unknown>>;
|
|
2
|
+
export type ConfigPatch = {
|
|
3
|
+
readonly op: 'set';
|
|
4
|
+
readonly path: readonly string[];
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
readonly op: 'remove';
|
|
8
|
+
readonly path: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
export interface ConfigDocumentSnapshot {
|
|
11
|
+
readonly document: RuntimeConfigDocument;
|
|
12
|
+
readonly revision: string;
|
|
13
|
+
}
|
|
14
|
+
/** A prepared write is inert until the generation handoff activates it. */
|
|
15
|
+
export interface PreparedConfigDocument {
|
|
16
|
+
readonly document: RuntimeConfigDocument;
|
|
17
|
+
commit(): Promise<ConfigDocumentSnapshot>;
|
|
18
|
+
rollback(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface ConfigDocumentPort {
|
|
21
|
+
/** Files whose external edits invalidate this document snapshot. */
|
|
22
|
+
readonly sources?: readonly string[];
|
|
23
|
+
read(): Promise<ConfigDocumentSnapshot>;
|
|
24
|
+
prepare(current: ConfigDocumentSnapshot, patches: readonly ConfigPatch[]): Promise<PreparedConfigDocument>;
|
|
25
|
+
}
|
|
26
|
+
export declare class ConfigDocumentDivergenceError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
export declare class ConfigPatchPathError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/** Applies structural patches without mutating the source document. */
|
|
33
|
+
export declare function applyConfigPatches(document: RuntimeConfigDocument, patches: readonly ConfigPatch[]): RuntimeConfigDocument;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export class ConfigDocumentDivergenceError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super('ConfigDocument adapter candidate differs from the validated Runtime candidate');
|
|
4
|
+
this.name = 'ConfigDocumentDivergenceError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ConfigPatchPathError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ConfigPatchPathError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Applies structural patches without mutating the source document. */
|
|
14
|
+
export function applyConfigPatches(document, patches) {
|
|
15
|
+
let candidate = structuredClone(document);
|
|
16
|
+
for (const patch of patches)
|
|
17
|
+
candidate = applyConfigPatch(candidate, patch);
|
|
18
|
+
return Object.freeze(candidate);
|
|
19
|
+
}
|
|
20
|
+
function applyConfigPatch(document, patch) {
|
|
21
|
+
assertPath(patch.path);
|
|
22
|
+
if (patch.path.length === 0) {
|
|
23
|
+
if (patch.op === 'remove') {
|
|
24
|
+
throw new ConfigPatchPathError('The config document root cannot be removed');
|
|
25
|
+
}
|
|
26
|
+
return cloneDocument(patch.value);
|
|
27
|
+
}
|
|
28
|
+
if (patch.op === 'set')
|
|
29
|
+
setValue(document, patch.path, structuredClone(patch.value));
|
|
30
|
+
else
|
|
31
|
+
removeValue(document, patch.path);
|
|
32
|
+
return document;
|
|
33
|
+
}
|
|
34
|
+
function setValue(document, path, value) {
|
|
35
|
+
let target = document;
|
|
36
|
+
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
37
|
+
const existing = readChild(target, segment, path.slice(0, index + 1));
|
|
38
|
+
if (existing === undefined) {
|
|
39
|
+
const created = {};
|
|
40
|
+
target[segment] = created;
|
|
41
|
+
target = created;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
target = requireContainer(existing, path.slice(0, index + 1));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
writeChild(target, lastSegment(path), value, path);
|
|
48
|
+
}
|
|
49
|
+
function removeValue(document, path) {
|
|
50
|
+
let target = document;
|
|
51
|
+
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
52
|
+
const existing = readChild(target, segment, path.slice(0, index + 1));
|
|
53
|
+
if (existing === undefined)
|
|
54
|
+
return;
|
|
55
|
+
target = requireContainer(existing, path.slice(0, index + 1));
|
|
56
|
+
}
|
|
57
|
+
const segment = lastSegment(path);
|
|
58
|
+
if (Array.isArray(target)) {
|
|
59
|
+
target.splice(arrayIndex(segment, target, path), 1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
delete target[segment];
|
|
63
|
+
}
|
|
64
|
+
function cloneDocument(value) {
|
|
65
|
+
const document = structuredClone(value);
|
|
66
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
67
|
+
throw new ConfigPatchPathError('The config document root must be an object');
|
|
68
|
+
}
|
|
69
|
+
return document;
|
|
70
|
+
}
|
|
71
|
+
function readChild(container, segment, path) {
|
|
72
|
+
if (Array.isArray(container))
|
|
73
|
+
return container[arrayIndex(segment, container, path)];
|
|
74
|
+
return container[segment];
|
|
75
|
+
}
|
|
76
|
+
function writeChild(container, segment, value, path) {
|
|
77
|
+
if (Array.isArray(container)) {
|
|
78
|
+
container[arrayIndex(segment, container, path)] = value;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
container[segment] = value;
|
|
82
|
+
}
|
|
83
|
+
function arrayIndex(segment, container, path) {
|
|
84
|
+
if (!/^(0|[1-9]\d*)$/.test(segment)) {
|
|
85
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} requires an array index, got "${segment}"`);
|
|
86
|
+
}
|
|
87
|
+
const index = Number(segment);
|
|
88
|
+
if (index >= container.length) {
|
|
89
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} is out of bounds (array length ${container.length})`);
|
|
90
|
+
}
|
|
91
|
+
return index;
|
|
92
|
+
}
|
|
93
|
+
function requireContainer(value, path) {
|
|
94
|
+
if (!value || typeof value !== 'object') {
|
|
95
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} is not an object`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function assertPath(path) {
|
|
100
|
+
for (const segment of path) {
|
|
101
|
+
if (!segment || segment === '__proto__' || segment === 'prototype' || segment === 'constructor') {
|
|
102
|
+
throw new ConfigPatchPathError(`Unsafe config path segment: ${segment || '<empty>'}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function lastSegment(path) {
|
|
107
|
+
const segment = path[path.length - 1];
|
|
108
|
+
if (!segment)
|
|
109
|
+
throw new ConfigPatchPathError('Config patch path is empty');
|
|
110
|
+
return segment;
|
|
111
|
+
}
|
|
112
|
+
function pointer(path) {
|
|
113
|
+
if (path.length === 0)
|
|
114
|
+
return '/';
|
|
115
|
+
return `/${path.map((segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1')).join('/')}`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const ROOT_CONFIG_FILE_NAMES: readonly ["config.yml", "config.yaml", "config.json", "zhin.config.yml", "zhin.config.yaml", "zhin.config.json"];
|
|
2
|
+
export type RootConfigFileName = typeof ROOT_CONFIG_FILE_NAMES[number];
|
|
3
|
+
export type RootConfigFormat = 'yaml' | 'json';
|
|
4
|
+
/** File materialized when a project has no Root configuration yet. */
|
|
5
|
+
export declare const DEFAULT_ROOT_CONFIG_FILE_NAME: RootConfigFileName;
|
|
6
|
+
/** Returns the only configuration formats accepted by Root Runtime. */
|
|
7
|
+
export declare function rootConfigFormat(filePath: string): RootConfigFormat | undefined;
|
|
8
|
+
/** Enforces a single Root configuration authority for one project. */
|
|
9
|
+
export declare function selectRootConfigFile(existingFiles: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const ROOT_CONFIG_FILE_NAMES = Object.freeze([
|
|
2
|
+
'config.yml',
|
|
3
|
+
'config.yaml',
|
|
4
|
+
'config.json',
|
|
5
|
+
'zhin.config.yml',
|
|
6
|
+
'zhin.config.yaml',
|
|
7
|
+
'zhin.config.json',
|
|
8
|
+
]);
|
|
9
|
+
/** File materialized when a project has no Root configuration yet. */
|
|
10
|
+
export const DEFAULT_ROOT_CONFIG_FILE_NAME = 'zhin.config.yml';
|
|
11
|
+
/** Returns the only configuration formats accepted by Root Runtime. */
|
|
12
|
+
export function rootConfigFormat(filePath) {
|
|
13
|
+
const fileName = filePath.split(/[\\/]/u).pop();
|
|
14
|
+
if (!fileName || !ROOT_CONFIG_FILE_NAMES.includes(fileName))
|
|
15
|
+
return undefined;
|
|
16
|
+
const extension = /\.[^.]+$/u.exec(fileName)?.[0];
|
|
17
|
+
if (extension === '.yml' || extension === '.yaml')
|
|
18
|
+
return 'yaml';
|
|
19
|
+
if (extension === '.json')
|
|
20
|
+
return 'json';
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Enforces a single Root configuration authority for one project. */
|
|
24
|
+
export function selectRootConfigFile(existingFiles) {
|
|
25
|
+
if (existingFiles.length > 1) {
|
|
26
|
+
throw new Error(`Multiple Root config files found: ${existingFiles.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
return existingFiles[0];
|
|
29
|
+
}
|
package/lib/schedule-host.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PluginId } from './identity.js';
|
|
2
|
+
import { OwnerScopedResourceHost } from './owner-scoped-resource-host.js';
|
|
2
3
|
/**
|
|
3
4
|
* Thin Host Resource for Plugin Runtime cron jobs.
|
|
4
5
|
* Implementations typically wrap `@zhin.js/schedule` CalendarScheduler (solar cron).
|
|
@@ -23,8 +24,10 @@ export interface ScheduleHost {
|
|
|
23
24
|
}[];
|
|
24
25
|
}
|
|
25
26
|
/** Scoped scheduling surface exposed to a Plugin setup function. */
|
|
26
|
-
export
|
|
27
|
-
|
|
27
|
+
export declare function qualifyPluginScheduleId(owner: PluginId, id: string): string;
|
|
28
|
+
export declare class PluginScheduleHost extends OwnerScopedResourceHost {
|
|
29
|
+
private readonly host;
|
|
30
|
+
constructor(owner: PluginId, host: ScheduleHost);
|
|
28
31
|
register(job: ScheduleJobRegistration): () => void;
|
|
29
32
|
list(): readonly {
|
|
30
33
|
readonly id: string;
|
|
@@ -32,11 +35,6 @@ export interface PluginScheduleHost {
|
|
|
32
35
|
readonly description?: string;
|
|
33
36
|
}[];
|
|
34
37
|
}
|
|
35
|
-
export declare function qualifyPluginScheduleId(owner: PluginId, id: string): string;
|
|
36
|
-
export declare function unqualifyPluginScheduleId(owner: PluginId, id: string): string | undefined;
|
|
37
|
-
export declare function createPluginScheduleHost(owner: PluginId, host: ScheduleHost): PluginScheduleHost;
|
|
38
|
-
/** See unwrapPluginDatabaseHost: preserves legacy custom RootResource installers. */
|
|
39
|
-
export declare function unwrapPluginScheduleHost(host: ScheduleHost | PluginScheduleHost): ScheduleHost;
|
|
40
38
|
/** @public Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
|
41
39
|
export declare const scheduleHostToken: import("./token.js").Token<PluginScheduleHost>;
|
|
42
40
|
/** Root-only process scheduler for CLI composition and Console administration. */
|
package/lib/schedule-host.js
CHANGED
|
@@ -1,49 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OwnerScopedResourceHost, qualifyOwnedResourceName, } from './owner-scoped-resource-host.js';
|
|
2
2
|
import { createToken } from './token.js';
|
|
3
|
-
|
|
4
|
-
const jobSeparator = '__';
|
|
5
|
-
const roots = new WeakMap();
|
|
3
|
+
/** Scoped scheduling surface exposed to a Plugin setup function. */
|
|
6
4
|
export function qualifyPluginScheduleId(owner, id) {
|
|
7
|
-
|
|
8
|
-
if (owner === rootPluginId())
|
|
9
|
-
return id;
|
|
10
|
-
return `${jobPrefix}${pluginOwnerResourceKey(owner)}${jobSeparator}${id}`;
|
|
5
|
+
return qualifyOwnedResourceName(owner, id);
|
|
11
6
|
}
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...(job.description === undefined ? {} : { description: job.description }),
|
|
33
|
-
})];
|
|
34
|
-
}));
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
roots.set(facade, host);
|
|
38
|
-
return facade;
|
|
39
|
-
}
|
|
40
|
-
/** See unwrapPluginDatabaseHost: preserves legacy custom RootResource installers. */
|
|
41
|
-
export function unwrapPluginScheduleHost(host) {
|
|
42
|
-
return roots.get(host) ?? host;
|
|
43
|
-
}
|
|
44
|
-
function assertLogicalJobId(id) {
|
|
45
|
-
if (!id || id.startsWith(jobPrefix)) {
|
|
46
|
-
throw new TypeError(`Invalid plugin schedule id: ${id}`);
|
|
7
|
+
export class PluginScheduleHost extends OwnerScopedResourceHost {
|
|
8
|
+
host;
|
|
9
|
+
constructor(owner, host) {
|
|
10
|
+
super(owner);
|
|
11
|
+
this.host = host;
|
|
12
|
+
}
|
|
13
|
+
register(job) {
|
|
14
|
+
return this.host.register({ ...job, id: this.qualify(job.id), owner: this.owner });
|
|
15
|
+
}
|
|
16
|
+
list() {
|
|
17
|
+
return Object.freeze(this.host.list().flatMap((job) => {
|
|
18
|
+
const id = this.unqualify(job.id);
|
|
19
|
+
if (id === undefined)
|
|
20
|
+
return [];
|
|
21
|
+
return [Object.freeze({
|
|
22
|
+
id,
|
|
23
|
+
cron: job.cron,
|
|
24
|
+
...(job.description === undefined ? {} : { description: job.description }),
|
|
25
|
+
})];
|
|
26
|
+
}));
|
|
47
27
|
}
|
|
48
28
|
}
|
|
49
29
|
/** @public Plugin-facing token; Runtime injects a facade bound to the current owner. */
|
package/lib/system-log.d.ts
CHANGED
|
@@ -2,8 +2,7 @@ import type { DatabaseHost } from './database-host.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* 系统日志表(console logs 页数据源)定义与写入辅助。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 读取端为 packages/host/http console-rest-pages registerLogsRoutes)。
|
|
5
|
+
* 读取端为 packages/host/http console-rest-pages registerLogsRoutes。
|
|
7
6
|
*/
|
|
8
7
|
export declare const SYSTEM_LOG_TABLE = "SystemLog";
|
|
9
8
|
export declare const SYSTEM_LOG_DEFINITION: Record<string, unknown>;
|
package/lib/system-log.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 系统日志表(console logs 页数据源)定义与写入辅助。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 读取端为 packages/host/http console-rest-pages registerLogsRoutes)。
|
|
4
|
+
* 读取端为 packages/host/http console-rest-pages registerLogsRoutes。
|
|
6
5
|
*/
|
|
7
6
|
export const SYSTEM_LOG_TABLE = 'SystemLog';
|
|
8
7
|
export const SYSTEM_LOG_DEFINITION = {
|
package/package.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { DisposeStack, type Dispose } from './dispose.js';
|
|
2
|
-
/**
|
|
3
|
-
* Minimal structural view of a plugin setup context: anything owning a
|
|
4
|
-
* generation-scoped lifecycle (e.g. PluginSetupContext) can provide values.
|
|
5
|
-
*/
|
|
6
|
-
export interface GenerationStoreContext {
|
|
7
|
-
readonly lifecycle: DisposeStack;
|
|
8
|
-
}
|
|
9
|
-
export interface GenerationStore<T> {
|
|
10
|
-
/**
|
|
11
|
-
* Publish `value` as the current generation's value. The registration is
|
|
12
|
-
* removed automatically when `context.lifecycle` disposes (generation end);
|
|
13
|
-
* the returned Dispose unregisters manually and is idempotent.
|
|
14
|
-
*/
|
|
15
|
-
provide(context: GenerationStoreContext, value: T): Dispose;
|
|
16
|
-
/** Latest live value; throws a store-named error when none was provided. */
|
|
17
|
-
use(): T;
|
|
18
|
-
/** Latest live value, or `undefined` when none was provided. */
|
|
19
|
-
tryUse(): T | undefined;
|
|
20
|
-
/** Drop every registration across all generations. Mainly for tests. */
|
|
21
|
-
clear(): void;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Generation-scoped replacement for module-level `let _x` singletons.
|
|
25
|
-
*
|
|
26
|
-
* Provided values form a stack: the newest live registration wins, and when a
|
|
27
|
-
* generation ends its registration is removed via `context.lifecycle`,
|
|
28
|
-
* re-exposing the previous generation's value. This structurally prevents
|
|
29
|
-
* stale references to an already disposed generation (the repeater singleton
|
|
30
|
-
* / rss `_db` class of bugs).
|
|
31
|
-
*/
|
|
32
|
-
export declare function createGenerationStore<T>(name: string): GenerationStore<T>;
|
package/lib/generation-store.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generation-scoped replacement for module-level `let _x` singletons.
|
|
3
|
-
*
|
|
4
|
-
* Provided values form a stack: the newest live registration wins, and when a
|
|
5
|
-
* generation ends its registration is removed via `context.lifecycle`,
|
|
6
|
-
* re-exposing the previous generation's value. This structurally prevents
|
|
7
|
-
* stale references to an already disposed generation (the repeater singleton
|
|
8
|
-
* / rss `_db` class of bugs).
|
|
9
|
-
*/
|
|
10
|
-
export function createGenerationStore(name) {
|
|
11
|
-
const registrations = [];
|
|
12
|
-
const unregister = (registration) => {
|
|
13
|
-
const index = registrations.lastIndexOf(registration);
|
|
14
|
-
if (index >= 0)
|
|
15
|
-
registrations.splice(index, 1);
|
|
16
|
-
};
|
|
17
|
-
return {
|
|
18
|
-
provide(context, value) {
|
|
19
|
-
const registration = Object.freeze({ value });
|
|
20
|
-
registrations.push(registration);
|
|
21
|
-
context.lifecycle.add(() => unregister(registration));
|
|
22
|
-
return () => unregister(registration);
|
|
23
|
-
},
|
|
24
|
-
use() {
|
|
25
|
-
const latest = registrations[registrations.length - 1];
|
|
26
|
-
if (!latest) {
|
|
27
|
-
throw new Error(`Generation store "${name}" has no live value — call provide(context, value) during plugin setup`);
|
|
28
|
-
}
|
|
29
|
-
return latest.value;
|
|
30
|
-
},
|
|
31
|
-
tryUse() {
|
|
32
|
-
return registrations[registrations.length - 1]?.value;
|
|
33
|
-
},
|
|
34
|
-
clear() {
|
|
35
|
-
registrations.length = 0;
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|