webloom-framework 0.3.0 → 0.4.0
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/README.md +70 -42
- package/dist/advanced.d.ts +168 -0
- package/dist/advanced.js +441 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-4DSINPZD.js +158 -0
- package/dist/chunk-4DSINPZD.js.map +1 -0
- package/dist/chunk-JHJZIO2H.js +4018 -0
- package/dist/chunk-JHJZIO2H.js.map +1 -0
- package/dist/chunk-RAQOFUZY.js +1737 -0
- package/dist/chunk-RAQOFUZY.js.map +1 -0
- package/dist/chunk-YOIH6H36.js +433 -0
- package/dist/chunk-YOIH6H36.js.map +1 -0
- package/dist/index.d.ts +39 -259
- package/dist/index.js +38 -1359
- package/dist/index.js.map +1 -1
- package/dist/messageBus-CtrwkjrO.d.ts +5 -0
- package/dist/messagePortServiceTransport-B0FyJr43.d.ts +264 -0
- package/dist/react.d.ts +41 -28
- package/dist/react.js +79 -88
- package/dist/react.js.map +1 -1
- package/dist/runtimeTypes-DquUCHz-.d.ts +1640 -0
- package/dist/sharedWorkerHost-Cb2a1_KD.d.ts +190 -0
- package/dist/testing.d.ts +18 -17
- package/dist/testing.js +23 -15
- package/dist/testing.js.map +1 -1
- package/dist/windowRuntime-B6Ue8jso.d.ts +23 -0
- package/docs/api.md +148 -111
- package/docs/proposals/webloom-v4/implementation-plan.md +443 -0
- package/docs/proposals/webloom-v4/requirements.md +555 -0
- package/docs/proposals/webloom-v4/verification.md +84 -0
- package/package.json +9 -2
- package/dist/chunk-76BGPI6M.js +0 -4261
- package/dist/chunk-76BGPI6M.js.map +0 -1
- package/dist/createPluginHost-BVsUCDKN.d.ts +0 -1318
- package/dist/resourceRegistry-BFnjmeGE.d.ts +0 -267
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/lifecycle/pluginIntentController.ts
|
|
2
|
+
function makeAuthorityInstanceId() {
|
|
3
|
+
try {
|
|
4
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
5
|
+
return `authority:${crypto.randomUUID()}`;
|
|
6
|
+
}
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
return `authority:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
}
|
|
11
|
+
function cloneSnapshot(snapshot) {
|
|
12
|
+
return {
|
|
13
|
+
revision: snapshot.revision,
|
|
14
|
+
desiredEnabled: { ...snapshot.desiredEnabled },
|
|
15
|
+
desiredRevision: { ...snapshot.desiredRevision }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function normalizeSnapshot(initial) {
|
|
19
|
+
const revision = Number.isSafeInteger(initial?.revision) && (initial?.revision ?? 0) >= 0 ? initial.revision : 0;
|
|
20
|
+
const desiredEnabled = {};
|
|
21
|
+
for (const [pluginId, value] of Object.entries(initial?.desiredEnabled ?? {})) {
|
|
22
|
+
if (typeof value === "boolean") desiredEnabled[pluginId] = value;
|
|
23
|
+
}
|
|
24
|
+
const desiredRevision = {};
|
|
25
|
+
for (const [pluginId, value] of Object.entries(initial?.desiredRevision ?? {})) {
|
|
26
|
+
if (Number.isSafeInteger(value) && value >= 0) desiredRevision[pluginId] = value;
|
|
27
|
+
}
|
|
28
|
+
return { revision, desiredEnabled, desiredRevision };
|
|
29
|
+
}
|
|
30
|
+
function commandFingerprint(command) {
|
|
31
|
+
return [
|
|
32
|
+
command.authorityInstanceId,
|
|
33
|
+
command.expectedRevision,
|
|
34
|
+
command.pluginId,
|
|
35
|
+
command.desiredEnabled ? "true" : "false"
|
|
36
|
+
].join("\0");
|
|
37
|
+
}
|
|
38
|
+
function commandError(message) {
|
|
39
|
+
return { status: "command-conflict", commandId: "", message };
|
|
40
|
+
}
|
|
41
|
+
function createPluginIntentController(options = {}) {
|
|
42
|
+
const authorityInstanceId = options.authorityInstanceId ?? makeAuthorityInstanceId();
|
|
43
|
+
const maxCommandRecords = Math.max(1, Math.floor(options.maxCommandRecords ?? 256));
|
|
44
|
+
let current = normalizeSnapshot(options.initial);
|
|
45
|
+
const commandRecords = /* @__PURE__ */ new Map();
|
|
46
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
47
|
+
let queue = Promise.resolve();
|
|
48
|
+
const notify = () => {
|
|
49
|
+
const snapshot = cloneSnapshot(current);
|
|
50
|
+
for (const listener of [...listeners]) {
|
|
51
|
+
try {
|
|
52
|
+
listener(snapshot);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const process = async (command) => {
|
|
58
|
+
const candidate = command;
|
|
59
|
+
const commandId = typeof candidate?.commandId === "string" ? candidate.commandId : "";
|
|
60
|
+
if (!candidate || typeof candidate.authorityInstanceId !== "string" || candidate.authorityInstanceId.length === 0 || typeof candidate.pluginId !== "string" || candidate.pluginId.length === 0 || typeof candidate.desiredEnabled !== "boolean" || !Number.isSafeInteger(candidate.expectedRevision) || (candidate.expectedRevision ?? -1) < 0 || commandId.length === 0) {
|
|
61
|
+
const result = commandError("commandId\u3001pluginId \u548C expectedRevision \u5FC5\u987B\u662F\u6709\u6548\u503C");
|
|
62
|
+
return { ...result, commandId };
|
|
63
|
+
}
|
|
64
|
+
if (command.authorityInstanceId !== authorityInstanceId) {
|
|
65
|
+
return {
|
|
66
|
+
status: "stale-authority",
|
|
67
|
+
commandId: command.commandId,
|
|
68
|
+
expectedAuthorityInstanceId: authorityInstanceId
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const fingerprint = commandFingerprint(command);
|
|
72
|
+
const existing = commandRecords.get(command.commandId);
|
|
73
|
+
if (existing) {
|
|
74
|
+
if (existing.fingerprint !== fingerprint) {
|
|
75
|
+
return {
|
|
76
|
+
status: "command-conflict",
|
|
77
|
+
commandId: command.commandId,
|
|
78
|
+
message: "\u76F8\u540C commandId \u7684\u547D\u4EE4\u5185\u5BB9\u4E0D\u540C\uFF0C\u62D2\u7EDD\u8986\u76D6\u539F\u547D\u4EE4"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
status: "duplicate",
|
|
83
|
+
commandId: command.commandId,
|
|
84
|
+
snapshot: cloneSnapshot(existing.result.snapshot),
|
|
85
|
+
persisted: true
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (command.expectedRevision !== current.revision) {
|
|
89
|
+
return {
|
|
90
|
+
status: "revision-conflict",
|
|
91
|
+
commandId: command.commandId,
|
|
92
|
+
snapshot: cloneSnapshot(current)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const next = {
|
|
96
|
+
revision: current.revision + 1,
|
|
97
|
+
desiredEnabled: {
|
|
98
|
+
...current.desiredEnabled,
|
|
99
|
+
[command.pluginId]: command.desiredEnabled
|
|
100
|
+
},
|
|
101
|
+
desiredRevision: {
|
|
102
|
+
...current.desiredRevision,
|
|
103
|
+
[command.pluginId]: (current.desiredRevision[command.pluginId] ?? 0) + 1
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
await options.persist?.(cloneSnapshot(next));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
status: "persistence-failed",
|
|
111
|
+
commandId: command.commandId,
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
snapshot: cloneSnapshot(current)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
current = next;
|
|
117
|
+
const accepted = {
|
|
118
|
+
status: "accepted",
|
|
119
|
+
commandId: command.commandId,
|
|
120
|
+
snapshot: cloneSnapshot(current),
|
|
121
|
+
persisted: true
|
|
122
|
+
};
|
|
123
|
+
commandRecords.set(command.commandId, {
|
|
124
|
+
fingerprint,
|
|
125
|
+
result: accepted
|
|
126
|
+
});
|
|
127
|
+
while (commandRecords.size > maxCommandRecords) {
|
|
128
|
+
const oldest = commandRecords.keys().next().value;
|
|
129
|
+
if (oldest === void 0) break;
|
|
130
|
+
commandRecords.delete(oldest);
|
|
131
|
+
}
|
|
132
|
+
notify();
|
|
133
|
+
return {
|
|
134
|
+
status: "accepted",
|
|
135
|
+
commandId: accepted.commandId,
|
|
136
|
+
snapshot: cloneSnapshot(accepted.snapshot),
|
|
137
|
+
persisted: true
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const controller = {
|
|
141
|
+
authorityInstanceId,
|
|
142
|
+
snapshot: () => cloneSnapshot(current),
|
|
143
|
+
submit(command) {
|
|
144
|
+
const result = queue.then(() => process(command));
|
|
145
|
+
queue = result.then(() => void 0, () => void 0);
|
|
146
|
+
return result;
|
|
147
|
+
},
|
|
148
|
+
subscribe(listener) {
|
|
149
|
+
listeners.add(listener);
|
|
150
|
+
return () => listeners.delete(listener);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
return controller;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { createPluginIntentController };
|
|
157
|
+
//# sourceMappingURL=chunk-4DSINPZD.js.map
|
|
158
|
+
//# sourceMappingURL=chunk-4DSINPZD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lifecycle/pluginIntentController.ts"],"names":[],"mappings":"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","file":"chunk-4DSINPZD.js","sourcesContent":["// 插件产品启停控制面。\n//\n// 这里处理“用户意图已持久化”和“运行实例已经启动”之间的边界:\n// - 命令是绝对 desiredEnabled,不使用 toggle;\n// - 同一个 authority 内按 commandId 去重;\n// - 先查去重,再查 expectedRevision;\n// - 只有持久化成功后才发布 accepted。\n//\n// 它不直接启停 PluginHost。Host / Coordinator 订阅成功后的意图快照,再按\n// 依赖图装配运行实例;因此启动失败不会把用户意图偷偷改回 false。\n\nimport type {\n PluginIntentCommand,\n PluginIntentCommandResult,\n PluginIntentController,\n PluginIntentSnapshot,\n} from \"../contracts/lifecycle.js\";\n\nexport interface CreatePluginIntentControllerOptions {\n /** 当前控制面启动身份;Worker 重启时应传入新值。 */\n authorityInstanceId?: string;\n /** Worker 恢复后读取的平台意图。 */\n initial?: Partial<PluginIntentSnapshot>;\n /** 将候选快照原子写入平台存储;缺省表示内存控制面(测试用)。 */\n persist?: (snapshot: PluginIntentSnapshot) => Promise<void>;\n /** 有界去重记录数量,避免 commandId 永久增长。 */\n maxCommandRecords?: number;\n}\n\ninterface CommandRecord {\n fingerprint: string;\n result: Extract<PluginIntentCommandResult, { status: \"accepted\" | \"duplicate\" }>;\n}\n\nfunction makeAuthorityInstanceId(): string {\n try {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return `authority:${crypto.randomUUID()}`;\n }\n } catch {\n // 非安全唯一值只作本次 Worker 运行身份,不能作为权限凭据。\n }\n return `authority:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;\n}\n\nfunction cloneSnapshot(snapshot: PluginIntentSnapshot): PluginIntentSnapshot {\n return {\n revision: snapshot.revision,\n desiredEnabled: { ...snapshot.desiredEnabled },\n desiredRevision: { ...snapshot.desiredRevision },\n };\n}\n\nfunction normalizeSnapshot(initial: Partial<PluginIntentSnapshot> | undefined): PluginIntentSnapshot {\n const revision = Number.isSafeInteger(initial?.revision) && (initial?.revision ?? 0) >= 0\n ? initial!.revision!\n : 0;\n const desiredEnabled: Record<string, boolean> = {};\n for (const [pluginId, value] of Object.entries(initial?.desiredEnabled ?? {})) {\n if (typeof value === \"boolean\") desiredEnabled[pluginId] = value;\n }\n const desiredRevision: Record<string, number> = {};\n for (const [pluginId, value] of Object.entries(initial?.desiredRevision ?? {})) {\n if (Number.isSafeInteger(value) && value >= 0) desiredRevision[pluginId] = value;\n }\n return { revision, desiredEnabled, desiredRevision };\n}\n\nfunction commandFingerprint(command: PluginIntentCommand): string {\n return [\n command.authorityInstanceId,\n command.expectedRevision,\n command.pluginId,\n command.desiredEnabled ? \"true\" : \"false\",\n ].join(\"\\u0000\");\n}\n\nfunction commandError(message: string): Extract<PluginIntentCommandResult, { status: \"command-conflict\" }> {\n return { status: \"command-conflict\", commandId: \"\", message };\n}\n\n/** 创建单一控制面上的插件意图控制器。 */\nexport function createPluginIntentController(\n options: CreatePluginIntentControllerOptions = {}\n): PluginIntentController {\n const authorityInstanceId = options.authorityInstanceId ?? makeAuthorityInstanceId();\n const maxCommandRecords = Math.max(1, Math.floor(options.maxCommandRecords ?? 256));\n let current = normalizeSnapshot(options.initial);\n const commandRecords = new Map<string, CommandRecord>();\n const listeners = new Set<(snapshot: PluginIntentSnapshot) => void>();\n let queue = Promise.resolve();\n\n const notify = () => {\n const snapshot = cloneSnapshot(current);\n for (const listener of [...listeners]) {\n try {\n listener(snapshot);\n } catch {\n // UI 观察者错误不能回滚已经成功持久化的意图。\n }\n }\n };\n\n const process = async (command: PluginIntentCommand): Promise<PluginIntentCommandResult> => {\n const candidate = command as Partial<PluginIntentCommand> | null | undefined;\n const commandId = typeof candidate?.commandId === \"string\" ? candidate.commandId : \"\";\n if (\n !candidate\n || typeof candidate.authorityInstanceId !== \"string\"\n || candidate.authorityInstanceId.length === 0\n || typeof candidate.pluginId !== \"string\"\n || candidate.pluginId.length === 0\n || typeof candidate.desiredEnabled !== \"boolean\"\n || !Number.isSafeInteger(candidate.expectedRevision)\n || (candidate.expectedRevision ?? -1) < 0\n || commandId.length === 0\n ) {\n const result = commandError(\"commandId、pluginId 和 expectedRevision 必须是有效值\");\n return { ...result, commandId };\n }\n if (command.authorityInstanceId !== authorityInstanceId) {\n return {\n status: \"stale-authority\",\n commandId: command.commandId,\n expectedAuthorityInstanceId: authorityInstanceId,\n };\n }\n\n const fingerprint = commandFingerprint(command);\n const existing = commandRecords.get(command.commandId);\n if (existing) {\n if (existing.fingerprint !== fingerprint) {\n return {\n status: \"command-conflict\",\n commandId: command.commandId,\n message: \"相同 commandId 的命令内容不同,拒绝覆盖原命令\",\n };\n }\n return {\n status: \"duplicate\",\n commandId: command.commandId,\n snapshot: cloneSnapshot(existing.result.snapshot),\n persisted: true,\n };\n }\n\n if (command.expectedRevision !== current.revision) {\n return {\n status: \"revision-conflict\",\n commandId: command.commandId,\n snapshot: cloneSnapshot(current),\n };\n }\n\n const next: PluginIntentSnapshot = {\n revision: current.revision + 1,\n desiredEnabled: {\n ...current.desiredEnabled,\n [command.pluginId]: command.desiredEnabled,\n },\n desiredRevision: {\n ...current.desiredRevision,\n [command.pluginId]: (current.desiredRevision[command.pluginId] ?? 0) + 1,\n },\n };\n try {\n await options.persist?.(cloneSnapshot(next));\n } catch (error) {\n return {\n status: \"persistence-failed\",\n commandId: command.commandId,\n message: error instanceof Error ? error.message : String(error),\n snapshot: cloneSnapshot(current),\n };\n }\n\n current = next;\n const accepted: Extract<PluginIntentCommandResult, { status: \"accepted\" | \"duplicate\" }> = {\n status: \"accepted\",\n commandId: command.commandId,\n snapshot: cloneSnapshot(current),\n persisted: true,\n };\n commandRecords.set(command.commandId, {\n fingerprint,\n result: accepted,\n });\n while (commandRecords.size > maxCommandRecords) {\n const oldest = commandRecords.keys().next().value as string | undefined;\n if (oldest === undefined) break;\n commandRecords.delete(oldest);\n }\n notify();\n return {\n status: \"accepted\",\n commandId: accepted.commandId,\n snapshot: cloneSnapshot(accepted.snapshot),\n persisted: true,\n };\n };\n\n const controller: PluginIntentController = {\n authorityInstanceId,\n snapshot: () => cloneSnapshot(current),\n submit(command) {\n const result = queue.then(() => process(command));\n // 无论上一条命令结果如何,后续命令都必须继续进入同一串行队列。\n queue = result.then(() => undefined, () => undefined);\n return result;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return controller;\n}\n"]}
|