mustardscript 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 (99) hide show
  1. package/Cargo.lock +1579 -0
  2. package/Cargo.toml +40 -0
  3. package/LICENSE +201 -0
  4. package/README.md +828 -0
  5. package/SECURITY.md +34 -0
  6. package/crates/mustard/Cargo.toml +31 -0
  7. package/crates/mustard/src/cancellation.rs +28 -0
  8. package/crates/mustard/src/diagnostic.rs +145 -0
  9. package/crates/mustard/src/ir.rs +435 -0
  10. package/crates/mustard/src/lib.rs +21 -0
  11. package/crates/mustard/src/limits.rs +22 -0
  12. package/crates/mustard/src/parser/expressions.rs +723 -0
  13. package/crates/mustard/src/parser/mod.rs +115 -0
  14. package/crates/mustard/src/parser/operators.rs +105 -0
  15. package/crates/mustard/src/parser/patterns.rs +123 -0
  16. package/crates/mustard/src/parser/scope.rs +107 -0
  17. package/crates/mustard/src/parser/statements.rs +298 -0
  18. package/crates/mustard/src/parser/tests/acceptance.rs +339 -0
  19. package/crates/mustard/src/parser/tests/mod.rs +2 -0
  20. package/crates/mustard/src/parser/tests/rejections.rs +107 -0
  21. package/crates/mustard/src/runtime/accounting.rs +613 -0
  22. package/crates/mustard/src/runtime/api.rs +192 -0
  23. package/crates/mustard/src/runtime/async_runtime/mod.rs +5 -0
  24. package/crates/mustard/src/runtime/async_runtime/promises.rs +246 -0
  25. package/crates/mustard/src/runtime/async_runtime/reactions.rs +400 -0
  26. package/crates/mustard/src/runtime/async_runtime/scheduler.rs +224 -0
  27. package/crates/mustard/src/runtime/builtins/arrays.rs +1205 -0
  28. package/crates/mustard/src/runtime/builtins/collections.rs +573 -0
  29. package/crates/mustard/src/runtime/builtins/install.rs +501 -0
  30. package/crates/mustard/src/runtime/builtins/intl.rs +553 -0
  31. package/crates/mustard/src/runtime/builtins/mod.rs +25 -0
  32. package/crates/mustard/src/runtime/builtins/objects.rs +405 -0
  33. package/crates/mustard/src/runtime/builtins/primitives.rs +859 -0
  34. package/crates/mustard/src/runtime/builtins/promises.rs +335 -0
  35. package/crates/mustard/src/runtime/builtins/regexp.rs +356 -0
  36. package/crates/mustard/src/runtime/builtins/strings.rs +803 -0
  37. package/crates/mustard/src/runtime/builtins/support.rs +561 -0
  38. package/crates/mustard/src/runtime/bytecode.rs +123 -0
  39. package/crates/mustard/src/runtime/compiler/assignments.rs +690 -0
  40. package/crates/mustard/src/runtime/compiler/bindings.rs +92 -0
  41. package/crates/mustard/src/runtime/compiler/context.rs +46 -0
  42. package/crates/mustard/src/runtime/compiler/control.rs +342 -0
  43. package/crates/mustard/src/runtime/compiler/expressions.rs +372 -0
  44. package/crates/mustard/src/runtime/compiler/mod.rs +173 -0
  45. package/crates/mustard/src/runtime/compiler/statements.rs +459 -0
  46. package/crates/mustard/src/runtime/conversions/boundary.rs +293 -0
  47. package/crates/mustard/src/runtime/conversions/coercions.rs +217 -0
  48. package/crates/mustard/src/runtime/conversions/errors.rs +118 -0
  49. package/crates/mustard/src/runtime/conversions/mod.rs +14 -0
  50. package/crates/mustard/src/runtime/conversions/operators.rs +334 -0
  51. package/crates/mustard/src/runtime/env.rs +355 -0
  52. package/crates/mustard/src/runtime/exceptions.rs +377 -0
  53. package/crates/mustard/src/runtime/gc.rs +595 -0
  54. package/crates/mustard/src/runtime/mod.rs +318 -0
  55. package/crates/mustard/src/runtime/properties.rs +1762 -0
  56. package/crates/mustard/src/runtime/serialization.rs +127 -0
  57. package/crates/mustard/src/runtime/shared.rs +108 -0
  58. package/crates/mustard/src/runtime/snapshot_validation_tests.rs +93 -0
  59. package/crates/mustard/src/runtime/state.rs +652 -0
  60. package/crates/mustard/src/runtime/tests/async_host.rs +104 -0
  61. package/crates/mustard/src/runtime/tests/collections.rs +50 -0
  62. package/crates/mustard/src/runtime/tests/diagnostics.rs +36 -0
  63. package/crates/mustard/src/runtime/tests/exceptions.rs +122 -0
  64. package/crates/mustard/src/runtime/tests/execution.rs +553 -0
  65. package/crates/mustard/src/runtime/tests/gc.rs +533 -0
  66. package/crates/mustard/src/runtime/tests/mod.rs +56 -0
  67. package/crates/mustard/src/runtime/tests/serialization.rs +170 -0
  68. package/crates/mustard/src/runtime/validation/bytecode.rs +484 -0
  69. package/crates/mustard/src/runtime/validation/mod.rs +14 -0
  70. package/crates/mustard/src/runtime/validation/policy.rs +94 -0
  71. package/crates/mustard/src/runtime/validation/snapshot.rs +406 -0
  72. package/crates/mustard/src/runtime/validation/walk.rs +206 -0
  73. package/crates/mustard/src/runtime/vm.rs +1016 -0
  74. package/crates/mustard/src/span.rs +22 -0
  75. package/crates/mustard/src/structured.rs +107 -0
  76. package/crates/mustard-bridge/Cargo.toml +17 -0
  77. package/crates/mustard-bridge/src/codec.rs +46 -0
  78. package/crates/mustard-bridge/src/dto.rs +99 -0
  79. package/crates/mustard-bridge/src/lib.rs +12 -0
  80. package/crates/mustard-bridge/src/operations.rs +142 -0
  81. package/crates/mustard-node/Cargo.toml +24 -0
  82. package/crates/mustard-node/build.rs +3 -0
  83. package/crates/mustard-node/src/lib.rs +236 -0
  84. package/crates/mustard-sidecar/Cargo.toml +21 -0
  85. package/crates/mustard-sidecar/src/lib.rs +134 -0
  86. package/crates/mustard-sidecar/src/main.rs +36 -0
  87. package/dist/index.js +20 -0
  88. package/dist/install.js +117 -0
  89. package/dist/lib/cancellation.js +124 -0
  90. package/dist/lib/errors.js +46 -0
  91. package/dist/lib/executor.js +555 -0
  92. package/dist/lib/policy.js +292 -0
  93. package/dist/lib/progress.js +356 -0
  94. package/dist/lib/runtime.js +109 -0
  95. package/dist/lib/structured.js +286 -0
  96. package/dist/native-loader.js +227 -0
  97. package/index.d.ts +23 -0
  98. package/mustard.d.ts +220 -0
  99. package/package.json +97 -0
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const KNOWN_ERROR_KINDS = new Set([
4
+ 'Parse',
5
+ 'Validation',
6
+ 'Runtime',
7
+ 'Limit',
8
+ 'Serialization',
9
+ ]);
10
+
11
+ class MustardError extends Error {
12
+ constructor(kind, message, cause) {
13
+ super(message, { cause });
14
+ this.kind = kind;
15
+ this.name = `Mustard${kind}Error`;
16
+ }
17
+ }
18
+
19
+ function normalizeNativeError(error) {
20
+ if (!(error instanceof Error)) {
21
+ return error;
22
+ }
23
+ const match = /^([A-Za-z]+):\s([\s\S]+)$/.exec(error.message);
24
+ if (!match) {
25
+ return error;
26
+ }
27
+ const [, kind, message] = match;
28
+ if (!KNOWN_ERROR_KINDS.has(kind)) {
29
+ return error;
30
+ }
31
+ return new MustardError(kind, message, error);
32
+ }
33
+
34
+ function callNative(fn, ...args) {
35
+ try {
36
+ return fn(...args);
37
+ } catch (error) {
38
+ throw normalizeNativeError(error);
39
+ }
40
+ }
41
+
42
+ module.exports = {
43
+ MustardError,
44
+ callNative,
45
+ normalizeNativeError,
46
+ };
@@ -0,0 +1,555 @@
1
+ 'use strict';
2
+
3
+ const { createHmac, randomUUID } = require('node:crypto');
4
+ const { setTimeout: delay } = require('node:timers/promises');
5
+ const { types } = require('node:util');
6
+
7
+ const { MustardError, normalizeNativeError } = require('./errors.js');
8
+ const { normalizeSnapshotKey } = require('./policy.js');
9
+
10
+ const TERMINAL_STATES = new Set(['completed', 'failed', 'cancelled']);
11
+ const RUNNABLE_STATES = new Set(['queued', 'waiting']);
12
+ const DEFAULT_MAX_CONCURRENT_JOBS = 1;
13
+ const DEFAULT_POLL_INTERVAL_MS = 25;
14
+ const EXECUTOR_JOB_SNAPSHOT_KEY_LABEL = 'mustard-executor-job-snapshot-key';
15
+
16
+ function assertPlainObject(value, label) {
17
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || types.isProxy(value)) {
18
+ throw new TypeError(`${label} must be a plain object`);
19
+ }
20
+ const prototype = Object.getPrototypeOf(value);
21
+ if (prototype !== Object.prototype && prototype !== null) {
22
+ throw new TypeError(`${label} must be a plain object`);
23
+ }
24
+ }
25
+
26
+ function cloneJobRecord(record) {
27
+ return structuredClone(record);
28
+ }
29
+
30
+ function clonePersistedProgress(progress) {
31
+ return {
32
+ capability: progress.capability,
33
+ args: structuredClone(progress.args),
34
+ snapshot: Buffer.from(progress.snapshot),
35
+ snapshot_id: progress.snapshot_id,
36
+ snapshot_key_digest: progress.snapshot_key_digest,
37
+ token: progress.token,
38
+ };
39
+ }
40
+
41
+ function sanitizeFailure(error) {
42
+ const normalized = normalizeNativeError(error);
43
+ if (normalized instanceof MustardError) {
44
+ return {
45
+ name: normalized.name,
46
+ message: normalized.message,
47
+ };
48
+ }
49
+ if (normalized && typeof normalized === 'object') {
50
+ const name =
51
+ typeof normalized.name === 'string' && normalized.name.length > 0
52
+ ? normalized.name
53
+ : 'Error';
54
+ const message =
55
+ typeof normalized.message === 'string' && normalized.message.length > 0
56
+ ? normalized.message
57
+ : String(normalized);
58
+ const sanitized = { name, message };
59
+ if (typeof normalized.code === 'string') {
60
+ sanitized.code = normalized.code;
61
+ }
62
+ if (normalized.details !== undefined) {
63
+ sanitized.details = normalized.details;
64
+ }
65
+ return sanitized;
66
+ }
67
+ return {
68
+ name: 'Error',
69
+ message: String(normalized),
70
+ };
71
+ }
72
+
73
+ function isCancellationFailure(error) {
74
+ const normalized = normalizeNativeError(error);
75
+ return (
76
+ normalized instanceof MustardError &&
77
+ normalized.kind === 'Limit' &&
78
+ normalized.message.includes('execution cancelled')
79
+ );
80
+ }
81
+
82
+ function deriveJobSnapshotKey(snapshotKey, jobId) {
83
+ return createHmac('sha256', snapshotKey)
84
+ .update(EXECUTOR_JOB_SNAPSHOT_KEY_LABEL, 'utf8')
85
+ .update('\0', 'utf8')
86
+ .update(String(jobId), 'utf8')
87
+ .digest();
88
+ }
89
+
90
+ function validateTransition(currentState, nextState) {
91
+ if (currentState === nextState) {
92
+ return;
93
+ }
94
+ const allowed = new Set([
95
+ 'queued:running',
96
+ 'queued:cancelled',
97
+ 'running:waiting',
98
+ 'running:completed',
99
+ 'running:failed',
100
+ 'running:cancelled',
101
+ 'waiting:running',
102
+ 'waiting:failed',
103
+ 'waiting:cancelled',
104
+ ]);
105
+ if (!allowed.has(`${currentState}:${nextState}`)) {
106
+ throw new Error(`invalid job transition from ${currentState} to ${nextState}`);
107
+ }
108
+ }
109
+
110
+ async function waitForSignalOrDelay(signal, ms) {
111
+ if (signal?.aborted) {
112
+ return;
113
+ }
114
+ try {
115
+ await delay(ms, undefined, { signal });
116
+ } catch (error) {
117
+ if (error?.name !== 'AbortError') {
118
+ throw error;
119
+ }
120
+ }
121
+ }
122
+
123
+ class InMemoryMustardExecutorStore {
124
+ constructor() {
125
+ this._jobs = new Map();
126
+ this._progress = new Map();
127
+ this._claims = new Map();
128
+ this._cancellationRequests = new Set();
129
+ }
130
+
131
+ _jobs;
132
+ _progress;
133
+ _claims;
134
+ _cancellationRequests;
135
+
136
+ async enqueue(record) {
137
+ if (this._jobs.has(record.jobId)) {
138
+ return {
139
+ jobId: record.jobId,
140
+ inserted: false,
141
+ };
142
+ }
143
+ this._jobs.set(record.jobId, cloneJobRecord(record));
144
+ return {
145
+ jobId: record.jobId,
146
+ inserted: true,
147
+ };
148
+ }
149
+
150
+ async get(jobId) {
151
+ const record = this._jobs.get(jobId);
152
+ return record ? cloneJobRecord(record) : null;
153
+ }
154
+
155
+ async claimRunnable(limit, workerId) {
156
+ const claimed = [];
157
+ for (const [jobId, record] of this._jobs.entries()) {
158
+ if (claimed.length >= limit) {
159
+ break;
160
+ }
161
+ if (!RUNNABLE_STATES.has(record.state)) {
162
+ continue;
163
+ }
164
+ if (this._claims.has(jobId)) {
165
+ continue;
166
+ }
167
+ this._claims.set(jobId, workerId);
168
+ claimed.push(jobId);
169
+ }
170
+ return claimed;
171
+ }
172
+
173
+ async releaseClaim(jobId, workerId) {
174
+ const owner = this._claims.get(jobId);
175
+ if (owner === workerId) {
176
+ this._claims.delete(jobId);
177
+ }
178
+ }
179
+
180
+ async update(jobId, patch) {
181
+ const current = this._jobs.get(jobId);
182
+ if (!current) {
183
+ throw new Error(`unknown job id ${jobId}`);
184
+ }
185
+ const nextState = patch.state ?? current.state;
186
+ validateTransition(current.state, nextState);
187
+ const next = {
188
+ ...current,
189
+ ...cloneJobRecord({ ...current, ...patch }),
190
+ state: nextState,
191
+ updatedAt: Date.now(),
192
+ };
193
+ this._jobs.set(jobId, next);
194
+ }
195
+
196
+ async saveProgress(jobId, progress) {
197
+ const record = this._jobs.get(jobId);
198
+ if (!record) {
199
+ throw new Error(`unknown job id ${jobId}`);
200
+ }
201
+ if (TERMINAL_STATES.has(record.state)) {
202
+ throw new Error(`cannot save progress for terminal job ${jobId}`);
203
+ }
204
+ this._progress.set(jobId, clonePersistedProgress(progress));
205
+ }
206
+
207
+ async loadProgress(jobId) {
208
+ const progress = this._progress.get(jobId);
209
+ return progress ? clonePersistedProgress(progress) : null;
210
+ }
211
+
212
+ async deleteProgress(jobId) {
213
+ this._progress.delete(jobId);
214
+ }
215
+
216
+ async requestCancel(jobId) {
217
+ const record = this._jobs.get(jobId);
218
+ if (!record) {
219
+ return 'ignored';
220
+ }
221
+ if (TERMINAL_STATES.has(record.state)) {
222
+ return 'ignored';
223
+ }
224
+ if (record.state === 'queued') {
225
+ this._jobs.set(jobId, {
226
+ ...record,
227
+ state: 'cancelled',
228
+ capability: undefined,
229
+ args: undefined,
230
+ result: undefined,
231
+ error: {
232
+ name: 'MustardLimitError',
233
+ message: 'execution cancelled',
234
+ },
235
+ updatedAt: Date.now(),
236
+ });
237
+ this._progress.delete(jobId);
238
+ return 'cancelled';
239
+ }
240
+ this._cancellationRequests.add(jobId);
241
+ return 'requested';
242
+ }
243
+
244
+ async consumeCancel(jobId) {
245
+ if (!this._cancellationRequests.has(jobId)) {
246
+ return false;
247
+ }
248
+ this._cancellationRequests.delete(jobId);
249
+ return true;
250
+ }
251
+ }
252
+
253
+ function createExecutorApi({ Mustard, Progress }) {
254
+ class MustardExecutor {
255
+ constructor(options) {
256
+ if (options === null || typeof options !== 'object') {
257
+ throw new TypeError('MustardExecutor options must be an object');
258
+ }
259
+ const {
260
+ program,
261
+ capabilities,
262
+ snapshotKey,
263
+ store,
264
+ limits = {},
265
+ } = options;
266
+ if (!(program instanceof Mustard)) {
267
+ throw new TypeError('MustardExecutor options.program must be a Mustard instance');
268
+ }
269
+ assertPlainObject(capabilities, 'MustardExecutor options.capabilities');
270
+ if (store === undefined || store === null || typeof store !== 'object') {
271
+ throw new TypeError('MustardExecutor options.store must be a MustardExecutorStore');
272
+ }
273
+ for (const method of [
274
+ 'enqueue',
275
+ 'get',
276
+ 'claimRunnable',
277
+ 'releaseClaim',
278
+ 'update',
279
+ 'saveProgress',
280
+ 'loadProgress',
281
+ 'deleteProgress',
282
+ 'requestCancel',
283
+ 'consumeCancel',
284
+ ]) {
285
+ if (typeof store[method] !== 'function') {
286
+ throw new TypeError(`MustardExecutor options.store is missing ${method}()`);
287
+ }
288
+ }
289
+ this._program = program;
290
+ this._capabilities = capabilities;
291
+ this._snapshotKey = normalizeSnapshotKey(
292
+ snapshotKey,
293
+ 'MustardExecutor options.snapshotKey',
294
+ );
295
+ this._store = store;
296
+ this._limits = { ...limits };
297
+ }
298
+
299
+ _program;
300
+ _capabilities;
301
+ _snapshotKey;
302
+ _store;
303
+ _limits;
304
+
305
+ async enqueue(input, options = {}) {
306
+ assertPlainObject(input, 'MustardExecutor.enqueue() input');
307
+ if (options === null || typeof options !== 'object') {
308
+ throw new TypeError('MustardExecutor.enqueue() options must be an object');
309
+ }
310
+ const now = Date.now();
311
+ const jobId = options.jobId ?? randomUUID();
312
+ const record = {
313
+ jobId,
314
+ state: 'queued',
315
+ input: structuredClone(input),
316
+ attempts: 0,
317
+ createdAt: now,
318
+ updatedAt: now,
319
+ };
320
+ const result = await this._store.enqueue(record);
321
+ return result.jobId;
322
+ }
323
+
324
+ async get(jobId) {
325
+ return this._store.get(jobId);
326
+ }
327
+
328
+ async cancel(jobId) {
329
+ await this._store.requestCancel(jobId);
330
+ }
331
+
332
+ async runWorker(options = {}) {
333
+ if (options === null || typeof options !== 'object') {
334
+ throw new TypeError('MustardExecutor.runWorker() options must be an object');
335
+ }
336
+ const workerId = randomUUID();
337
+ const maxConcurrentJobs = options.maxConcurrentJobs ?? DEFAULT_MAX_CONCURRENT_JOBS;
338
+ if (!Number.isInteger(maxConcurrentJobs) || maxConcurrentJobs <= 0) {
339
+ throw new TypeError('MustardExecutor.runWorker() maxConcurrentJobs must be a positive integer');
340
+ }
341
+ const signal = options.signal;
342
+ const drain = options.drain === true;
343
+ const inFlight = new Set();
344
+
345
+ while (!signal?.aborted) {
346
+ const available = maxConcurrentJobs - inFlight.size;
347
+ let claimed = [];
348
+ if (available > 0) {
349
+ claimed = await this._store.claimRunnable(available, workerId, Date.now());
350
+ for (const jobId of claimed) {
351
+ const task = this._processClaimedJob(jobId, workerId).finally(() => {
352
+ inFlight.delete(task);
353
+ });
354
+ inFlight.add(task);
355
+ }
356
+ }
357
+
358
+ if (drain && inFlight.size === 0 && claimed.length === 0) {
359
+ return;
360
+ }
361
+
362
+ if (inFlight.size === 0) {
363
+ await waitForSignalOrDelay(signal, DEFAULT_POLL_INTERVAL_MS);
364
+ continue;
365
+ }
366
+
367
+ if (claimed.length === 0 || inFlight.size >= maxConcurrentJobs) {
368
+ await Promise.race(inFlight);
369
+ }
370
+ }
371
+
372
+ await Promise.allSettled(inFlight);
373
+ }
374
+
375
+ async _processClaimedJob(jobId, workerId) {
376
+ try {
377
+ const record = await this._store.get(jobId);
378
+ if (record === null || TERMINAL_STATES.has(record.state)) {
379
+ return;
380
+ }
381
+
382
+ if (record.state === 'queued') {
383
+ await this._store.update(jobId, {
384
+ state: 'running',
385
+ attempts: record.attempts + 1,
386
+ capability: undefined,
387
+ args: undefined,
388
+ result: undefined,
389
+ error: undefined,
390
+ });
391
+ let step;
392
+ try {
393
+ const snapshotKey = deriveJobSnapshotKey(this._snapshotKey, jobId);
394
+ step = this._program.start({
395
+ inputs: record.input,
396
+ capabilities: this._capabilities,
397
+ limits: this._limits,
398
+ snapshotKey,
399
+ });
400
+ } catch (error) {
401
+ await this._failJob(jobId, error);
402
+ return;
403
+ }
404
+ await this._driveExecution(jobId, step);
405
+ return;
406
+ }
407
+
408
+ if (record.state === 'waiting') {
409
+ await this._resumeWaitingJob(jobId, record);
410
+ }
411
+ } finally {
412
+ await this._store.releaseClaim(jobId, workerId);
413
+ }
414
+ }
415
+
416
+ async _resumeWaitingJob(jobId, record) {
417
+ const dumped = await this._store.loadProgress(jobId);
418
+ if (dumped === null) {
419
+ await this._failJob(jobId, new MustardError('Serialization', `missing stored progress for job ${jobId}`));
420
+ return;
421
+ }
422
+ const snapshotKey = deriveJobSnapshotKey(this._snapshotKey, jobId);
423
+
424
+ let progress;
425
+ try {
426
+ progress = Progress.load(dumped, {
427
+ capabilities: this._capabilities,
428
+ limits: this._limits,
429
+ snapshotKey,
430
+ });
431
+ } catch (error) {
432
+ await this._failJob(jobId, error);
433
+ return;
434
+ }
435
+
436
+ if (await this._store.consumeCancel(jobId)) {
437
+ await this._store.update(jobId, {
438
+ state: 'running',
439
+ capability: undefined,
440
+ args: undefined,
441
+ });
442
+ try {
443
+ const step = progress.cancel();
444
+ await this._driveExecution(jobId, step);
445
+ } catch (error) {
446
+ if (isCancellationFailure(error)) {
447
+ await this._cancelJob(jobId, error);
448
+ } else {
449
+ await this._failJob(jobId, error);
450
+ }
451
+ }
452
+ return;
453
+ }
454
+
455
+ const handler = this._capabilities[progress.capability];
456
+ if (typeof handler !== 'function') {
457
+ await this._failJob(
458
+ jobId,
459
+ new MustardError('Runtime', `Missing capability: ${progress.capability}`),
460
+ );
461
+ return;
462
+ }
463
+
464
+ let outcome;
465
+ try {
466
+ outcome = {
467
+ type: 'value',
468
+ value: await handler(...progress.args),
469
+ };
470
+ } catch (error) {
471
+ outcome = {
472
+ type: 'error',
473
+ error,
474
+ };
475
+ }
476
+
477
+ await this._store.update(jobId, {
478
+ state: 'running',
479
+ capability: undefined,
480
+ args: undefined,
481
+ });
482
+
483
+ try {
484
+ const shouldCancel = await this._store.consumeCancel(jobId);
485
+ const step = shouldCancel
486
+ ? progress.cancel()
487
+ : outcome.type === 'value'
488
+ ? progress.resume(outcome.value)
489
+ : progress.resumeError(outcome.error);
490
+ await this._driveExecution(jobId, step);
491
+ } catch (error) {
492
+ if (isCancellationFailure(error)) {
493
+ await this._cancelJob(jobId, error);
494
+ } else {
495
+ await this._failJob(jobId, error);
496
+ }
497
+ }
498
+ }
499
+
500
+ async _driveExecution(jobId, step) {
501
+ let current = step;
502
+ while (current instanceof Progress) {
503
+ await this._store.saveProgress(jobId, current.dump());
504
+ await this._store.update(jobId, {
505
+ state: 'waiting',
506
+ capability: current.capability,
507
+ args: structuredClone(current.args),
508
+ });
509
+ await this._resumeWaitingJob(jobId, await this._store.get(jobId));
510
+ return;
511
+ }
512
+
513
+ await this._store.update(jobId, {
514
+ state: 'completed',
515
+ capability: undefined,
516
+ args: undefined,
517
+ result: current,
518
+ error: undefined,
519
+ });
520
+ await this._store.deleteProgress(jobId);
521
+ }
522
+
523
+ async _failJob(jobId, error) {
524
+ await this._store.update(jobId, {
525
+ state: 'failed',
526
+ capability: undefined,
527
+ args: undefined,
528
+ result: undefined,
529
+ error: sanitizeFailure(error),
530
+ });
531
+ await this._store.deleteProgress(jobId);
532
+ }
533
+
534
+ async _cancelJob(jobId, error) {
535
+ await this._store.update(jobId, {
536
+ state: 'cancelled',
537
+ capability: undefined,
538
+ args: undefined,
539
+ result: undefined,
540
+ error: sanitizeFailure(error),
541
+ });
542
+ await this._store.deleteProgress(jobId);
543
+ }
544
+ }
545
+
546
+ return {
547
+ InMemoryMustardExecutorStore,
548
+ MustardExecutor,
549
+ };
550
+ }
551
+
552
+ module.exports = {
553
+ createExecutorApi,
554
+ InMemoryMustardExecutorStore,
555
+ };