@agent-native/core 0.211.0 → 0.211.1-nightly-20261020213022
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/dist/cli/template-baseline.js +18 -5
- package/dist/cli/template-sync.js +100 -19
- package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
- package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/bounded-task-bundle.d.ts +17 -0
- package/dist/collab/bounded-task-bundle.js +39 -0
- package/dist/collab/bounded-task-source.d.ts +14 -0
- package/dist/collab/bounded-task-source.js +8 -0
- package/dist/collab/bounded-tasks.d.ts +67 -0
- package/dist/collab/bounded-tasks.js +439 -0
- package/dist/collab/index.d.ts +5 -2
- package/dist/collab/index.js +5 -2
- package/dist/collab/live-copy-tasks.d.ts +80 -0
- package/dist/collab/live-copy-tasks.js +330 -0
- package/dist/collab/live-copy.d.ts +188 -0
- package/dist/collab/live-copy.js +415 -0
- package/dist/collab/routes.d.ts +6 -1
- package/dist/collab/routes.js +24 -8
- package/dist/collab/storage.d.ts +53 -1
- package/dist/collab/storage.js +93 -5
- package/dist/collab/ydoc-manager.d.ts +92 -3
- package/dist/collab/ydoc-manager.js +447 -38
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.js +25 -5
- package/dist/deploy/build.js +2 -0
- package/dist/extensions/actions.js +1 -0
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/agent-connected.js +5 -1
- package/dist/mcp/build-server.js +39 -10
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/collab-plugin.d.ts +10 -1
- package/dist/server/collab-plugin.js +89 -20
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/optional-node-builtins.d.ts +1 -0
- package/dist/shared/optional-node-builtins.js +3 -0
- package/dist/tracking/providers.d.ts +1 -0
- package/dist/tracking/providers.js +2 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/bounded-task-plugin.d.ts +22 -0
- package/dist/vite/bounded-task-plugin.js +44 -0
- package/dist/vite/client.js +2 -0
- package/dist/vitest-config.js +3 -0
- package/package.json +4 -3
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { getWorkerThreads } from "../shared/optional-node-builtins.js";
|
|
2
|
+
import { BOUNDED_TASK_GLOBAL, } from "./bounded-task-source.js";
|
|
3
|
+
export { BOUNDED_TASK_GLOBAL };
|
|
4
|
+
export class BoundedTaskError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
task;
|
|
7
|
+
/** `name` of the error the task threw, for `task_threw`. */
|
|
8
|
+
taskErrorName;
|
|
9
|
+
constructor(code, task, message, options = {}) {
|
|
10
|
+
super(message, options.cause === undefined ? undefined : options);
|
|
11
|
+
this.name = "BoundedTaskError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.task = task;
|
|
14
|
+
if (options.taskErrorName)
|
|
15
|
+
this.taskErrorName = options.taskErrorName;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Size of a structured-clone payload in bytes, or -1 when it holds anything
|
|
20
|
+
* but plain data (or more than 200,000 values). Stops counting once
|
|
21
|
+
* `limit` is passed. The worker host evaluates this function's source, so it
|
|
22
|
+
* must not reference anything outside its own body.
|
|
23
|
+
*/
|
|
24
|
+
export function measureBoundedPayload(value, limit) {
|
|
25
|
+
const maxNodes = 200_000;
|
|
26
|
+
let total = 0;
|
|
27
|
+
let nodes = 0;
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const stack = [value];
|
|
30
|
+
while (stack.length > 0) {
|
|
31
|
+
const current = stack.pop();
|
|
32
|
+
nodes += 1;
|
|
33
|
+
if (nodes > maxNodes)
|
|
34
|
+
return -1;
|
|
35
|
+
if (current === null ||
|
|
36
|
+
current === undefined ||
|
|
37
|
+
typeof current === "boolean") {
|
|
38
|
+
total += 1;
|
|
39
|
+
}
|
|
40
|
+
else if (typeof current === "number") {
|
|
41
|
+
total += 8;
|
|
42
|
+
}
|
|
43
|
+
else if (typeof current === "string") {
|
|
44
|
+
total += current.length * 2;
|
|
45
|
+
}
|
|
46
|
+
else if (typeof current === "object") {
|
|
47
|
+
if (seen.has(current))
|
|
48
|
+
return -1;
|
|
49
|
+
seen.add(current);
|
|
50
|
+
if (ArrayBuffer.isView(current)) {
|
|
51
|
+
// Structured clone copies a view's whole buffer, and a shared buffer
|
|
52
|
+
// is not copied at all, so the task would see later writes.
|
|
53
|
+
if (typeof SharedArrayBuffer !== "undefined" &&
|
|
54
|
+
current.buffer instanceof SharedArrayBuffer) {
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
total += current.buffer.byteLength;
|
|
58
|
+
}
|
|
59
|
+
else if (Array.isArray(current)) {
|
|
60
|
+
total += 8;
|
|
61
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
62
|
+
stack.push(current[index]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const proto = Object.getPrototypeOf(current);
|
|
67
|
+
if (proto !== Object.prototype && proto !== null)
|
|
68
|
+
return -1;
|
|
69
|
+
for (const key of Object.keys(current)) {
|
|
70
|
+
total += key.length * 2;
|
|
71
|
+
stack.push(current[key]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
if (total > limit)
|
|
79
|
+
return total;
|
|
80
|
+
}
|
|
81
|
+
return total;
|
|
82
|
+
}
|
|
83
|
+
function hostScript(source) {
|
|
84
|
+
return `"use strict";
|
|
85
|
+
const { parentPort, workerData } = require("node:worker_threads");
|
|
86
|
+
const measureBoundedPayload = ${measureBoundedPayload.toString()};
|
|
87
|
+
${source.code}
|
|
88
|
+
;(() => {
|
|
89
|
+
const loaded = typeof ${BOUNDED_TASK_GLOBAL} === "undefined" ? undefined : ${BOUNDED_TASK_GLOBAL};
|
|
90
|
+
const tasks = loaded && typeof loaded === "object"
|
|
91
|
+
? (loaded.tasks || (loaded.default && loaded.default.tasks))
|
|
92
|
+
: undefined;
|
|
93
|
+
if (!tasks || typeof tasks !== "object") {
|
|
94
|
+
throw new Error("Bounded task module ${JSON.stringify(source.name).slice(1, -1)} does not export a tasks object");
|
|
95
|
+
}
|
|
96
|
+
const describe = (error) => ({
|
|
97
|
+
name: error && typeof error.name === "string" ? error.name : "Error",
|
|
98
|
+
message: String(error && error.message !== undefined ? error.message : error).slice(0, 2000),
|
|
99
|
+
});
|
|
100
|
+
parentPort.on("message", async (message) => {
|
|
101
|
+
const id = message.id;
|
|
102
|
+
const run = Object.prototype.hasOwnProperty.call(tasks, message.task) ? tasks[message.task] : undefined;
|
|
103
|
+
if (typeof run !== "function") {
|
|
104
|
+
parentPort.postMessage({ id, ok: false, code: "task_unknown", name: "Error", message: "Unknown task " + String(message.task) });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let output;
|
|
108
|
+
try {
|
|
109
|
+
output = await run(message.input);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
parentPort.postMessage({ id, ok: false, code: "task_threw", ...describe(error) });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const size = measureBoundedPayload(output, workerData.maxOutputBytes);
|
|
115
|
+
if (size < 0) {
|
|
116
|
+
parentPort.postMessage({ id, ok: false, code: "invalid_output", name: "Error", message: "Task output is not plain data" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (size > workerData.maxOutputBytes) {
|
|
120
|
+
parentPort.postMessage({ id, ok: false, code: "output_too_large", name: "Error", message: "Task output exceeds " + workerData.maxOutputBytes + " bytes" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
parentPort.postMessage({ id, ok: true, output });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
parentPort.postMessage({ id, ok: false, code: "invalid_output", ...describe(error) });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
parentPort.postMessage({ ready: true });
|
|
130
|
+
})();
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Runs functions from one bundled module in worker threads, each under a
|
|
135
|
+
* terminable deadline and a heap limit, with a fixed number of workers and a
|
|
136
|
+
* bounded wait queue. A task that overruns is stopped by terminating its
|
|
137
|
+
* worker, so synchronous work in the task never blocks the caller's event
|
|
138
|
+
* loop. Where worker threads do not exist (Cloudflare Workers), every run
|
|
139
|
+
* fails with `unavailable`; it never falls back to running inline.
|
|
140
|
+
*/
|
|
141
|
+
export function createBoundedTaskRunner(options) {
|
|
142
|
+
const { source } = options;
|
|
143
|
+
if (!source ||
|
|
144
|
+
source.kind !== "bounded-task-source" ||
|
|
145
|
+
typeof source.code !== "string" ||
|
|
146
|
+
typeof source.name !== "string") {
|
|
147
|
+
throw new TypeError("createBoundedTaskRunner needs a BoundedTaskSource from bundleBoundedTaskSource");
|
|
148
|
+
}
|
|
149
|
+
const deadlineMs = positive(options.deadlineMs, 2_000, "deadlineMs");
|
|
150
|
+
const startupTimeoutMs = positive(options.startupTimeoutMs, 10_000, "startupTimeoutMs");
|
|
151
|
+
const maxWorkers = count(options.maxWorkers, 1, 1, "maxWorkers");
|
|
152
|
+
const maxQueue = count(options.maxQueue, 8, 0, "maxQueue");
|
|
153
|
+
const queueTimeoutMs = positive(options.queueTimeoutMs, deadlineMs, "queueTimeoutMs");
|
|
154
|
+
const maxInputBytes = positive(options.maxInputBytes, 8 * 1024 * 1024, "maxInputBytes");
|
|
155
|
+
const maxOutputBytes = positive(options.maxOutputBytes, 16 * 1024 * 1024, "maxOutputBytes");
|
|
156
|
+
const memoryLimitMb = positive(options.memoryLimitMb, 128, "memoryLimitMb");
|
|
157
|
+
const stackSizeMb = positive(options.stackSizeMb, 4, "stackSizeMb");
|
|
158
|
+
const idleTimeoutMs = positive(options.idleTimeoutMs, 30_000, "idleTimeoutMs");
|
|
159
|
+
const script = hostScript(source);
|
|
160
|
+
const slots = [];
|
|
161
|
+
// A terminated worker can keep running for a while; it holds its place
|
|
162
|
+
// under maxWorkers until it has exited.
|
|
163
|
+
const terminating = new Set();
|
|
164
|
+
const queue = [];
|
|
165
|
+
let nextId = 1;
|
|
166
|
+
let closed = false;
|
|
167
|
+
const fail = (pending, code, task, message, extra = {}) => pending.reject(new BoundedTaskError(code, task, message, extra));
|
|
168
|
+
function removeSlot(slot) {
|
|
169
|
+
if (slot.dead)
|
|
170
|
+
return;
|
|
171
|
+
slot.dead = true;
|
|
172
|
+
if (slot.startupTimer)
|
|
173
|
+
clearTimeout(slot.startupTimer);
|
|
174
|
+
if (slot.idleTimer)
|
|
175
|
+
clearTimeout(slot.idleTimer);
|
|
176
|
+
const index = slots.indexOf(slot);
|
|
177
|
+
if (index >= 0)
|
|
178
|
+
slots.splice(index, 1);
|
|
179
|
+
const exited = slot.worker
|
|
180
|
+
.terminate()
|
|
181
|
+
.then(() => undefined, () => undefined)
|
|
182
|
+
.then(() => {
|
|
183
|
+
terminating.delete(exited);
|
|
184
|
+
if (!closed)
|
|
185
|
+
drain();
|
|
186
|
+
});
|
|
187
|
+
terminating.add(exited);
|
|
188
|
+
}
|
|
189
|
+
function killSlot(slot, code, message, cause) {
|
|
190
|
+
if (slot.dead)
|
|
191
|
+
return;
|
|
192
|
+
const current = slot.current;
|
|
193
|
+
slot.current = undefined;
|
|
194
|
+
if (current) {
|
|
195
|
+
clearTimeout(current.deadline);
|
|
196
|
+
fail(current, code, current.task, message, { cause });
|
|
197
|
+
}
|
|
198
|
+
removeSlot(slot);
|
|
199
|
+
if (code === "startup_failed") {
|
|
200
|
+
// Respawning would hit the same broken bundle; fail what is waiting.
|
|
201
|
+
for (const task of queue.splice(0, queue.length)) {
|
|
202
|
+
if (task.queueTimer)
|
|
203
|
+
clearTimeout(task.queueTimer);
|
|
204
|
+
fail(task, code, task.task, message, { cause });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
drain();
|
|
209
|
+
}
|
|
210
|
+
function spawn() {
|
|
211
|
+
const threads = getWorkerThreads();
|
|
212
|
+
let worker;
|
|
213
|
+
try {
|
|
214
|
+
if (!threads?.Worker)
|
|
215
|
+
throw new Error("node:worker_threads is missing");
|
|
216
|
+
worker = new threads.Worker(script, {
|
|
217
|
+
eval: true,
|
|
218
|
+
workerData: { maxOutputBytes },
|
|
219
|
+
resourceLimits: {
|
|
220
|
+
maxOldGenerationSizeMb: memoryLimitMb,
|
|
221
|
+
stackSizeMb,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
unavailable(error);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
worker.unref();
|
|
230
|
+
const slot = { worker, ready: false, dead: false };
|
|
231
|
+
slot.startupTimer = setTimeout(() => {
|
|
232
|
+
killSlot(slot, "startup_failed", `Bounded task module ${source.name} did not start within ${startupTimeoutMs} ms`);
|
|
233
|
+
}, startupTimeoutMs);
|
|
234
|
+
slot.startupTimer.unref?.();
|
|
235
|
+
worker.on("message", (message) => {
|
|
236
|
+
if (slot.dead)
|
|
237
|
+
return;
|
|
238
|
+
if (message?.ready === true) {
|
|
239
|
+
slot.ready = true;
|
|
240
|
+
if (slot.startupTimer)
|
|
241
|
+
clearTimeout(slot.startupTimer);
|
|
242
|
+
slot.startupTimer = undefined;
|
|
243
|
+
drain();
|
|
244
|
+
armQueueTimers();
|
|
245
|
+
if (!slot.current)
|
|
246
|
+
markIdle(slot);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const current = slot.current;
|
|
250
|
+
if (!current || message?.id !== current.id)
|
|
251
|
+
return;
|
|
252
|
+
clearTimeout(current.deadline);
|
|
253
|
+
slot.current = undefined;
|
|
254
|
+
if (message.ok === true) {
|
|
255
|
+
current.resolve(message.output);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const code = message.code === "task_unknown" ||
|
|
259
|
+
message.code === "invalid_output" ||
|
|
260
|
+
message.code === "output_too_large"
|
|
261
|
+
? message.code
|
|
262
|
+
: "task_threw";
|
|
263
|
+
fail(current, code, current.task, `Bounded task ${source.name}.${current.task} failed: ${String(message.message)}`, { taskErrorName: String(message.name ?? "Error") });
|
|
264
|
+
}
|
|
265
|
+
markIdle(slot);
|
|
266
|
+
drain();
|
|
267
|
+
});
|
|
268
|
+
worker.on("error", (error) => {
|
|
269
|
+
const outOfMemory = error?.code === "ERR_WORKER_OUT_OF_MEMORY";
|
|
270
|
+
// Before ready no task is running, so running out of heap means the
|
|
271
|
+
// module itself does; a respawn would too, forever.
|
|
272
|
+
if (!slot.ready) {
|
|
273
|
+
killSlot(slot, "startup_failed", outOfMemory
|
|
274
|
+
? `Bounded task module ${source.name} exceeded its ${memoryLimitMb} MiB heap while starting`
|
|
275
|
+
: `Bounded task module ${source.name} failed to start: ${error?.message}`, error);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
killSlot(slot, outOfMemory ? "memory_exceeded" : "worker_crashed", outOfMemory
|
|
279
|
+
? `Bounded task ${source.name} exceeded its ${memoryLimitMb} MiB heap`
|
|
280
|
+
: `Bounded task worker for ${source.name} crashed: ${error?.message}`, error);
|
|
281
|
+
});
|
|
282
|
+
worker.on("exit", (exitCode) => {
|
|
283
|
+
killSlot(slot, slot.ready ? "worker_crashed" : "startup_failed", `Bounded task worker for ${source.name} exited with code ${exitCode}`);
|
|
284
|
+
});
|
|
285
|
+
slots.push(slot);
|
|
286
|
+
return slot;
|
|
287
|
+
}
|
|
288
|
+
function unavailable(error) {
|
|
289
|
+
const pending = queue.splice(0, queue.length);
|
|
290
|
+
for (const task of pending) {
|
|
291
|
+
if (task.queueTimer)
|
|
292
|
+
clearTimeout(task.queueTimer);
|
|
293
|
+
fail(task, "unavailable", task.task, `Worker threads are unavailable here, so bounded task ${source.name}.${task.task} cannot run`, { cause: error });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function armQueueTimers() {
|
|
297
|
+
for (const pending of queue) {
|
|
298
|
+
if (pending.queueTimer)
|
|
299
|
+
continue;
|
|
300
|
+
pending.queueTimer = setTimeout(() => {
|
|
301
|
+
const index = queue.indexOf(pending);
|
|
302
|
+
if (index < 0)
|
|
303
|
+
return;
|
|
304
|
+
queue.splice(index, 1);
|
|
305
|
+
fail(pending, "queue_timeout", pending.task, `Bounded task ${source.name}.${pending.task} waited more than ${queueTimeoutMs} ms for a worker`);
|
|
306
|
+
}, queueTimeoutMs);
|
|
307
|
+
pending.queueTimer.unref?.();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function markIdle(slot) {
|
|
311
|
+
if (slot.idleTimer)
|
|
312
|
+
clearTimeout(slot.idleTimer);
|
|
313
|
+
slot.idleTimer = setTimeout(() => {
|
|
314
|
+
if (!slot.current)
|
|
315
|
+
removeSlot(slot);
|
|
316
|
+
}, idleTimeoutMs);
|
|
317
|
+
slot.idleTimer.unref?.();
|
|
318
|
+
}
|
|
319
|
+
function dispatch(slot, task) {
|
|
320
|
+
if (task.queueTimer)
|
|
321
|
+
clearTimeout(task.queueTimer);
|
|
322
|
+
if (slot.idleTimer)
|
|
323
|
+
clearTimeout(slot.idleTimer);
|
|
324
|
+
const id = nextId++;
|
|
325
|
+
const deadline = setTimeout(() => {
|
|
326
|
+
killSlot(slot, "deadline_exceeded", `Bounded task ${source.name}.${task.task} did not finish within ${deadlineMs} ms`);
|
|
327
|
+
}, deadlineMs);
|
|
328
|
+
deadline.unref?.();
|
|
329
|
+
slot.current = { ...task, id, deadline };
|
|
330
|
+
try {
|
|
331
|
+
slot.worker.postMessage({ id, task: task.task, input: task.input });
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
clearTimeout(deadline);
|
|
335
|
+
slot.current = undefined;
|
|
336
|
+
fail(task, "invalid_input", task.task, `Bounded task ${source.name}.${task.task} input cannot be sent to a worker`, { cause: error });
|
|
337
|
+
markIdle(slot);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function admit(pending) {
|
|
341
|
+
const size = measureBoundedPayload(pending.input, maxInputBytes);
|
|
342
|
+
if (size < 0) {
|
|
343
|
+
fail(pending, "invalid_input", pending.task, `Bounded task ${source.name}.${pending.task} input is not plain data`);
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (size > maxInputBytes) {
|
|
347
|
+
fail(pending, "input_too_large", pending.task, `Bounded task ${source.name}.${pending.task} input exceeds ${maxInputBytes} bytes`);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
function drain() {
|
|
353
|
+
while (queue.length > 0) {
|
|
354
|
+
const idle = slots.find((slot) => slot.ready && !slot.current);
|
|
355
|
+
if (!idle)
|
|
356
|
+
break;
|
|
357
|
+
const next = queue.shift();
|
|
358
|
+
if (next.queueTimer)
|
|
359
|
+
clearTimeout(next.queueTimer);
|
|
360
|
+
// The caller still holds the input and can grow it while it waits;
|
|
361
|
+
// postMessage copies it in this same turn, so this check holds.
|
|
362
|
+
if (!admit(next))
|
|
363
|
+
continue;
|
|
364
|
+
dispatch(idle, next);
|
|
365
|
+
}
|
|
366
|
+
let needed = queue.length - slots.filter((slot) => !slot.ready).length;
|
|
367
|
+
while (needed > 0 && slots.length + terminating.size < maxWorkers) {
|
|
368
|
+
if (!spawn())
|
|
369
|
+
return;
|
|
370
|
+
needed -= 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
name: source.name,
|
|
375
|
+
run(task, input) {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const pending = {
|
|
378
|
+
task,
|
|
379
|
+
input,
|
|
380
|
+
resolve: resolve,
|
|
381
|
+
reject,
|
|
382
|
+
};
|
|
383
|
+
if (closed) {
|
|
384
|
+
fail(pending, "closed", task, `Bounded task runner ${source.name} is closed`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!admit(pending))
|
|
388
|
+
return;
|
|
389
|
+
const running = slots.filter((slot) => slot.current).length;
|
|
390
|
+
if (running + queue.length >= maxWorkers + maxQueue) {
|
|
391
|
+
fail(pending, "queue_full", task, `Bounded task runner ${source.name} already has ${maxQueue} tasks waiting`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
queue.push(pending);
|
|
395
|
+
drain();
|
|
396
|
+
// Only a starting worker bounds the wait by itself. With no slot at
|
|
397
|
+
// all, the task waits on a worker still terminating, which has none.
|
|
398
|
+
if (slots.length === 0 || slots.some((slot) => slot.ready)) {
|
|
399
|
+
armQueueTimers();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
stats() {
|
|
404
|
+
return {
|
|
405
|
+
workers: slots.length + terminating.size,
|
|
406
|
+
running: slots.filter((slot) => slot.current).length,
|
|
407
|
+
queued: queue.length,
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
async close() {
|
|
411
|
+
closed = true;
|
|
412
|
+
for (const task of queue.splice(0, queue.length)) {
|
|
413
|
+
if (task.queueTimer)
|
|
414
|
+
clearTimeout(task.queueTimer);
|
|
415
|
+
fail(task, "closed", task.task, `Bounded task runner ${source.name} closed`);
|
|
416
|
+
}
|
|
417
|
+
for (const slot of slots.slice()) {
|
|
418
|
+
killSlot(slot, "closed", `Bounded task runner ${source.name} closed`);
|
|
419
|
+
}
|
|
420
|
+
await Promise.all(terminating);
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function positive(value, fallback, name) {
|
|
425
|
+
if (value === undefined)
|
|
426
|
+
return fallback;
|
|
427
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
428
|
+
throw new RangeError(`Bounded task option ${name} must be a positive number`);
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
function count(value, fallback, min, name) {
|
|
433
|
+
if (value === undefined)
|
|
434
|
+
return fallback;
|
|
435
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
436
|
+
throw new RangeError(`Bounded task option ${name} must be an integer of at least ${min}`);
|
|
437
|
+
}
|
|
438
|
+
return value;
|
|
439
|
+
}
|
package/dist/collab/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export { loadYDocState,
|
|
2
|
-
export {
|
|
1
|
+
export { loadYDocState, loadYDocVersion, hasCollabState, deleteCollabState, uint8ArrayToBase64, base64ToUint8Array, strictBase64ToUint8Array, CollabStateMalformedError, type CollabStateMalformedReason, } from "./storage.js";
|
|
2
|
+
export { createBoundedTaskRunner, measureBoundedPayload, BoundedTaskError, BOUNDED_TASK_GLOBAL, type BoundedTaskErrorCode, type BoundedTaskRunner, type BoundedTaskRunnerOptions, type BoundedTaskRunnerStats, type BoundedTaskSource, } from "./bounded-tasks.js";
|
|
3
|
+
export { bundleBoundedTaskSource, type BundleBoundedTaskSourceOptions, type BundledBoundedTaskSource, } from "./bounded-task-bundle.js";
|
|
4
|
+
export { defineLiveCopy, releaseLiveCopy, LiveCopyError, LIVE_COPY_DEFAULT_FIELD, type LiveCopyBinding, type LiveCopyCommit, type LiveCopyErrorCode, type LiveCopyOptions, type LiveCopyOutcome, type LiveCopyPersistOutcome, } from "./live-copy.js";
|
|
5
|
+
export { CollabBaseVersionConflictError, getDoc, saveYDocState, withPreparedYDocMutation, applyUpdate, seedXmlFragmentIfEmpty, applyText, getText, getState, getIncUpdate, seedFromText, releaseDoc, searchAndReplace, applyJson, applyPatchOps, getJson, seedFromJson, readLiveCopy, mutateLiveCopy, seedLiveCopy, type PreparedYDocMutationLease, type LiveCopyRead, type MutateLiveCopyOptions, type LiveCopyCallChange, type LiveCopyMutationResult, type SeedLiveCopyOptions, type LiveCopySeedResult, } from "./ydoc-manager.js";
|
|
3
6
|
export { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
|
|
4
7
|
export { applyTextToYDoc, initYDocWithText } from "./text-to-yjs.js";
|
|
5
8
|
export { getCollabEmitter, emitCollabUpdate, type CollabEvent, } from "./emitter.js";
|
package/dist/collab/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export { loadYDocState,
|
|
2
|
-
export {
|
|
1
|
+
export { loadYDocState, loadYDocVersion, hasCollabState, deleteCollabState, uint8ArrayToBase64, base64ToUint8Array, strictBase64ToUint8Array, CollabStateMalformedError, } from "./storage.js";
|
|
2
|
+
export { createBoundedTaskRunner, measureBoundedPayload, BoundedTaskError, BOUNDED_TASK_GLOBAL, } from "./bounded-tasks.js";
|
|
3
|
+
export { bundleBoundedTaskSource, } from "./bounded-task-bundle.js";
|
|
4
|
+
export { defineLiveCopy, releaseLiveCopy, LiveCopyError, LIVE_COPY_DEFAULT_FIELD, } from "./live-copy.js";
|
|
5
|
+
export { CollabBaseVersionConflictError, getDoc, saveYDocState, withPreparedYDocMutation, applyUpdate, seedXmlFragmentIfEmpty, applyText, getText, getState, getIncUpdate, seedFromText, releaseDoc, searchAndReplace, applyJson, applyPatchOps, getJson, seedFromJson, readLiveCopy, mutateLiveCopy, seedLiveCopy, } from "./ydoc-manager.js";
|
|
3
6
|
export { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
|
|
4
7
|
export { applyTextToYDoc, initYDocWithText } from "./text-to-yjs.js";
|
|
5
8
|
export { getCollabEmitter, emitCollabUpdate, } from "./emitter.js";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
export declare const LIVE_COPY_TASK: {
|
|
3
|
+
readonly read: "liveCopy.read";
|
|
4
|
+
readonly apply: "liveCopy.apply";
|
|
5
|
+
readonly seed: "liveCopy.seed";
|
|
6
|
+
readonly edit: "liveCopy.edit";
|
|
7
|
+
};
|
|
8
|
+
export interface LiveCopyTaskRefusal {
|
|
9
|
+
kind: "refused";
|
|
10
|
+
reason: string;
|
|
11
|
+
detail?: string;
|
|
12
|
+
}
|
|
13
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
14
|
+
export interface LiveCopyTaskHandlers {
|
|
15
|
+
/**
|
|
16
|
+
* The body's text. Return a refusal for content the text cannot carry;
|
|
17
|
+
* never drop it. Must not edit `doc`.
|
|
18
|
+
*/
|
|
19
|
+
derive(doc: Y.Doc, field: string): MaybePromise<string | LiveCopyTaskRefusal>;
|
|
20
|
+
/** Writes the first content for `source` into `field` of an empty doc. */
|
|
21
|
+
seed?(doc: Y.Doc, field: string, source: string): MaybePromise<void | LiveCopyTaskRefusal>;
|
|
22
|
+
/** Server edits by name, run by `mutateLiveCopy`. */
|
|
23
|
+
edits?: Record<string, (doc: Y.Doc, field: string, input: unknown) => MaybePromise<void | LiveCopyTaskRefusal>>;
|
|
24
|
+
}
|
|
25
|
+
export type LiveCopyTaskFailureCode = "malformed_state" | "unsupported_content" | "state_too_large" | "not_seeded" | "derive_refused" | "derive_failed" | "build_refused" | "build_failed";
|
|
26
|
+
/**
|
|
27
|
+
* What brings a client up to date without decoding on the request thread:
|
|
28
|
+
* the state's vector, compared by bytes, and the deletions a client at that
|
|
29
|
+
* vector may still lack, since deleting never moves a state vector.
|
|
30
|
+
*/
|
|
31
|
+
export interface LiveCopyCatchUp {
|
|
32
|
+
stateVector: Uint8Array;
|
|
33
|
+
deletions: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export type LiveCopyTaskResult = {
|
|
36
|
+
kind: "failed";
|
|
37
|
+
code: LiveCopyTaskFailureCode;
|
|
38
|
+
reason: string;
|
|
39
|
+
message: string;
|
|
40
|
+
} | ({
|
|
41
|
+
kind: "unseeded";
|
|
42
|
+
} & LiveCopyCatchUp) | ({
|
|
43
|
+
kind: "live";
|
|
44
|
+
text?: string;
|
|
45
|
+
} & LiveCopyCatchUp) | {
|
|
46
|
+
kind: "unchanged";
|
|
47
|
+
text?: string;
|
|
48
|
+
} | ({
|
|
49
|
+
kind: "changed";
|
|
50
|
+
state: Uint8Array;
|
|
51
|
+
text: string;
|
|
52
|
+
update?: Uint8Array;
|
|
53
|
+
} & LiveCopyCatchUp);
|
|
54
|
+
interface TaskInput {
|
|
55
|
+
field: string;
|
|
56
|
+
maxStateBytes: number;
|
|
57
|
+
}
|
|
58
|
+
export interface LiveCopyReadInput extends TaskInput {
|
|
59
|
+
state: Uint8Array;
|
|
60
|
+
derive: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface LiveCopyApplyInput extends TaskInput {
|
|
63
|
+
state: Uint8Array | null;
|
|
64
|
+
update: Uint8Array;
|
|
65
|
+
}
|
|
66
|
+
export interface LiveCopySeedInput extends TaskInput {
|
|
67
|
+
state: Uint8Array | null;
|
|
68
|
+
source: string;
|
|
69
|
+
}
|
|
70
|
+
export interface LiveCopyEditInput extends TaskInput {
|
|
71
|
+
state: Uint8Array;
|
|
72
|
+
edit: string;
|
|
73
|
+
input: unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The tasks a live copy's runner must host. Export the result as `tasks`
|
|
77
|
+
* from the module the runner bundles, alongside any tasks of your own.
|
|
78
|
+
*/
|
|
79
|
+
export declare function defineLiveCopyTasks(handlers: LiveCopyTaskHandlers): Record<string, (input: never) => Promise<LiveCopyTaskResult>>;
|
|
80
|
+
export {};
|