@workser/runtime 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +71 -2
- package/dist/cjs/client/follow.js +103 -0
- package/dist/cjs/client/index.js +6 -0
- package/dist/cjs/contract/actions.js +223 -0
- package/dist/cjs/contract/catalog.js +335 -0
- package/dist/cjs/contract/index.js +20 -0
- package/dist/cjs/contract/ordered-feed.js +94 -0
- package/dist/cjs/contract/types.js +9 -0
- package/dist/cjs/engine/engine.js +2 -0
- package/dist/cjs/index.js +27 -0
- package/dist/cjs/internal/async-queue.js +56 -0
- package/dist/cjs/internal/sse.js +170 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/place/place.js +2 -0
- package/dist/cjs/runner/index.js +6 -0
- package/dist/cjs/runner/journal.js +82 -0
- package/dist/cjs/runner/runner-client.js +477 -0
- package/dist/client/follow.d.ts +45 -0
- package/dist/client/follow.js +100 -0
- package/dist/client/follow.js.map +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -0
- package/dist/contract/actions.d.ts +73 -0
- package/dist/contract/actions.js +217 -0
- package/dist/contract/actions.js.map +1 -0
- package/dist/contract/catalog.d.ts +97 -0
- package/dist/contract/catalog.js +328 -0
- package/dist/contract/catalog.js.map +1 -0
- package/dist/contract/index.d.ts +4 -0
- package/dist/contract/index.js +5 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/ordered-feed.d.ts +56 -0
- package/dist/contract/ordered-feed.js +91 -0
- package/dist/contract/ordered-feed.js.map +1 -0
- package/dist/contract/types.d.ts +311 -0
- package/dist/contract/types.js +9 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/engine/engine.d.ts +39 -0
- package/dist/engine/engine.js +2 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/async-queue.d.ts +14 -0
- package/dist/internal/async-queue.js +53 -0
- package/dist/internal/async-queue.js.map +1 -0
- package/dist/internal/sse.d.ts +57 -0
- package/dist/internal/sse.js +164 -0
- package/dist/internal/sse.js.map +1 -0
- package/dist/place/place.d.ts +81 -0
- package/dist/place/place.js +2 -0
- package/dist/place/place.js.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.js +2 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/journal.d.ts +32 -0
- package/dist/runner/journal.js +79 -0
- package/dist/runner/journal.js.map +1 -0
- package/dist/runner/runner-client.d.ts +185 -0
- package/dist/runner/runner-client.js +473 -0
- package/dist/runner/runner-client.js.map +1 -0
- package/package.json +30 -4
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The run event catalog: every event a runner may send, and what it must
|
|
4
|
+
* carry. Pure and dependency-free: this file is the source of truth in
|
|
5
|
+
* `@workser/runtime`, and core-api keeps a generated copy of it (plan Phase 0).
|
|
6
|
+
*
|
|
7
|
+
* TWO GATES, ON PURPOSE
|
|
8
|
+
* ---------------------
|
|
9
|
+
* - A runner (`v1/runner/runs/:id/events`) is strict: an unknown type or a
|
|
10
|
+
* malformed payload is refused, so a screen never has to guess what an event
|
|
11
|
+
* means.
|
|
12
|
+
* - The service engine (`internal/runs/:id/events`) is lenient: it already
|
|
13
|
+
* sends ~30 types, some built at runtime, and refusing one would lose a live
|
|
14
|
+
* run's history. Unknown types are accepted and counted (`unknownLegacyType`)
|
|
15
|
+
* so the catalog can grow to cover them before that gate tightens.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.LEGACY_EVENT_TYPES = exports.TERMINAL_EVENT_TYPES = exports.RUN_EVENT_CATALOG = exports.MAX_EVENT_BATCH = exports.MAX_EVENT_DATA_BYTES = exports.RUNNER_PROTOCOL_VERSION = void 0;
|
|
19
|
+
exports.checkRunnerEvent = checkRunnerEvent;
|
|
20
|
+
exports.checkLegacyEvent = checkLegacyEvent;
|
|
21
|
+
exports.unknownLegacyType = unknownLegacyType;
|
|
22
|
+
exports.statusForRunnerEvent = statusForRunnerEvent;
|
|
23
|
+
exports.checkBatchSeq = checkBatchSeq;
|
|
24
|
+
exports.RUNNER_PROTOCOL_VERSION = 1;
|
|
25
|
+
/** Largest event payload we keep, serialised. Bigger outputs go to files. */
|
|
26
|
+
exports.MAX_EVENT_DATA_BYTES = 64 * 1024;
|
|
27
|
+
/** Largest batch a runner may send in one call. */
|
|
28
|
+
exports.MAX_EVENT_BATCH = 100;
|
|
29
|
+
const text = (maxLength = 20_000, required = true) => ({
|
|
30
|
+
type: 'string',
|
|
31
|
+
required,
|
|
32
|
+
maxLength,
|
|
33
|
+
});
|
|
34
|
+
const id = (required = true) => text(120, required);
|
|
35
|
+
/**
|
|
36
|
+
* What each event means, for the people reading it on a screen:
|
|
37
|
+
*
|
|
38
|
+
* - run.* the run's life. Terminal ones end the stream.
|
|
39
|
+
* - step.* one thing the agent did (a command, an edit, a tool call).
|
|
40
|
+
* - message* what the agent said; `.delta` streams it as it is written.
|
|
41
|
+
* - file/diff what changed on disk.
|
|
42
|
+
* - check.result a done-check (tests, typecheck, deck validates, numbers match).
|
|
43
|
+
* - preview.ready a URL the owner can open.
|
|
44
|
+
* - handoff.* a sub-job this run asked for started or ended (core-api
|
|
45
|
+
* writes these on the parent; `run.completed.result` is the
|
|
46
|
+
* child's typed answer).
|
|
47
|
+
* - usage tokens and cost as the runner saw them; billing uses the
|
|
48
|
+
* gateway's own figure, never this one.
|
|
49
|
+
* - log plain lines for the details view.
|
|
50
|
+
*/
|
|
51
|
+
exports.RUN_EVENT_CATALOG = {
|
|
52
|
+
'run.started': { engine: text(80, false), place: text(40, false) },
|
|
53
|
+
'run.paused': { reason: text(500, false) },
|
|
54
|
+
'run.resumed': {},
|
|
55
|
+
// `result`: the typed answer a handoff's parent asked for (`Handoff.expected`).
|
|
56
|
+
'run.completed': { summary: text(20_000, false), result: { type: 'object' } },
|
|
57
|
+
'run.failed': { error_kind: text(80, false), message: text(4_000, false) },
|
|
58
|
+
'run.cancelled': { reason: text(500, false) },
|
|
59
|
+
// A person stopped the job before it read some messages sent into it (its
|
|
60
|
+
// unacked `message` commands). Core writes it on Stop; nothing new starts.
|
|
61
|
+
// `messages`: [{ pos, text (≤500 chars), author (name or null) }], oldest first.
|
|
62
|
+
'run.messages_unseen': {
|
|
63
|
+
count: { type: 'number', required: true },
|
|
64
|
+
messages: { type: 'array', required: true },
|
|
65
|
+
},
|
|
66
|
+
// A session-mode job finished a turn and is waiting for the next message.
|
|
67
|
+
'run.idle': { summary: text(20_000, false), turn: { type: 'number' } },
|
|
68
|
+
// A reply turned into a job (reply-or-job contract §2c): the AI agent called
|
|
69
|
+
// `begin_job`, or the safety net saw an action, a computer, a fan-out, a
|
|
70
|
+
// helper or a long run. Core makes it a job once (migration 300).
|
|
71
|
+
'run.became_job': {
|
|
72
|
+
reason: { type: 'string', required: true, oneOf: ['begin_job', 'action', 'computer', 'fan_out', 'long', 'helper', 'owner'] },
|
|
73
|
+
title: text(500, false),
|
|
74
|
+
eta: text(200, false),
|
|
75
|
+
},
|
|
76
|
+
// The engine's own session, so a later runner can reattach to it
|
|
77
|
+
// (core-api puts the latest into the brief as `run.resume`).
|
|
78
|
+
'engine.session': { engine: text(80), session_id: text(200), workdir: text(1_000) },
|
|
79
|
+
// The agent is waiting on a person (`owner`) or on an approval it raised.
|
|
80
|
+
// `run.resumed` says it carried on.
|
|
81
|
+
'run.waiting': {
|
|
82
|
+
on: { type: 'string', required: true, oneOf: ['owner', 'approval'] },
|
|
83
|
+
ask_id: id(false),
|
|
84
|
+
},
|
|
85
|
+
'step.started': {
|
|
86
|
+
step_id: id(),
|
|
87
|
+
title: text(500),
|
|
88
|
+
tool: text(120, false),
|
|
89
|
+
input: { type: 'object' },
|
|
90
|
+
// The sub-job this step handed work to, and the step it is nested under.
|
|
91
|
+
child_run_id: id(false),
|
|
92
|
+
parent_step_id: id(false),
|
|
93
|
+
},
|
|
94
|
+
// Live output of a long step. Throttle to about one a second per step.
|
|
95
|
+
'step.progress': {
|
|
96
|
+
step_id: id(),
|
|
97
|
+
text: text(2_000, false),
|
|
98
|
+
pct: { type: 'number' },
|
|
99
|
+
stdout_tail: text(2_000, false),
|
|
100
|
+
},
|
|
101
|
+
// The agent's to-do list changed (`write_todos`): the whole list, every time.
|
|
102
|
+
'plan.updated': {
|
|
103
|
+
todos: { type: 'array', required: true },
|
|
104
|
+
},
|
|
105
|
+
'step.completed': {
|
|
106
|
+
step_id: id(),
|
|
107
|
+
status: { type: 'string', required: true, oneOf: ['ok', 'error', 'skipped'] },
|
|
108
|
+
output: { type: 'object' },
|
|
109
|
+
error: text(4_000, false),
|
|
110
|
+
duration_ms: { type: 'number' },
|
|
111
|
+
},
|
|
112
|
+
message: {
|
|
113
|
+
message_id: id(false),
|
|
114
|
+
role: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
required: true,
|
|
117
|
+
oneOf: ['assistant', 'user', 'system'],
|
|
118
|
+
},
|
|
119
|
+
text: text(),
|
|
120
|
+
},
|
|
121
|
+
'message.delta': { message_id: id(), text: text() },
|
|
122
|
+
'file.changed': {
|
|
123
|
+
path: text(1_000),
|
|
124
|
+
change: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
required: true,
|
|
127
|
+
oneOf: ['added', 'modified', 'deleted', 'renamed'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
'diff.ready': {
|
|
131
|
+
files: { type: 'number', required: true },
|
|
132
|
+
additions: { type: 'number' },
|
|
133
|
+
deletions: { type: 'number' },
|
|
134
|
+
},
|
|
135
|
+
'check.result': {
|
|
136
|
+
name: text(200),
|
|
137
|
+
status: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
required: true,
|
|
140
|
+
oneOf: ['pass', 'fail', 'skip'],
|
|
141
|
+
},
|
|
142
|
+
detail: text(8_000, false),
|
|
143
|
+
},
|
|
144
|
+
'preview.ready': { url: text(2_000) },
|
|
145
|
+
// Written by core-api on the PARENT when a handoff starts and ends.
|
|
146
|
+
'handoff.created': { child_run_id: id(), goal: text(2_000, false) },
|
|
147
|
+
'handoff.finished': {
|
|
148
|
+
child_run_id: id(),
|
|
149
|
+
status: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
required: true,
|
|
152
|
+
oneOf: ['completed', 'failed', 'cancelled'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
usage: {
|
|
156
|
+
model: text(200, false),
|
|
157
|
+
tokens_in: { type: 'number' },
|
|
158
|
+
tokens_out: { type: 'number' },
|
|
159
|
+
cost_usd: { type: 'number' },
|
|
160
|
+
},
|
|
161
|
+
log: {
|
|
162
|
+
level: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
required: true,
|
|
165
|
+
oneOf: ['debug', 'info', 'warn', 'error'],
|
|
166
|
+
},
|
|
167
|
+
text: text(),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
/** Events that end a run. The stream closes after it delivers one. */
|
|
171
|
+
exports.TERMINAL_EVENT_TYPES = new Set([
|
|
172
|
+
'run.completed',
|
|
173
|
+
'run.failed',
|
|
174
|
+
'run.cancelled',
|
|
175
|
+
]);
|
|
176
|
+
/**
|
|
177
|
+
* Types the service engine sends today that aren't runner events. Accepted on
|
|
178
|
+
* the lenient gate without being counted as unknown.
|
|
179
|
+
*/
|
|
180
|
+
exports.LEGACY_EVENT_TYPES = new Set([
|
|
181
|
+
'browser.live',
|
|
182
|
+
'composio.authorization',
|
|
183
|
+
'computer.busy',
|
|
184
|
+
'computer.ready',
|
|
185
|
+
'computer.requested',
|
|
186
|
+
'computer.viewer.available',
|
|
187
|
+
'computer.viewer.unavailable',
|
|
188
|
+
'connector.execution.receipt',
|
|
189
|
+
'run.capabilities_attached',
|
|
190
|
+
'run.inference_overridden',
|
|
191
|
+
'run.limit_reached',
|
|
192
|
+
'run.llm_call',
|
|
193
|
+
'run.model_fallback',
|
|
194
|
+
'run.running',
|
|
195
|
+
'run.starting',
|
|
196
|
+
'run.tool_call',
|
|
197
|
+
'run.tool_refused',
|
|
198
|
+
'run.training_loaded',
|
|
199
|
+
'setup.change',
|
|
200
|
+
'ui.card',
|
|
201
|
+
'workser_computer.conversation.gate',
|
|
202
|
+
'workser_computer.route.selected',
|
|
203
|
+
]);
|
|
204
|
+
/** Is this a well-formed runner event? The strict gate. */
|
|
205
|
+
function checkRunnerEvent(event) {
|
|
206
|
+
if (!event || typeof event !== 'object') {
|
|
207
|
+
return { ok: false, reason: 'An event must be an object.' };
|
|
208
|
+
}
|
|
209
|
+
if (typeof event.event_id !== 'string' || !event.event_id || event.event_id.length > 40) {
|
|
210
|
+
return { ok: false, reason: 'event_id must be a string of 1–40 characters.' };
|
|
211
|
+
}
|
|
212
|
+
if (!Number.isInteger(event.seq) || event.seq < 1) {
|
|
213
|
+
return { ok: false, reason: 'seq must be a positive whole number.' };
|
|
214
|
+
}
|
|
215
|
+
const shape = exports.RUN_EVENT_CATALOG[event.type];
|
|
216
|
+
if (!shape) {
|
|
217
|
+
return { ok: false, reason: `Unknown event type "${String(event.type)}".` };
|
|
218
|
+
}
|
|
219
|
+
const data = event.data ?? {};
|
|
220
|
+
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
|
221
|
+
return { ok: false, reason: `${event.type}: data must be an object.` };
|
|
222
|
+
}
|
|
223
|
+
const size = byteLength(data);
|
|
224
|
+
if (size > exports.MAX_EVENT_DATA_BYTES) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: `${event.type}: data is ${size} bytes; the limit is ${exports.MAX_EVENT_DATA_BYTES}. Put large output in a file.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
for (const [field, rule] of Object.entries(shape)) {
|
|
231
|
+
const value = data[field];
|
|
232
|
+
if (value === undefined || value === null) {
|
|
233
|
+
if (rule.required) {
|
|
234
|
+
return { ok: false, reason: `${event.type}: "${field}" is required.` };
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const actual = Array.isArray(value) ? 'array' : typeof value;
|
|
239
|
+
if (actual !== rule.type) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
reason: `${event.type}: "${field}" must be ${rule.type}, got ${actual}.`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (rule.type === 'number' && !Number.isFinite(value)) {
|
|
246
|
+
return { ok: false, reason: `${event.type}: "${field}" must be finite.` };
|
|
247
|
+
}
|
|
248
|
+
if (rule.oneOf && !rule.oneOf.includes(value)) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
reason: `${event.type}: "${field}" must be one of ${rule.oneOf.join(', ')}.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (rule.maxLength && value.length > rule.maxLength) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: `${event.type}: "${field}" is longer than ${rule.maxLength} characters.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { ok: true };
|
|
262
|
+
}
|
|
263
|
+
/** The lenient gate: only the envelope is checked. */
|
|
264
|
+
function checkLegacyEvent(event) {
|
|
265
|
+
if (typeof event.event_type !== 'string' || !event.event_type || event.event_type.length > 80) {
|
|
266
|
+
return { ok: false, reason: 'event_type must be a string of 1–80 characters.' };
|
|
267
|
+
}
|
|
268
|
+
if (typeof event.event_id !== 'string' || !event.event_id || event.event_id.length > 40) {
|
|
269
|
+
return { ok: false, reason: 'event_id must be a string of 1–40 characters.' };
|
|
270
|
+
}
|
|
271
|
+
if (!Number.isInteger(event.seq)) {
|
|
272
|
+
return { ok: false, reason: 'seq must be a whole number.' };
|
|
273
|
+
}
|
|
274
|
+
return { ok: true };
|
|
275
|
+
}
|
|
276
|
+
/** A type neither in the catalog nor in the known legacy list. */
|
|
277
|
+
function unknownLegacyType(type) {
|
|
278
|
+
return !exports.RUN_EVENT_CATALOG[type] && !exports.LEGACY_EVENT_TYPES.has(type);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The run status a runner event moves the run to, or null when it doesn't.
|
|
282
|
+
* Status only moves forward: a terminal run never changes again.
|
|
283
|
+
*/
|
|
284
|
+
function statusForRunnerEvent(type) {
|
|
285
|
+
switch (type) {
|
|
286
|
+
case 'run.started':
|
|
287
|
+
case 'run.resumed':
|
|
288
|
+
return 'RUNNING';
|
|
289
|
+
case 'run.completed':
|
|
290
|
+
return 'COMPLETED';
|
|
291
|
+
case 'run.failed':
|
|
292
|
+
return 'FAILED';
|
|
293
|
+
case 'run.cancelled':
|
|
294
|
+
return 'CANCELLED';
|
|
295
|
+
default:
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check a runner batch against the run's cursor (the last seq accepted).
|
|
301
|
+
*
|
|
302
|
+
* Events at or below the cursor are retries and are dropped here — their
|
|
303
|
+
* `event_id` is checked against the stored row by the caller. What remains
|
|
304
|
+
* must continue the cursor exactly: `cursor+1, cursor+2, …`. A gap means the
|
|
305
|
+
* runner lost an event, and it is told where to resend from rather than
|
|
306
|
+
* having the hole papered over.
|
|
307
|
+
*/
|
|
308
|
+
function checkBatchSeq(cursor, events) {
|
|
309
|
+
const sorted = [...events].sort((a, b) => a.seq - b.seq);
|
|
310
|
+
const fresh = sorted.filter((one) => one.seq > cursor);
|
|
311
|
+
let expected = cursor + 1;
|
|
312
|
+
for (const one of fresh) {
|
|
313
|
+
if (one.seq !== expected) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
expected,
|
|
317
|
+
reason: one.seq < expected
|
|
318
|
+
? `seq ${one.seq} appears twice in this batch.`
|
|
319
|
+
: `seq ${one.seq} skips ahead; expected ${expected}. Resend from ${expected}.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
expected += 1;
|
|
323
|
+
}
|
|
324
|
+
return { ok: true, fresh, nextCursor: expected - 1 };
|
|
325
|
+
}
|
|
326
|
+
// TextEncoder, not Buffer: the same file runs in browsers and in core-api.
|
|
327
|
+
const encoder = new TextEncoder();
|
|
328
|
+
function byteLength(value) {
|
|
329
|
+
try {
|
|
330
|
+
return encoder.encode(JSON.stringify(value)).length;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return Number.POSITIVE_INFINITY;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./catalog.js"), exports);
|
|
18
|
+
__exportStar(require("./ordered-feed.js"), exports);
|
|
19
|
+
__exportStar(require("./types.js"), exports);
|
|
20
|
+
__exportStar(require("./actions.js"), exports);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrderedFeed = exports.FEED_PAGE = void 0;
|
|
4
|
+
exports.FEED_PAGE = 500;
|
|
5
|
+
class OrderedFeed {
|
|
6
|
+
fetchAfter;
|
|
7
|
+
emit;
|
|
8
|
+
onError;
|
|
9
|
+
last;
|
|
10
|
+
chain = Promise.resolve();
|
|
11
|
+
closed = false;
|
|
12
|
+
constructor(start, fetchAfter, emit, onError = () => undefined) {
|
|
13
|
+
this.fetchAfter = fetchAfter;
|
|
14
|
+
this.emit = emit;
|
|
15
|
+
this.onError = onError;
|
|
16
|
+
this.last = Math.max(0, Math.floor(start || 0));
|
|
17
|
+
}
|
|
18
|
+
/** The position of the last row emitted. */
|
|
19
|
+
get cursor() {
|
|
20
|
+
return this.last;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Replay everything after the start cursor. Call once, right after creating.
|
|
24
|
+
*
|
|
25
|
+
* `show`, when given, hides replayed rows it rejects while still moving the
|
|
26
|
+
* cursor past them — for the old `after_seq` resume, which skipped rows by
|
|
27
|
+
* the writer's own number. Live rows are never filtered.
|
|
28
|
+
*/
|
|
29
|
+
start(show) {
|
|
30
|
+
return this.enqueue(() => this.drain(undefined, show));
|
|
31
|
+
}
|
|
32
|
+
/** A live row. Emitted in order; a skip ahead reads the gap first. */
|
|
33
|
+
offer(row) {
|
|
34
|
+
return this.enqueue(async () => {
|
|
35
|
+
const pos = row.pos;
|
|
36
|
+
if (pos == null || pos <= this.last)
|
|
37
|
+
return;
|
|
38
|
+
if (pos > this.last + 1) {
|
|
39
|
+
await this.drain(pos);
|
|
40
|
+
if (pos <= this.last)
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.push(row);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Read anything after the cursor that live delivery didn't bring. */
|
|
47
|
+
catchUp() {
|
|
48
|
+
return this.enqueue(() => this.drain());
|
|
49
|
+
}
|
|
50
|
+
/** Stop emitting. Pending work finishes silently. */
|
|
51
|
+
close() {
|
|
52
|
+
this.closed = true;
|
|
53
|
+
}
|
|
54
|
+
push(row, show) {
|
|
55
|
+
if (this.closed || row.pos == null || row.pos <= this.last)
|
|
56
|
+
return;
|
|
57
|
+
this.last = row.pos;
|
|
58
|
+
if (show && !show(row))
|
|
59
|
+
return;
|
|
60
|
+
this.emit(row);
|
|
61
|
+
}
|
|
62
|
+
/** Emit stored rows after the cursor, up to `until` when given. */
|
|
63
|
+
async drain(until, show) {
|
|
64
|
+
for (;;) {
|
|
65
|
+
if (this.closed)
|
|
66
|
+
return;
|
|
67
|
+
const rows = await this.fetchAfter(this.last, exports.FEED_PAGE);
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
if (row.pos == null)
|
|
70
|
+
continue;
|
|
71
|
+
if (until !== undefined && row.pos > until)
|
|
72
|
+
return;
|
|
73
|
+
this.push(row, show);
|
|
74
|
+
}
|
|
75
|
+
if (rows.length < exports.FEED_PAGE)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
enqueue(work) {
|
|
80
|
+
const next = this.chain.then(async () => {
|
|
81
|
+
if (this.closed)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
await work();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.onError(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
this.chain = next;
|
|
91
|
+
return next;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.OrderedFeed = OrderedFeed;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The shapes a run is made of, as core-api sends and accepts them.
|
|
4
|
+
*
|
|
5
|
+
* One shape per thing, with optional fields, rather than tagged unions:
|
|
6
|
+
* core-api builds without strictNullChecks, where a union doesn't narrow, and
|
|
7
|
+
* the same types are used on both sides of the wire.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.connectSse = exports.SseParser = void 0;
|
|
18
|
+
/**
|
|
19
|
+
* @workser/runtime — the run contract every Workser runner, screen and service
|
|
20
|
+
* speaks. See README.md for runner protocol v1.
|
|
21
|
+
*/
|
|
22
|
+
__exportStar(require("./contract/index.js"), exports);
|
|
23
|
+
__exportStar(require("./runner/index.js"), exports);
|
|
24
|
+
__exportStar(require("./client/index.js"), exports);
|
|
25
|
+
var sse_js_1 = require("./internal/sse.js");
|
|
26
|
+
Object.defineProperty(exports, "SseParser", { enumerable: true, get: function () { return sse_js_1.SseParser; } });
|
|
27
|
+
Object.defineProperty(exports, "connectSse", { enumerable: true, get: function () { return sse_js_1.connectSse; } });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AsyncQueue = void 0;
|
|
4
|
+
/** A push-to-pull bridge: callbacks push, an async iterator pulls. */
|
|
5
|
+
class AsyncQueue {
|
|
6
|
+
onReturn;
|
|
7
|
+
items = [];
|
|
8
|
+
waiters = [];
|
|
9
|
+
ended = false;
|
|
10
|
+
failure = null;
|
|
11
|
+
constructor(onReturn = () => undefined) {
|
|
12
|
+
this.onReturn = onReturn;
|
|
13
|
+
}
|
|
14
|
+
push(item) {
|
|
15
|
+
if (this.ended)
|
|
16
|
+
return;
|
|
17
|
+
const waiter = this.waiters.shift();
|
|
18
|
+
if (waiter)
|
|
19
|
+
waiter({ value: item, done: false });
|
|
20
|
+
else
|
|
21
|
+
this.items.push(item);
|
|
22
|
+
}
|
|
23
|
+
end(error) {
|
|
24
|
+
if (this.ended)
|
|
25
|
+
return;
|
|
26
|
+
this.ended = true;
|
|
27
|
+
if (error)
|
|
28
|
+
this.failure = error;
|
|
29
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
30
|
+
waiter({ value: undefined, done: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
next() {
|
|
34
|
+
const item = this.items.shift();
|
|
35
|
+
if (item !== undefined)
|
|
36
|
+
return Promise.resolve({ value: item, done: false });
|
|
37
|
+
if (this.ended) {
|
|
38
|
+
if (this.failure) {
|
|
39
|
+
const error = this.failure;
|
|
40
|
+
this.failure = null;
|
|
41
|
+
return Promise.reject(error);
|
|
42
|
+
}
|
|
43
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
46
|
+
}
|
|
47
|
+
return() {
|
|
48
|
+
this.end();
|
|
49
|
+
this.onReturn();
|
|
50
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
51
|
+
}
|
|
52
|
+
[Symbol.asyncIterator]() {
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.AsyncQueue = AsyncQueue;
|