@mearl/client 2.20.6 → 2.22.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 +153 -3
- package/dist/actionTimeouts.js +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/browserRegistry.d.ts +1 -1
- package/dist/check.d.ts +1 -1
- package/dist/cli.js +14 -8
- package/dist/commands.d.ts +23 -14
- package/dist/commands.js +24 -4
- package/dist/{generated-app-profile-definition.d.ts → generated/app-profile-definition.d.ts} +2 -2
- package/dist/{generated-app-profile-runtime-types.d.ts → generated/app-profile-runtime-types.d.ts} +2 -2
- package/dist/{generated-browser-action-protocol.d.ts → generated/browser-action-protocol.d.ts} +11 -1
- package/dist/{generated-browser-action-protocol.js → generated/browser-action-protocol.js} +3 -0
- package/dist/generated/plugin-diagnostics.d.ts +110 -0
- package/dist/generated/plugin-diagnostics.js +1 -0
- package/dist/generated/plugin-execution.d.ts +27 -0
- package/dist/generated/plugin-execution.js +1 -0
- package/dist/generated/plugin-models.d.ts +64 -0
- package/dist/generated/plugin-models.js +1 -0
- package/dist/generated/plugin-pages.d.ts +76 -0
- package/dist/generated/plugin-pages.js +59 -0
- package/dist/generated/plugin-recovery.d.ts +32 -0
- package/dist/generated/plugin-recovery.js +1 -0
- package/dist/generated/plugin-state.d.ts +50 -0
- package/dist/generated/plugin-state.js +1 -0
- package/dist/generated/plugin-tasks.d.ts +74 -0
- package/dist/generated/plugin-tasks.js +7 -0
- package/dist/generated/toolbox-icon.d.ts +10 -0
- package/dist/generated/toolbox-icon.js +37 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +7 -1
- package/dist/pageEvalArguments.d.ts +1 -1
- package/dist/pageEvalArguments.js +1 -1
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +1 -65
- package/dist/plugin.js +1 -391
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/plugins/clients/pluginHostConnection.d.ts +35 -0
- package/dist/plugins/clients/pluginHostConnection.js +256 -0
- package/dist/plugins/clients/pluginModels.d.ts +17 -0
- package/dist/plugins/clients/pluginModels.js +47 -0
- package/dist/plugins/clients/pluginPages.d.ts +21 -0
- package/dist/plugins/clients/pluginPages.js +46 -0
- package/dist/plugins/clients/pluginState.d.ts +26 -0
- package/dist/plugins/clients/pluginState.js +28 -0
- package/dist/plugins/clients/pluginTasks.d.ts +51 -0
- package/dist/plugins/clients/pluginTasks.js +181 -0
- package/dist/{officialPluginInstaller.js → plugins/officialPluginInstaller.js} +1 -3
- package/dist/{pluginPageBundle.js → plugins/pluginPageBundle.js} +74 -17
- package/dist/{pluginRegistry.d.ts → plugins/pluginRegistry.d.ts} +3 -4
- package/dist/{pluginRegistry.js → plugins/pluginRegistry.js} +16 -15
- package/dist/plugins/pluginTaskProtocol.d.ts +47 -0
- package/dist/plugins/pluginTaskProtocol.js +1 -0
- package/dist/plugins/pluginTaskWire.d.ts +4 -0
- package/dist/plugins/pluginTaskWire.js +42 -0
- package/dist/plugins/runtime/pluginCommandWorker.d.ts +2 -0
- package/dist/plugins/runtime/pluginCommandWorker.js +174 -0
- package/dist/plugins/runtime/pluginDiagnostics.d.ts +26 -0
- package/dist/plugins/runtime/pluginDiagnostics.js +309 -0
- package/dist/plugins/runtime/pluginExecution.d.ts +21 -0
- package/dist/plugins/runtime/pluginExecution.js +43 -0
- package/dist/plugins/runtime/pluginExecutor.d.ts +29 -0
- package/dist/plugins/runtime/pluginExecutor.js +223 -0
- package/dist/plugins/runtime/pluginExecutorProtocol.d.ts +55 -0
- package/dist/plugins/runtime/pluginExecutorProtocol.js +47 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/plugins/runtime/pluginExecutorSupervisor.js +63 -0
- package/dist/plugins/runtime/pluginHost.d.ts +20 -0
- package/dist/plugins/runtime/pluginHost.js +53 -0
- package/dist/plugins/runtime/pluginHostFiles.d.ts +1 -0
- package/dist/plugins/runtime/pluginHostFiles.js +21 -0
- package/dist/plugins/runtime/pluginHostPaths.d.ts +11 -0
- package/dist/plugins/runtime/pluginHostPaths.js +22 -0
- package/dist/plugins/runtime/pluginHostServer.d.ts +7 -0
- package/dist/plugins/runtime/pluginHostServer.js +252 -0
- package/dist/plugins/runtime/pluginModelCatalog.d.ts +14 -0
- package/dist/plugins/runtime/pluginModelCatalog.js +51 -0
- package/dist/plugins/runtime/pluginModelSessions.d.ts +16 -0
- package/dist/plugins/runtime/pluginModelSessions.js +234 -0
- package/dist/plugins/runtime/pluginPageManager.d.ts +57 -0
- package/dist/plugins/runtime/pluginPageManager.js +877 -0
- package/dist/plugins/runtime/pluginProcessTree.d.ts +2 -0
- package/dist/plugins/runtime/pluginProcessTree.js +31 -0
- package/dist/plugins/runtime/pluginRevision.d.ts +14 -0
- package/dist/plugins/runtime/pluginRevision.js +227 -0
- package/dist/plugins/runtime/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/plugins/runtime/pluginRuntimeCleanup.js +55 -0
- package/dist/plugins/runtime/pluginStateManager.d.ts +24 -0
- package/dist/plugins/runtime/pluginStateManager.js +132 -0
- package/dist/plugins/runtime/pluginStateStore.d.ts +57 -0
- package/dist/plugins/runtime/pluginStateStore.js +396 -0
- package/dist/plugins/runtime/pluginTaskManager.d.ts +92 -0
- package/dist/plugins/runtime/pluginTaskManager.js +1080 -0
- package/dist/plugins/runtime/pluginTaskStore.d.ts +53 -0
- package/dist/plugins/runtime/pluginTaskStore.js +319 -0
- package/dist/plugins/runtime/pythonPlugin.d.ts +31 -0
- package/dist/{pythonPlugin.js → plugins/runtime/pythonPlugin.js} +236 -31
- package/dist/plugins/sdk/plugin.d.ts +118 -0
- package/dist/plugins/sdk/plugin.js +672 -0
- package/dist/{pluginAppProfiles.d.ts → plugins/sdk/pluginAppProfiles.d.ts} +1 -1
- package/dist/{pluginAppProfiles.js → plugins/sdk/pluginAppProfiles.js} +2 -2
- package/dist/plugins/sdk/pluginModelDefinition.d.ts +6 -0
- package/dist/plugins/sdk/pluginModelDefinition.js +75 -0
- package/dist/plugins/sdk/pluginPageDefinition.d.ts +3 -0
- package/dist/plugins/sdk/pluginPageDefinition.js +62 -0
- package/dist/plugins/sdk/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/plugins/sdk/pluginRecoveryDefinition.js +56 -0
- package/dist/plugins/sdk/pluginStateDefinition.d.ts +4 -0
- package/dist/plugins/sdk/pluginStateDefinition.js +68 -0
- package/dist/runtimePaths.d.ts +4 -0
- package/dist/runtimePaths.js +8 -0
- package/dist/socket.d.ts +2 -2
- package/dist/socket.js +10 -6
- package/dist/unified.d.ts +3 -2
- package/dist/unified.js +83 -2
- package/package.json +7 -3
- package/dist/pythonPlugin.d.ts +0 -20
- /package/dist/{generated-app-profile-definition.js → generated/app-profile-definition.js} +0 -0
- /package/dist/{generated-app-profile-runtime-types.js → generated/app-profile-runtime-types.js} +0 -0
- /package/dist/{officialPluginCatalog.d.ts → plugins/officialPluginCatalog.d.ts} +0 -0
- /package/dist/{officialPluginCatalog.js → plugins/officialPluginCatalog.js} +0 -0
- /package/dist/{officialPluginInstaller.d.ts → plugins/officialPluginInstaller.d.ts} +0 -0
- /package/dist/{pluginPageBundle.d.ts → plugins/pluginPageBundle.d.ts} +0 -0
- /package/dist/{pluginSource.d.ts → plugins/pluginSource.d.ts} +0 -0
- /package/dist/{pluginSource.js → plugins/pluginSource.js} +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join, sep } from 'node:path';
|
|
4
|
+
import { pluginHostPaths } from './pluginHostPaths.js';
|
|
5
|
+
import { writePluginHostFile } from './pluginHostFiles.js';
|
|
6
|
+
import { compilePluginStateSchema } from '../sdk/pluginStateDefinition.js';
|
|
7
|
+
function fail(code, message) {
|
|
8
|
+
throw Object.assign(new Error(message), { code });
|
|
9
|
+
}
|
|
10
|
+
function schemaHash(definition) {
|
|
11
|
+
return createHash('sha256')
|
|
12
|
+
.update(JSON.stringify({ schema: definition.schema, storage: definition.storage }, (_key, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
14
|
+
: value))
|
|
15
|
+
.digest('hex');
|
|
16
|
+
}
|
|
17
|
+
function key(value) {
|
|
18
|
+
if (typeof value !== 'string' ||
|
|
19
|
+
!value.length ||
|
|
20
|
+
Buffer.byteLength(value) > 512 ||
|
|
21
|
+
['__proto__', 'constructor', 'prototype'].includes(value))
|
|
22
|
+
fail('PLUGIN_STATE_INVALID', 'Invalid state key');
|
|
23
|
+
}
|
|
24
|
+
/** A collection namespace is one atomic record, including CAS and commit receipts. */
|
|
25
|
+
export class PluginStateStore {
|
|
26
|
+
directory;
|
|
27
|
+
now;
|
|
28
|
+
memory = new Map();
|
|
29
|
+
memoryBytes = 0;
|
|
30
|
+
memoryRevisionFloor = 0;
|
|
31
|
+
clearMemory() {
|
|
32
|
+
this.memory.clear();
|
|
33
|
+
this.memoryBytes = 0;
|
|
34
|
+
this.memoryRevisionFloor = 0;
|
|
35
|
+
}
|
|
36
|
+
valueBytes = new WeakMap();
|
|
37
|
+
memorySize(document) {
|
|
38
|
+
let bytes = Buffer.byteLength(JSON.stringify({ ...document, entries: {} }));
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (const [name, entry] of Object.entries(document.entries)) {
|
|
41
|
+
let valueBytes = typeof entry.value === 'object' && entry.value !== null
|
|
42
|
+
? this.valueBytes.get(entry.value)
|
|
43
|
+
: undefined;
|
|
44
|
+
if (valueBytes === undefined) {
|
|
45
|
+
valueBytes = Buffer.byteLength(JSON.stringify(entry.value));
|
|
46
|
+
if (typeof entry.value === 'object' && entry.value !== null)
|
|
47
|
+
this.valueBytes.set(entry.value, valueBytes);
|
|
48
|
+
}
|
|
49
|
+
bytes +=
|
|
50
|
+
Buffer.byteLength(JSON.stringify(name)) +
|
|
51
|
+
1 +
|
|
52
|
+
Buffer.byteLength(JSON.stringify({ ...entry, value: null })) -
|
|
53
|
+
4 +
|
|
54
|
+
valueBytes +
|
|
55
|
+
(count++ ? 1 : 0);
|
|
56
|
+
}
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
saveMemory(path, document, bytes = this.memorySize(document)) {
|
|
60
|
+
const total = this.memoryBytes - (this.memory.get(path)?.bytes ?? 0) + bytes;
|
|
61
|
+
if (total > 128 * 1024 * 1024 || (!this.memory.has(path) && this.memory.size >= 512))
|
|
62
|
+
fail('PLUGIN_STATE_QUOTA', 'Memory state exceeds its host limits');
|
|
63
|
+
this.memory.set(path, { document, bytes });
|
|
64
|
+
this.memoryBytes = total;
|
|
65
|
+
}
|
|
66
|
+
constructor(directory = pluginHostPaths().state, now = Date.now) {
|
|
67
|
+
this.directory = directory;
|
|
68
|
+
this.now = now;
|
|
69
|
+
}
|
|
70
|
+
path(namespace) {
|
|
71
|
+
if (!/^[a-f0-9-]{36}$/i.test(namespace.installationId) ||
|
|
72
|
+
!/^[a-z][a-z0-9-]{0,63}$/.test(namespace.collection))
|
|
73
|
+
fail('PLUGIN_STATE_INVALID', 'Invalid state namespace');
|
|
74
|
+
const id = createHash('sha256').update(namespace.scope).digest('hex');
|
|
75
|
+
return join(this.directory, namespace.installationId, `${namespace.collection}-${id}.json`);
|
|
76
|
+
}
|
|
77
|
+
read(namespace, definition) {
|
|
78
|
+
try {
|
|
79
|
+
const file = this.path(namespace);
|
|
80
|
+
const saved = this.memory.get(file);
|
|
81
|
+
if ((definition.storage !== 'memory' && saved) ||
|
|
82
|
+
(definition.storage === 'memory' && !saved && existsSync(file)))
|
|
83
|
+
fail('PLUGIN_STATE_STORAGE_CHANGED', 'Switching between memory and disk state requires a new collection or namespace');
|
|
84
|
+
if (definition.storage === 'memory' && !saved) {
|
|
85
|
+
if (this.memory.size >= 512) {
|
|
86
|
+
// Retiring empty collections advances absence versions so stale CAS cannot be reused.
|
|
87
|
+
this.memoryRevisionFloor =
|
|
88
|
+
Math.max(this.memoryRevisionFloor, ...[...this.memory.values()].map(entry => entry.document.revision)) + 1;
|
|
89
|
+
for (const [path, entry] of this.memory)
|
|
90
|
+
if (!Object.keys(entry.document.entries).length) {
|
|
91
|
+
this.memory.delete(path);
|
|
92
|
+
this.memoryBytes -= entry.bytes;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
|
|
96
|
+
}
|
|
97
|
+
const document = saved
|
|
98
|
+
? { ...saved.document, entries: { ...saved.document.entries } }
|
|
99
|
+
: JSON.parse(readFileSync(file, 'utf8'));
|
|
100
|
+
if (!Number.isSafeInteger(document.revision) ||
|
|
101
|
+
!Number.isSafeInteger(document.schemaVersion) ||
|
|
102
|
+
!document.entries ||
|
|
103
|
+
!Array.isArray(document.commits))
|
|
104
|
+
throw new Error('Invalid state document');
|
|
105
|
+
if (definition.storage !== 'persistent') {
|
|
106
|
+
const entries = Object.fromEntries(Object.entries(document.entries).filter(([, entry]) => entry.expiresAt === undefined || entry.expiresAt > this.now()));
|
|
107
|
+
if (Object.keys(entries).length !== Object.keys(document.entries).length) {
|
|
108
|
+
document.entries = entries;
|
|
109
|
+
document.revision++;
|
|
110
|
+
if (definition.storage === 'memory')
|
|
111
|
+
this.saveMemory(file, document);
|
|
112
|
+
else
|
|
113
|
+
writePluginHostFile(file, JSON.stringify(document));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return document;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code !== 'ENOENT')
|
|
120
|
+
throw error;
|
|
121
|
+
return {
|
|
122
|
+
revision: definition.storage === 'memory' ? this.memoryRevisionFloor : 0,
|
|
123
|
+
schemaVersion: definition.schemaVersion,
|
|
124
|
+
schemaHash: schemaHash(definition),
|
|
125
|
+
storage: definition.storage,
|
|
126
|
+
scope: namespace.scope,
|
|
127
|
+
entries: {},
|
|
128
|
+
commits: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
entry(document, name, now) {
|
|
133
|
+
const entry = Object.hasOwn(document.entries, name) ? document.entries[name] : undefined;
|
|
134
|
+
return entry && (entry.expiresAt === undefined || entry.expiresAt > now)
|
|
135
|
+
? {
|
|
136
|
+
key: name,
|
|
137
|
+
exists: true,
|
|
138
|
+
version: `v:${entry.revision}`,
|
|
139
|
+
value: document.storage === 'memory' ? structuredClone(entry.value) : entry.value,
|
|
140
|
+
...(entry.expiresAt === undefined ? {} : { expiresAt: entry.expiresAt }),
|
|
141
|
+
}
|
|
142
|
+
: { key: name, exists: false, version: `a:${document.revision}` };
|
|
143
|
+
}
|
|
144
|
+
discardScope(installationId, scope) {
|
|
145
|
+
if (!/^[a-f0-9-]{36}$/i.test(installationId) || !scope.startsWith('instance:pi_'))
|
|
146
|
+
throw new Error('Only ended instance scopes can be discarded');
|
|
147
|
+
const directory = join(this.directory, installationId);
|
|
148
|
+
const memorySuffix = `-${createHash('sha256').update(scope).digest('hex')}.json`;
|
|
149
|
+
for (const [path, entry] of this.memory)
|
|
150
|
+
if (path.startsWith(directory + sep) && path.endsWith(memorySuffix)) {
|
|
151
|
+
this.memory.delete(path);
|
|
152
|
+
this.memoryBytes -= entry.bytes;
|
|
153
|
+
}
|
|
154
|
+
const suffix = `-${createHash('sha256').update(scope).digest('hex')}.json`;
|
|
155
|
+
let names;
|
|
156
|
+
try {
|
|
157
|
+
names = readdirSync(directory);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error.code === 'ENOENT')
|
|
161
|
+
return;
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
for (const name of names)
|
|
165
|
+
if (name.endsWith(suffix))
|
|
166
|
+
rmSync(join(directory, name), { force: true });
|
|
167
|
+
}
|
|
168
|
+
prune(skipFile) {
|
|
169
|
+
for (const [path, saved] of this.memory) {
|
|
170
|
+
const document = { ...saved.document, entries: { ...saved.document.entries } };
|
|
171
|
+
for (const [key, entry] of Object.entries(document.entries))
|
|
172
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= this.now())
|
|
173
|
+
delete document.entries[key];
|
|
174
|
+
if (Object.keys(document.entries).length === Object.keys(saved.document.entries).length)
|
|
175
|
+
continue;
|
|
176
|
+
document.revision++;
|
|
177
|
+
this.saveMemory(path, document);
|
|
178
|
+
}
|
|
179
|
+
let installations;
|
|
180
|
+
try {
|
|
181
|
+
installations = readdirSync(this.directory);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code === 'ENOENT')
|
|
185
|
+
return;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
for (const installation of installations) {
|
|
189
|
+
if (!/^[a-f0-9-]{36}$/i.test(installation))
|
|
190
|
+
continue;
|
|
191
|
+
for (const name of readdirSync(join(this.directory, installation))) {
|
|
192
|
+
if (!name.endsWith('.json'))
|
|
193
|
+
continue;
|
|
194
|
+
const file = join(this.directory, installation, name);
|
|
195
|
+
if (file === skipFile)
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
const document = JSON.parse(readFileSync(file, 'utf8'));
|
|
199
|
+
if (document.storage !== 'cache')
|
|
200
|
+
continue;
|
|
201
|
+
const entries = Object.fromEntries(Object.entries(document.entries).filter(([, value]) => value.expiresAt === undefined || value.expiresAt > this.now()));
|
|
202
|
+
if (Object.keys(entries).length === Object.keys(document.entries).length)
|
|
203
|
+
continue;
|
|
204
|
+
writePluginHostFile(file, JSON.stringify({ ...document, revision: document.revision + 1, entries }));
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error(`Cannot maintain plugin cache ${file}:`, error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
get(namespace, definition, name) {
|
|
213
|
+
key(name);
|
|
214
|
+
const document = this.read(namespace, definition);
|
|
215
|
+
return {
|
|
216
|
+
...this.entry(document, name, this.now()),
|
|
217
|
+
revision: document.revision,
|
|
218
|
+
schemaVersion: document.schemaVersion,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
getMany(namespace, definition, names) {
|
|
222
|
+
if (!Array.isArray(names) ||
|
|
223
|
+
names.length < 1 ||
|
|
224
|
+
names.length > 128 ||
|
|
225
|
+
new Set(names).size !== names.length)
|
|
226
|
+
fail('PLUGIN_STATE_INVALID', 'Expected 1 to 128 unique state keys');
|
|
227
|
+
names.forEach(name => key(name));
|
|
228
|
+
const document = this.read(namespace, definition), now = this.now();
|
|
229
|
+
const entries = names.map(name => this.entry(document, name, now));
|
|
230
|
+
if (Buffer.byteLength(JSON.stringify(entries)) > 1024 * 1024)
|
|
231
|
+
fail('PLUGIN_STATE_RESPONSE_TOO_LARGE', 'State batch exceeds 1 MiB; read fewer keys');
|
|
232
|
+
return { entries, revision: document.revision, schemaVersion: document.schemaVersion };
|
|
233
|
+
}
|
|
234
|
+
list(namespace, definition, input = {}) {
|
|
235
|
+
const document = this.read(namespace, definition);
|
|
236
|
+
const limit = input.limit ?? 100;
|
|
237
|
+
if (!Number.isSafeInteger(limit) ||
|
|
238
|
+
limit < 1 ||
|
|
239
|
+
limit > 200 ||
|
|
240
|
+
(input.cursor !== undefined && typeof input.cursor !== 'string'))
|
|
241
|
+
fail('PLUGIN_STATE_INVALID', 'Invalid state list cursor or limit');
|
|
242
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== document.revision)
|
|
243
|
+
fail('PLUGIN_STATE_CONFLICT', 'State list changed; restart pagination');
|
|
244
|
+
const now = this.now();
|
|
245
|
+
const candidates = Object.keys(document.entries)
|
|
246
|
+
.sort()
|
|
247
|
+
.filter(name => (input.cursor === undefined || name > input.cursor) &&
|
|
248
|
+
this.entry(document, name, now).exists);
|
|
249
|
+
const entries = [];
|
|
250
|
+
let bytes = 0;
|
|
251
|
+
for (const name of candidates) {
|
|
252
|
+
const entry = this.entry(document, name, now), size = Buffer.byteLength(JSON.stringify(entry));
|
|
253
|
+
if (entries.length && (entries.length >= limit || bytes + size > 256 * 1024))
|
|
254
|
+
break;
|
|
255
|
+
entries.push(entry);
|
|
256
|
+
bytes += size;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
entries,
|
|
260
|
+
revision: document.revision,
|
|
261
|
+
schemaVersion: document.schemaVersion,
|
|
262
|
+
...(entries.length < candidates.length ? { nextCursor: entries.at(-1).key } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
transact(namespace, definition, input) {
|
|
266
|
+
if (!input ||
|
|
267
|
+
!Array.isArray(input.operations) ||
|
|
268
|
+
input.operations.length > 128 ||
|
|
269
|
+
Buffer.byteLength(JSON.stringify(input)) > 512 * 1024)
|
|
270
|
+
fail('PLUGIN_STATE_INVALID', 'State transaction exceeds its limits');
|
|
271
|
+
const document = this.read(namespace, definition);
|
|
272
|
+
const hash = createHash('sha256')
|
|
273
|
+
.update(JSON.stringify({
|
|
274
|
+
input,
|
|
275
|
+
schemaVersion: definition.schemaVersion,
|
|
276
|
+
schema: definition.schema,
|
|
277
|
+
}))
|
|
278
|
+
.digest('hex');
|
|
279
|
+
if (input.idempotencyKey !== undefined) {
|
|
280
|
+
if (typeof input.idempotencyKey !== 'string' ||
|
|
281
|
+
!input.idempotencyKey.length ||
|
|
282
|
+
input.idempotencyKey.length > 128)
|
|
283
|
+
fail('PLUGIN_STATE_INVALID', 'Invalid transaction idempotency key');
|
|
284
|
+
const existing = document.commits.find(item => item.id === input.idempotencyKey);
|
|
285
|
+
if (existing) {
|
|
286
|
+
if (existing.hash !== hash)
|
|
287
|
+
fail('PLUGIN_STATE_IDEMPOTENCY_CONFLICT', 'Transaction key refers to different data or schema');
|
|
288
|
+
return { revision: existing.revision, schemaVersion: document.schemaVersion, reused: true };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== document.revision)
|
|
292
|
+
fail('PLUGIN_STATE_CONFLICT', 'Collection revision changed');
|
|
293
|
+
if (document.schemaVersion === definition.schemaVersion &&
|
|
294
|
+
document.schemaHash !== schemaHash(definition))
|
|
295
|
+
fail('PLUGIN_STATE_SCHEMA_VERSION_REQUIRED', 'Changing the state schema or storage policy requires a new schemaVersion');
|
|
296
|
+
if (document.schemaVersion > definition.schemaVersion)
|
|
297
|
+
fail('PLUGIN_STATE_SCHEMA_DOWNGRADE', 'An older plugin revision cannot write a newer state schema');
|
|
298
|
+
if (document.schemaVersion !== definition.schemaVersion &&
|
|
299
|
+
(input.migrateFromVersion !== document.schemaVersion || input.expectedRevision === undefined))
|
|
300
|
+
fail('PLUGIN_STATE_MIGRATION_REQUIRED', 'Schema migration requires the previous schema version and collection revision');
|
|
301
|
+
if (input.migrateFromVersion !== undefined &&
|
|
302
|
+
input.migrateFromVersion !== document.schemaVersion)
|
|
303
|
+
fail('PLUGIN_STATE_CONFLICT', 'State schema changed');
|
|
304
|
+
const now = this.now(), names = new Set();
|
|
305
|
+
const entries = Object.fromEntries(Object.entries(document.entries)
|
|
306
|
+
.filter(([, value]) => value.expiresAt === undefined || value.expiresAt > now)
|
|
307
|
+
.map(([name, value]) => [name, { ...value }]));
|
|
308
|
+
const revision = document.revision + 1;
|
|
309
|
+
if (!Number.isSafeInteger(revision))
|
|
310
|
+
fail('PLUGIN_STATE_QUOTA', 'State revision limit reached');
|
|
311
|
+
if (document.storage !== definition.storage)
|
|
312
|
+
for (const entry of Object.values(entries)) {
|
|
313
|
+
entry.revision = revision;
|
|
314
|
+
if (definition.storage !== 'persistent')
|
|
315
|
+
entry.expiresAt = now + (definition.ttlMs ?? 86400000);
|
|
316
|
+
else
|
|
317
|
+
delete entry.expiresAt;
|
|
318
|
+
}
|
|
319
|
+
const validate = compilePluginStateSchema(definition.schema);
|
|
320
|
+
for (const operation of input.operations) {
|
|
321
|
+
key(operation.key);
|
|
322
|
+
if (names.has(operation.key) ||
|
|
323
|
+
!['put', 'delete'].includes(operation.type) ||
|
|
324
|
+
operation.expectedVersion !== this.entry(document, operation.key, now).version)
|
|
325
|
+
fail('PLUGIN_STATE_CONFLICT', 'State entry version changed or key repeated');
|
|
326
|
+
names.add(operation.key);
|
|
327
|
+
if (operation.type === 'delete') {
|
|
328
|
+
delete entries[operation.key];
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!validate(operation.value) ||
|
|
332
|
+
Buffer.byteLength(JSON.stringify(operation.value)) > 256 * 1024)
|
|
333
|
+
fail('PLUGIN_STATE_SCHEMA_INVALID', 'State value does not match the declared schema or exceeds 256 KiB');
|
|
334
|
+
const ttl = operation.ttlMs ?? definition.ttlMs ?? 86400000;
|
|
335
|
+
if (definition.storage === 'persistent' && operation.ttlMs !== undefined)
|
|
336
|
+
fail('PLUGIN_STATE_INVALID', 'Persistent state does not accept TTL');
|
|
337
|
+
if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 7 * 86400000)
|
|
338
|
+
fail('PLUGIN_STATE_INVALID', 'Invalid cache TTL');
|
|
339
|
+
entries[operation.key] = {
|
|
340
|
+
revision,
|
|
341
|
+
value: definition.storage === 'memory' ? structuredClone(operation.value) : operation.value,
|
|
342
|
+
...(definition.storage !== 'persistent' ? { expiresAt: now + ttl } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
for (const entry of Object.values(entries))
|
|
346
|
+
if (!validate(entry.value))
|
|
347
|
+
fail('PLUGIN_STATE_SCHEMA_INVALID', 'Migration leaves entries incompatible with the target schema');
|
|
348
|
+
const commit = {
|
|
349
|
+
revision,
|
|
350
|
+
schemaVersion: definition.schemaVersion,
|
|
351
|
+
schemaHash: schemaHash(definition),
|
|
352
|
+
storage: definition.storage,
|
|
353
|
+
scope: namespace.scope,
|
|
354
|
+
entries,
|
|
355
|
+
commits: [
|
|
356
|
+
...document.commits,
|
|
357
|
+
...(input.idempotencyKey ? [{ id: input.idempotencyKey, hash, revision }] : []),
|
|
358
|
+
].slice(-64),
|
|
359
|
+
};
|
|
360
|
+
const content = definition.storage === 'memory' ? '' : JSON.stringify(commit);
|
|
361
|
+
const bytes = definition.storage === 'memory' ? this.memorySize(commit) : Buffer.byteLength(content);
|
|
362
|
+
if (Object.keys(entries).length > (definition.maxEntries ?? 1024) ||
|
|
363
|
+
bytes > (definition.maxBytes ?? 8 * 1024 * 1024))
|
|
364
|
+
fail('PLUGIN_STATE_QUOTA', 'State collection quota exceeded');
|
|
365
|
+
const file = this.path(namespace);
|
|
366
|
+
if (definition.storage === 'memory') {
|
|
367
|
+
this.saveMemory(file, commit, bytes);
|
|
368
|
+
return { revision, schemaVersion: definition.schemaVersion };
|
|
369
|
+
}
|
|
370
|
+
let total = Buffer.byteLength(content);
|
|
371
|
+
try {
|
|
372
|
+
for (const name of readdirSync(join(this.directory, namespace.installationId))) {
|
|
373
|
+
const path = join(this.directory, namespace.installationId, name);
|
|
374
|
+
if (path !== file && name.endsWith('.json'))
|
|
375
|
+
total += statSync(path).size;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (error.code !== 'ENOENT')
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
if (total > 64 * 1024 * 1024) {
|
|
383
|
+
this.prune(file);
|
|
384
|
+
total = Buffer.byteLength(content);
|
|
385
|
+
for (const name of readdirSync(join(this.directory, namespace.installationId))) {
|
|
386
|
+
const path = join(this.directory, namespace.installationId, name);
|
|
387
|
+
if (path !== file && name.endsWith('.json'))
|
|
388
|
+
total += statSync(path).size;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (total > 64 * 1024 * 1024)
|
|
392
|
+
fail('PLUGIN_STATE_QUOTA', 'Plugin state exceeds 64 MiB');
|
|
393
|
+
writePluginHostFile(file, content);
|
|
394
|
+
return { revision, schemaVersion: definition.schemaVersion };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PluginModelCatalog } from './pluginModelCatalog.js';
|
|
2
|
+
import type { PluginStateManager } from './pluginStateManager.js';
|
|
3
|
+
import type { PluginPageManager } from './pluginPageManager.js';
|
|
4
|
+
import type { PluginStateDeclaration } from '../../generated/plugin-state.js';
|
|
5
|
+
import type { PluginTaskCheckpoint, PluginTaskRecoveryContext, PluginTaskRecoveryDeclaration } from '../../generated/plugin-recovery.js';
|
|
6
|
+
import { executeRegisteredPlugin } from './pluginExecutor.js';
|
|
7
|
+
import { capturePluginRevision } from './pluginRevision.js';
|
|
8
|
+
import type { PluginRevision } from './pluginRevision.js';
|
|
9
|
+
import { PluginTaskStore } from './pluginTaskStore.js';
|
|
10
|
+
import type { PluginTask, PluginTaskStartResult, PluginTaskListResult } from '../../generated/plugin-tasks.js';
|
|
11
|
+
import type { PluginTaskScope, PluginTaskStartRequest, PluginTaskResumeRequest } from '../pluginTaskProtocol.js';
|
|
12
|
+
import type { PluginInvocationIdentity } from '../../generated/plugin-execution.js';
|
|
13
|
+
import type { PluginPageTarget } from '../../generated/plugin-pages.js';
|
|
14
|
+
import type { PluginOptions } from '../sdk/plugin.js';
|
|
15
|
+
import type { PluginPageBinding } from '../../generated/plugin-pages.js';
|
|
16
|
+
import type { PluginPageRuntime } from '../../generated/plugin-pages.js';
|
|
17
|
+
export interface PluginTaskBridge {
|
|
18
|
+
id: string;
|
|
19
|
+
scope: PluginTaskScope;
|
|
20
|
+
invoke(action: string, payload: Record<string, unknown>, options: PluginOptions): Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface PluginTaskManagerOptions {
|
|
23
|
+
store?: PluginTaskStore;
|
|
24
|
+
captureRevision?: typeof capturePluginRevision;
|
|
25
|
+
execute?: typeof executeRegisteredPlugin;
|
|
26
|
+
workerPath?: string;
|
|
27
|
+
concurrency?: number;
|
|
28
|
+
modelConcurrency?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface PluginTaskServices {
|
|
31
|
+
state: PluginStateManager['call'];
|
|
32
|
+
page: PluginPageManager['call'];
|
|
33
|
+
listModels: PluginModelCatalog['list'];
|
|
34
|
+
model?: (owner: string, ...args: Parameters<typeof executeRegisteredPlugin>) => ReturnType<typeof executeRegisteredPlugin>;
|
|
35
|
+
releaseModels?: (owner: string) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare class PluginTaskManager {
|
|
38
|
+
private readonly configuration;
|
|
39
|
+
private readonly services?;
|
|
40
|
+
readonly host: import("./pluginExecution.js").PluginExecutionHost;
|
|
41
|
+
readonly store: PluginTaskStore;
|
|
42
|
+
private readonly changes;
|
|
43
|
+
private readonly queue;
|
|
44
|
+
private readonly running;
|
|
45
|
+
private readonly resuming;
|
|
46
|
+
private readonly activeChildren;
|
|
47
|
+
private closed;
|
|
48
|
+
private readonly maintenance;
|
|
49
|
+
private descriptionsRunning;
|
|
50
|
+
private readonly descriptions;
|
|
51
|
+
private descriptionBytes;
|
|
52
|
+
private readonly revisionPins;
|
|
53
|
+
private readonly recoveries;
|
|
54
|
+
pinRevision(revision: PluginRevision): () => void;
|
|
55
|
+
describeRevision(revision: PluginRevision, signal: AbortSignal, options?: {
|
|
56
|
+
cache?: boolean;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
pageRuntime?: PluginPageRuntime;
|
|
59
|
+
stateStorage?: PluginStateDeclaration;
|
|
60
|
+
taskRecovery?: PluginTaskRecoveryDeclaration;
|
|
61
|
+
}>;
|
|
62
|
+
constructor(configuration?: PluginTaskManagerOptions, services?: PluginTaskServices | undefined);
|
|
63
|
+
private authorize;
|
|
64
|
+
get(taskId: string, scope: PluginTaskScope): PluginTask;
|
|
65
|
+
list(scope: PluginTaskScope, activeOnly?: boolean, cursor?: string, limit?: number): PluginTaskListResult;
|
|
66
|
+
start(request: PluginTaskStartRequest, bridge: PluginTaskBridge, binding?: {
|
|
67
|
+
revision: PluginRevision;
|
|
68
|
+
guard?: () => Promise<void>;
|
|
69
|
+
page?: PluginPageBinding;
|
|
70
|
+
parentTaskId?: string;
|
|
71
|
+
parentIdentity?: PluginInvocationIdentity;
|
|
72
|
+
documentTarget?: PluginPageTarget;
|
|
73
|
+
recovery?: PluginTaskRecoveryContext;
|
|
74
|
+
recoveryRequestHash?: string;
|
|
75
|
+
} | undefined): PluginTaskStartResult;
|
|
76
|
+
checkpoint(taskId: string, scope: PluginTaskScope): PluginTaskCheckpoint | null;
|
|
77
|
+
resume(request: PluginTaskResumeRequest, bridge: PluginTaskBridge): Promise<PluginTaskStartResult>;
|
|
78
|
+
private resumeAttempt;
|
|
79
|
+
private assertExecutionTreeStopped;
|
|
80
|
+
cancel(taskId: string, scope: PluginTaskScope): PluginTask;
|
|
81
|
+
events(taskId: string, scope: PluginTaskScope, cursor: number, waitMs?: number, signal?: AbortSignal): Promise<import("../../generated/plugin-tasks.js").PluginTaskEvents>;
|
|
82
|
+
resultChunk(taskId: string, scope: PluginTaskScope, index: number): import("../../generated/plugin-tasks.js").PluginTaskResultChunk;
|
|
83
|
+
private update;
|
|
84
|
+
private drain;
|
|
85
|
+
private execute;
|
|
86
|
+
private assertRouting;
|
|
87
|
+
assertChildBudget(parent: string): void;
|
|
88
|
+
awaitChild(parent: string, child: string, scope: PluginTaskScope, signal: AbortSignal, onProgress?: (progress: NonNullable<PluginTask['progress']>) => void): Promise<unknown>;
|
|
89
|
+
private stopChildren;
|
|
90
|
+
private waitResult;
|
|
91
|
+
close(): Promise<void>;
|
|
92
|
+
}
|