@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -2
  3. package/dist/cjs/client/follow.js +103 -0
  4. package/dist/cjs/client/index.js +6 -0
  5. package/dist/cjs/contract/actions.js +223 -0
  6. package/dist/cjs/contract/catalog.js +335 -0
  7. package/dist/cjs/contract/index.js +20 -0
  8. package/dist/cjs/contract/ordered-feed.js +94 -0
  9. package/dist/cjs/contract/types.js +9 -0
  10. package/dist/cjs/engine/engine.js +2 -0
  11. package/dist/cjs/index.js +27 -0
  12. package/dist/cjs/internal/async-queue.js +56 -0
  13. package/dist/cjs/internal/sse.js +170 -0
  14. package/dist/cjs/package.json +1 -0
  15. package/dist/cjs/place/place.js +2 -0
  16. package/dist/cjs/runner/index.js +6 -0
  17. package/dist/cjs/runner/journal.js +82 -0
  18. package/dist/cjs/runner/runner-client.js +477 -0
  19. package/dist/client/follow.d.ts +45 -0
  20. package/dist/client/follow.js +100 -0
  21. package/dist/client/follow.js.map +1 -0
  22. package/dist/client/index.d.ts +1 -0
  23. package/dist/client/index.js +2 -0
  24. package/dist/client/index.js.map +1 -0
  25. package/dist/contract/actions.d.ts +73 -0
  26. package/dist/contract/actions.js +217 -0
  27. package/dist/contract/actions.js.map +1 -0
  28. package/dist/contract/catalog.d.ts +97 -0
  29. package/dist/contract/catalog.js +328 -0
  30. package/dist/contract/catalog.js.map +1 -0
  31. package/dist/contract/index.d.ts +4 -0
  32. package/dist/contract/index.js +5 -0
  33. package/dist/contract/index.js.map +1 -0
  34. package/dist/contract/ordered-feed.d.ts +56 -0
  35. package/dist/contract/ordered-feed.js +91 -0
  36. package/dist/contract/ordered-feed.js.map +1 -0
  37. package/dist/contract/types.d.ts +311 -0
  38. package/dist/contract/types.js +9 -0
  39. package/dist/contract/types.js.map +1 -0
  40. package/dist/engine/engine.d.ts +39 -0
  41. package/dist/engine/engine.js +2 -0
  42. package/dist/engine/engine.js.map +1 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +9 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/internal/async-queue.d.ts +14 -0
  47. package/dist/internal/async-queue.js +53 -0
  48. package/dist/internal/async-queue.js.map +1 -0
  49. package/dist/internal/sse.d.ts +57 -0
  50. package/dist/internal/sse.js +164 -0
  51. package/dist/internal/sse.js.map +1 -0
  52. package/dist/place/place.d.ts +81 -0
  53. package/dist/place/place.js +2 -0
  54. package/dist/place/place.js.map +1 -0
  55. package/dist/runner/index.d.ts +2 -0
  56. package/dist/runner/index.js +2 -0
  57. package/dist/runner/index.js.map +1 -0
  58. package/dist/runner/journal.d.ts +32 -0
  59. package/dist/runner/journal.js +79 -0
  60. package/dist/runner/journal.js.map +1 -0
  61. package/dist/runner/runner-client.d.ts +185 -0
  62. package/dist/runner/runner-client.js +473 -0
  63. package/dist/runner/runner-client.js.map +1 -0
  64. package/package.json +30 -4
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /**
3
+ * Server-Sent Events over `fetch`, for Node 18+ and browsers alike.
4
+ *
5
+ * Not `EventSource`: it can't send an `Authorization` header (a runner's token
6
+ * has to go somewhere), it doesn't exist in Node, and its reconnect can't be
7
+ * told to resume from a cursor we choose. This follows the HTML spec's parsing
8
+ * rules (line endings, `data` joining, comments, `id`, `retry`).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.SseHttpError = exports.SseParser = void 0;
12
+ exports.connectSse = connectSse;
13
+ exports.sleep = sleep;
14
+ /** Incremental parser: feed it text chunks, get whole messages back. */
15
+ class SseParser {
16
+ buffer = '';
17
+ data = [];
18
+ event = '';
19
+ lastId = '';
20
+ retry;
21
+ sawCr = false;
22
+ /** Parse one chunk. A message split across chunks is held until complete. */
23
+ push(chunk) {
24
+ const out = [];
25
+ let text = chunk;
26
+ // A `\r\n` split across two chunks: the `\n` belongs to the `\r` already
27
+ // treated as a line end.
28
+ if (this.sawCr && text.startsWith('\n'))
29
+ text = text.slice(1);
30
+ this.sawCr = false;
31
+ this.buffer += text;
32
+ for (;;) {
33
+ const match = /\r\n|\r|\n/.exec(this.buffer);
34
+ if (!match)
35
+ break;
36
+ const line = this.buffer.slice(0, match.index);
37
+ const endsWithLoneCr = match[0] === '\r' && match.index + 1 === this.buffer.length;
38
+ this.buffer = this.buffer.slice(match.index + match[0].length);
39
+ if (endsWithLoneCr)
40
+ this.sawCr = true;
41
+ const message = this.line(line);
42
+ if (message)
43
+ out.push(message);
44
+ }
45
+ return out;
46
+ }
47
+ line(line) {
48
+ if (line === '')
49
+ return this.dispatch();
50
+ if (line.startsWith(':'))
51
+ return null; // a comment (servers use them as pings)
52
+ const colon = line.indexOf(':');
53
+ const field = colon === -1 ? line : line.slice(0, colon);
54
+ let value = colon === -1 ? '' : line.slice(colon + 1);
55
+ if (value.startsWith(' '))
56
+ value = value.slice(1);
57
+ switch (field) {
58
+ case 'data':
59
+ this.data.push(value);
60
+ break;
61
+ case 'event':
62
+ this.event = value;
63
+ break;
64
+ case 'id':
65
+ if (!value.includes('\u0000'))
66
+ this.lastId = value;
67
+ break;
68
+ case 'retry':
69
+ if (/^\d+$/.test(value))
70
+ this.retry = Number(value);
71
+ break;
72
+ default:
73
+ break; // unknown fields are ignored
74
+ }
75
+ return null;
76
+ }
77
+ dispatch() {
78
+ const retry = this.retry;
79
+ this.retry = undefined;
80
+ if (this.data.length === 0) {
81
+ this.event = '';
82
+ return null;
83
+ }
84
+ const message = {
85
+ event: this.event || 'message',
86
+ data: this.data.join('\n'),
87
+ id: this.lastId,
88
+ };
89
+ if (retry !== undefined)
90
+ message.retry = retry;
91
+ this.data = [];
92
+ this.event = '';
93
+ return message;
94
+ }
95
+ }
96
+ exports.SseParser = SseParser;
97
+ /**
98
+ * Keep an SSE connection open until `signal` aborts: connect, parse, and on
99
+ * any drop reconnect with backoff. The caller's `url()` decides where to
100
+ * resume from, so reconnects continue rather than replay.
101
+ */
102
+ async function connectSse(options) {
103
+ const base = options.baseDelayMs ?? 500;
104
+ const max = options.maxDelayMs ?? 15_000;
105
+ const fatal = new Set(options.fatalStatuses ?? [401, 403, 404]);
106
+ let delay = base;
107
+ let serverRetry;
108
+ while (!options.signal.aborted) {
109
+ let failure = null;
110
+ try {
111
+ const response = await options.fetch(options.url(), {
112
+ headers: { Accept: 'text/event-stream', ...options.headers() },
113
+ signal: options.signal,
114
+ });
115
+ if (!response.ok || !response.body) {
116
+ const error = new SseHttpError(response.status);
117
+ if (fatal.has(response.status)) {
118
+ options.onError?.(error);
119
+ return;
120
+ }
121
+ throw error;
122
+ }
123
+ delay = base;
124
+ const parser = new SseParser();
125
+ const decoder = new TextDecoder();
126
+ const reader = response.body.getReader();
127
+ for (;;) {
128
+ const { value, done } = await reader.read();
129
+ if (done)
130
+ break;
131
+ for (const message of parser.push(decoder.decode(value, { stream: true }))) {
132
+ if (message.retry !== undefined)
133
+ serverRetry = message.retry;
134
+ options.onMessage(message);
135
+ }
136
+ }
137
+ }
138
+ catch (error) {
139
+ failure = error;
140
+ }
141
+ if (options.signal.aborted)
142
+ return;
143
+ if (failure && options.onError?.(failure) === false)
144
+ return;
145
+ await sleep(serverRetry ?? delay, options.signal);
146
+ delay = Math.min(delay * 2, max);
147
+ }
148
+ }
149
+ class SseHttpError extends Error {
150
+ status;
151
+ constructor(status) {
152
+ super(`The stream answered ${status}.`);
153
+ this.status = status;
154
+ this.name = 'SseHttpError';
155
+ }
156
+ }
157
+ exports.SseHttpError = SseHttpError;
158
+ function sleep(ms, signal) {
159
+ return new Promise((resolve) => {
160
+ if (signal?.aborted)
161
+ return resolve();
162
+ const timer = setTimeout(done, ms);
163
+ function done() {
164
+ clearTimeout(timer);
165
+ signal?.removeEventListener('abort', done);
166
+ resolve();
167
+ }
168
+ signal?.addEventListener('abort', done, { once: true });
169
+ });
170
+ }
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RunnerError = exports.RunnerClient = void 0;
4
+ var runner_client_js_1 = require("./runner-client.js");
5
+ Object.defineProperty(exports, "RunnerClient", { enumerable: true, get: function () { return runner_client_js_1.RunnerClient; } });
6
+ Object.defineProperty(exports, "RunnerError", { enumerable: true, get: function () { return runner_client_js_1.RunnerError; } });
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventJournal = void 0;
4
+ class EventJournal {
5
+ fs;
6
+ path;
7
+ pathMod;
8
+ dirty = false;
9
+ constructor(fs, path, pathMod) {
10
+ this.fs = fs;
11
+ this.path = path;
12
+ this.pathMod = pathMod;
13
+ }
14
+ static async open(path) {
15
+ const fs = await Promise.resolve().then(() => require('node:fs'));
16
+ const pathMod = await Promise.resolve().then(() => require('node:path'));
17
+ fs.mkdirSync(pathMod.dirname(path), { recursive: true, mode: 0o700 });
18
+ if (!fs.existsSync(path))
19
+ fs.writeFileSync(path, '', { mode: 0o600 });
20
+ return new EventJournal(fs, path, pathMod);
21
+ }
22
+ /** Events saved by an earlier process, oldest first, one per event id. */
23
+ load() {
24
+ let raw = '';
25
+ try {
26
+ raw = this.fs.readFileSync(this.path, 'utf8');
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ const byId = new Map();
32
+ for (const line of raw.split('\n')) {
33
+ if (!line.trim())
34
+ continue;
35
+ try {
36
+ const event = JSON.parse(line);
37
+ if (typeof event?.event_id === 'string' && Number.isInteger(event.seq) && typeof event.type === 'string') {
38
+ // A later line for the same id is a renumbering: it wins.
39
+ byId.set(event.event_id, { ...event, data: event.data ?? {} });
40
+ }
41
+ }
42
+ catch {
43
+ // A torn last line (killed mid-append) is skipped; its event was never
44
+ // acknowledged to the caller as sent, and every earlier line is whole.
45
+ }
46
+ }
47
+ return [...byId.values()].sort((a, b) => a.seq - b.seq);
48
+ }
49
+ append(event) {
50
+ this.fs.appendFileSync(this.path, `${JSON.stringify(event)}\n`, { mode: 0o600 });
51
+ this.dirty = true;
52
+ }
53
+ /** Make appended lines durable. Called before each send. */
54
+ sync() {
55
+ if (!this.dirty)
56
+ return;
57
+ const fd = this.fs.openSync(this.path, 'r+');
58
+ try {
59
+ this.fs.fsyncSync(fd);
60
+ }
61
+ finally {
62
+ this.fs.closeSync(fd);
63
+ }
64
+ this.dirty = false;
65
+ }
66
+ /** Replace the journal with what is still unsent. */
67
+ rewrite(events) {
68
+ const temp = this.pathMod.join(this.pathMod.dirname(this.path), `.journal-${process.pid}-${Date.now()}.tmp`);
69
+ const body = events.map((event) => JSON.stringify(event)).join('\n');
70
+ const fd = this.fs.openSync(temp, 'w', 0o600);
71
+ try {
72
+ this.fs.writeSync(fd, body ? `${body}\n` : '');
73
+ this.fs.fsyncSync(fd);
74
+ }
75
+ finally {
76
+ this.fs.closeSync(fd);
77
+ }
78
+ this.fs.renameSync(temp, this.path);
79
+ this.dirty = false;
80
+ }
81
+ }
82
+ exports.EventJournal = EventJournal;