duroxide 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 +158 -0
- package/index.d.ts +220 -0
- package/index.js +321 -0
- package/lib/duroxide.js +727 -0
- package/package.json +47 -0
package/lib/duroxide.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duroxide - Node.js SDK for the Duroxide durable execution runtime.
|
|
3
|
+
*
|
|
4
|
+
* Generator-based orchestrations: users write function*(ctx, input) { ... }
|
|
5
|
+
* and yield ScheduledTask descriptors. The Rust runtime handles DurableFutures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Load native bindings from the auto-generated napi-rs loader
|
|
9
|
+
const { JsSqliteProvider, JsPostgresProvider, JsClient, JsRuntime, activityTraceLog, orchestrationTraceLog, activityIsCancelled } = require('../index.js');
|
|
10
|
+
|
|
11
|
+
// ─── Generator Driver ────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** @type {Map<number, Generator>} */
|
|
14
|
+
const generators = new Map();
|
|
15
|
+
let nextGeneratorId = 1;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registered orchestration functions keyed by name.
|
|
19
|
+
* @type {Map<string, GeneratorFunction>}
|
|
20
|
+
*/
|
|
21
|
+
const orchestrationFunctions = new Map();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Called from Rust via ThreadsafeFunction when starting a new orchestration.
|
|
25
|
+
* Creates a generator, drives it to the first yield, and returns the result.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} payloadJson - JSON: { ctxInfo, input }
|
|
28
|
+
* @returns {string} JSON: GeneratorStepResult
|
|
29
|
+
*/
|
|
30
|
+
function createGenerator(payloadJson) {
|
|
31
|
+
try {
|
|
32
|
+
const payload = JSON.parse(payloadJson);
|
|
33
|
+
const { ctxInfo, input } = payload;
|
|
34
|
+
|
|
35
|
+
// Find the orchestration function by name@version, falling back to name
|
|
36
|
+
const orchName = ctxInfo.orchestrationName;
|
|
37
|
+
const orchVersion = ctxInfo.orchestrationVersion;
|
|
38
|
+
const versionedKey = orchVersion ? `${orchName}@${orchVersion}` : null;
|
|
39
|
+
const fn = (versionedKey && orchestrationFunctions.get(versionedKey)) || orchestrationFunctions.get(orchName);
|
|
40
|
+
if (!fn) {
|
|
41
|
+
const err = JSON.stringify({
|
|
42
|
+
status: 'error',
|
|
43
|
+
message: `Orchestration '${orchName}' not registered on JS side`,
|
|
44
|
+
});
|
|
45
|
+
return err;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create the orchestration context wrapper
|
|
49
|
+
const ctx = new OrchestrationContext(ctxInfo);
|
|
50
|
+
|
|
51
|
+
// Create the generator
|
|
52
|
+
const gen = fn(ctx, input ? JSON.parse(input) : undefined);
|
|
53
|
+
|
|
54
|
+
// Assign an ID and store
|
|
55
|
+
const id = nextGeneratorId++;
|
|
56
|
+
generators.set(id, gen);
|
|
57
|
+
|
|
58
|
+
// Drive to first yield
|
|
59
|
+
return driveStep(id, gen, undefined);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
status: 'error',
|
|
63
|
+
message: String(e.stack || e),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Called from Rust via ThreadsafeFunction to feed a result and get the next step.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} payloadJson - JSON: { generatorId, result, isError }
|
|
72
|
+
* @returns {string} JSON: GeneratorStepResult
|
|
73
|
+
*/
|
|
74
|
+
function nextStep(payloadJson) {
|
|
75
|
+
try {
|
|
76
|
+
const { generatorId, result, isError } = JSON.parse(payloadJson);
|
|
77
|
+
|
|
78
|
+
const gen = generators.get(generatorId);
|
|
79
|
+
if (!gen) {
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
status: 'error',
|
|
82
|
+
message: `Generator ${generatorId} not found`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse the result from Rust
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(result);
|
|
90
|
+
} catch {
|
|
91
|
+
value = result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isError) {
|
|
95
|
+
return driveStepWithError(generatorId, gen, value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return driveStep(generatorId, gen, value);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
status: 'error',
|
|
102
|
+
message: String(e.stack || e),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Called from Rust to clean up a generator.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} idStr - Generator ID as string
|
|
111
|
+
* @returns {string} "ok"
|
|
112
|
+
*/
|
|
113
|
+
function disposeGenerator(idStr) {
|
|
114
|
+
const id = parseInt(idStr, 10);
|
|
115
|
+
generators.delete(id);
|
|
116
|
+
return 'ok';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Drive a generator one step forward with a value.
|
|
121
|
+
* @returns {string} JSON GeneratorStepResult
|
|
122
|
+
*/
|
|
123
|
+
function driveStep(generatorId, gen, value) {
|
|
124
|
+
try {
|
|
125
|
+
const { value: task, done } = gen.next(value);
|
|
126
|
+
|
|
127
|
+
if (done) {
|
|
128
|
+
generators.delete(generatorId);
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
status: 'completed',
|
|
131
|
+
output: JSON.stringify(task === undefined ? null : task),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// task should be a ScheduledTask descriptor
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
status: 'yielded',
|
|
138
|
+
generatorId,
|
|
139
|
+
task,
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
generators.delete(generatorId);
|
|
143
|
+
return JSON.stringify({
|
|
144
|
+
status: 'error',
|
|
145
|
+
message: String(e.stack || e),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Drive a generator by throwing an error into it.
|
|
152
|
+
* @returns {string} JSON GeneratorStepResult
|
|
153
|
+
*/
|
|
154
|
+
function driveStepWithError(generatorId, gen, error) {
|
|
155
|
+
try {
|
|
156
|
+
const { value: task, done } = gen.throw(
|
|
157
|
+
new Error(typeof error === 'string' ? error : JSON.stringify(error))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (done) {
|
|
161
|
+
generators.delete(generatorId);
|
|
162
|
+
return JSON.stringify({
|
|
163
|
+
status: 'completed',
|
|
164
|
+
output: JSON.stringify(task === undefined ? null : task),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
status: 'yielded',
|
|
170
|
+
generatorId,
|
|
171
|
+
task,
|
|
172
|
+
});
|
|
173
|
+
} catch (e) {
|
|
174
|
+
generators.delete(generatorId);
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
status: 'error',
|
|
177
|
+
message: String(e.stack || e),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── OrchestrationContext ────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Context object passed to orchestration generator functions.
|
|
186
|
+
* Methods that schedule work return ScheduledTask descriptors to be yielded.
|
|
187
|
+
* Logging methods are fire-and-forget (no yield needed).
|
|
188
|
+
*/
|
|
189
|
+
class OrchestrationContext {
|
|
190
|
+
constructor(ctxInfo) {
|
|
191
|
+
this.instanceId = ctxInfo.instanceId;
|
|
192
|
+
this.executionId = ctxInfo.executionId;
|
|
193
|
+
this.orchestrationName = ctxInfo.orchestrationName;
|
|
194
|
+
this.orchestrationVersion = ctxInfo.orchestrationVersion;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Scheduling (yield these) ──────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Schedule an activity. Yield the return value.
|
|
201
|
+
* @param {string} name - Activity name
|
|
202
|
+
* @param {*} input - Activity input (will be JSON-serialized)
|
|
203
|
+
* @returns {ScheduledTask}
|
|
204
|
+
*/
|
|
205
|
+
scheduleActivity(name, input) {
|
|
206
|
+
return {
|
|
207
|
+
type: 'activity',
|
|
208
|
+
name,
|
|
209
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Schedule an activity with retry policy. Yield the return value.
|
|
215
|
+
*/
|
|
216
|
+
scheduleActivityWithRetry(name, input, retry) {
|
|
217
|
+
return {
|
|
218
|
+
type: 'activityWithRetry',
|
|
219
|
+
name,
|
|
220
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
221
|
+
retry: {
|
|
222
|
+
maxAttempts: retry.maxAttempts,
|
|
223
|
+
timeoutMs: retry.timeoutMs,
|
|
224
|
+
totalTimeoutMs: retry.totalTimeoutMs,
|
|
225
|
+
backoff: retry.backoff,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Schedule a timer (delay in milliseconds). Yield the return value.
|
|
232
|
+
* @param {number} delayMs
|
|
233
|
+
* @returns {ScheduledTask}
|
|
234
|
+
*/
|
|
235
|
+
scheduleTimer(delayMs) {
|
|
236
|
+
return { type: 'timer', delayMs };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wait for an external event. Yield the return value.
|
|
241
|
+
* @param {string} name - Event name
|
|
242
|
+
* @returns {ScheduledTask}
|
|
243
|
+
*/
|
|
244
|
+
waitForEvent(name) {
|
|
245
|
+
return { type: 'waitEvent', name };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Schedule a sub-orchestration. Yield the return value.
|
|
250
|
+
* @param {string} name - Orchestration name
|
|
251
|
+
* @param {*} input
|
|
252
|
+
* @returns {ScheduledTask}
|
|
253
|
+
*/
|
|
254
|
+
scheduleSubOrchestration(name, input) {
|
|
255
|
+
return {
|
|
256
|
+
type: 'subOrchestration',
|
|
257
|
+
name,
|
|
258
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Schedule a sub-orchestration with a specific instance ID.
|
|
264
|
+
*/
|
|
265
|
+
scheduleSubOrchestrationWithId(name, instanceId, input) {
|
|
266
|
+
return {
|
|
267
|
+
type: 'subOrchestrationWithId',
|
|
268
|
+
name,
|
|
269
|
+
instanceId,
|
|
270
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Schedule a versioned sub-orchestration. Yield the return value.
|
|
276
|
+
*/
|
|
277
|
+
scheduleSubOrchestrationVersioned(name, version, input) {
|
|
278
|
+
return {
|
|
279
|
+
type: 'subOrchestrationVersioned',
|
|
280
|
+
name,
|
|
281
|
+
version: version || null,
|
|
282
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Schedule a versioned sub-orchestration with a specific instance ID. Yield the return value.
|
|
288
|
+
*/
|
|
289
|
+
scheduleSubOrchestrationVersionedWithId(name, version, instanceId, input) {
|
|
290
|
+
return {
|
|
291
|
+
type: 'subOrchestrationVersionedWithId',
|
|
292
|
+
name,
|
|
293
|
+
version: version || null,
|
|
294
|
+
instanceId,
|
|
295
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Start a detached orchestration (fire-and-forget). Yield the return value.
|
|
301
|
+
* Unlike scheduleSubOrchestration, this does NOT wait for completion.
|
|
302
|
+
* @param {string} name - Orchestration name
|
|
303
|
+
* @param {string} instanceId - Instance ID for the new orchestration
|
|
304
|
+
* @param {*} input
|
|
305
|
+
* @returns {ScheduledTask}
|
|
306
|
+
*/
|
|
307
|
+
startOrchestration(name, instanceId, input) {
|
|
308
|
+
return {
|
|
309
|
+
type: 'orchestration',
|
|
310
|
+
name,
|
|
311
|
+
instanceId,
|
|
312
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Start a versioned detached orchestration (fire-and-forget). Yield the return value.
|
|
318
|
+
*/
|
|
319
|
+
startOrchestrationVersioned(name, version, instanceId, input) {
|
|
320
|
+
return {
|
|
321
|
+
type: 'orchestrationVersioned',
|
|
322
|
+
name,
|
|
323
|
+
version: version || null,
|
|
324
|
+
instanceId,
|
|
325
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get a deterministic GUID. Yield the return value.
|
|
331
|
+
* @returns {ScheduledTask}
|
|
332
|
+
*/
|
|
333
|
+
newGuid() {
|
|
334
|
+
return { type: 'newGuid' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get the current deterministic UTC time. Yield the return value.
|
|
339
|
+
* Returns a timestamp in milliseconds.
|
|
340
|
+
* @returns {ScheduledTask}
|
|
341
|
+
*/
|
|
342
|
+
utcNow() {
|
|
343
|
+
return { type: 'utcNow' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Continue the orchestration as a new instance with new input.
|
|
348
|
+
* @param {*} input
|
|
349
|
+
* @returns {ScheduledTask}
|
|
350
|
+
*/
|
|
351
|
+
continueAsNew(input) {
|
|
352
|
+
return {
|
|
353
|
+
type: 'continueAsNew',
|
|
354
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Continue the orchestration as a new instance with new input and a specific version.
|
|
360
|
+
*/
|
|
361
|
+
continueAsNewVersioned(input, version) {
|
|
362
|
+
return {
|
|
363
|
+
type: 'continueAsNewVersioned',
|
|
364
|
+
input: JSON.stringify(input === undefined ? null : input),
|
|
365
|
+
version: version || null,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Composition helpers ───────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Join multiple tasks (wait for all). Yield the return value.
|
|
373
|
+
* @param {ScheduledTask[]} tasks - Array of scheduled task descriptors
|
|
374
|
+
* @returns {ScheduledTask}
|
|
375
|
+
*/
|
|
376
|
+
all(tasks) {
|
|
377
|
+
return { type: 'join', tasks };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Select/race multiple tasks (wait for first). Yield the return value.
|
|
382
|
+
* @param {...ScheduledTask} tasks
|
|
383
|
+
* @returns {ScheduledTask}
|
|
384
|
+
*/
|
|
385
|
+
race(...tasks) {
|
|
386
|
+
return { type: 'select', tasks };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Logging (fire-and-forget, delegates to Rust ctx.trace()) ───
|
|
390
|
+
|
|
391
|
+
traceInfo(message) {
|
|
392
|
+
orchestrationTraceLog(this.instanceId, 'info', String(message));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
traceWarn(message) {
|
|
396
|
+
orchestrationTraceLog(this.instanceId, 'warn', String(message));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
traceError(message) {
|
|
400
|
+
orchestrationTraceLog(this.instanceId, 'error', String(message));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
traceDebug(message) {
|
|
404
|
+
orchestrationTraceLog(this.instanceId, 'debug', String(message));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── Public API ──────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* SQLite provider for duroxide.
|
|
412
|
+
*/
|
|
413
|
+
class SqliteProvider {
|
|
414
|
+
/** @param {JsSqliteProvider} native */
|
|
415
|
+
constructor(native) {
|
|
416
|
+
this._native = native;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Open a SQLite database file.
|
|
421
|
+
* @param {string} path
|
|
422
|
+
* @returns {Promise<SqliteProvider>}
|
|
423
|
+
*/
|
|
424
|
+
static async open(path) {
|
|
425
|
+
const n = await JsSqliteProvider.open(path);
|
|
426
|
+
return new SqliteProvider(n);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Create an in-memory SQLite database.
|
|
431
|
+
* @returns {Promise<SqliteProvider>}
|
|
432
|
+
*/
|
|
433
|
+
static async inMemory() {
|
|
434
|
+
const n = await JsSqliteProvider.inMemory();
|
|
435
|
+
return new SqliteProvider(n);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* PostgreSQL provider for duroxide.
|
|
441
|
+
*/
|
|
442
|
+
class PostgresProvider {
|
|
443
|
+
/** @param {JsPostgresProvider} native */
|
|
444
|
+
constructor(native) {
|
|
445
|
+
this._native = native;
|
|
446
|
+
this._type = 'postgres';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Connect to a PostgreSQL database (uses "public" schema).
|
|
451
|
+
* @param {string} databaseUrl - e.g. "postgresql://user:pass@host:5432/db"
|
|
452
|
+
* @returns {Promise<PostgresProvider>}
|
|
453
|
+
*/
|
|
454
|
+
static async connect(databaseUrl) {
|
|
455
|
+
const n = await JsPostgresProvider.connect(databaseUrl);
|
|
456
|
+
return new PostgresProvider(n);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Connect to a PostgreSQL database with a custom schema.
|
|
461
|
+
* @param {string} databaseUrl
|
|
462
|
+
* @param {string} schema - Schema name (created if it doesn't exist)
|
|
463
|
+
* @returns {Promise<PostgresProvider>}
|
|
464
|
+
*/
|
|
465
|
+
static async connectWithSchema(databaseUrl, schema) {
|
|
466
|
+
const n = await JsPostgresProvider.connectWithSchema(databaseUrl, schema);
|
|
467
|
+
return new PostgresProvider(n);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Client for starting and managing orchestration instances.
|
|
473
|
+
*/
|
|
474
|
+
class Client {
|
|
475
|
+
/**
|
|
476
|
+
* @param {SqliteProvider|PostgresProvider} provider
|
|
477
|
+
*/
|
|
478
|
+
constructor(provider) {
|
|
479
|
+
if (provider._type === 'postgres') {
|
|
480
|
+
this._native = JsClient.fromPostgres(provider._native);
|
|
481
|
+
} else {
|
|
482
|
+
this._native = new JsClient(provider._native);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async startOrchestration(instanceId, orchestrationName, input) {
|
|
487
|
+
await this._native.startOrchestration(
|
|
488
|
+
instanceId,
|
|
489
|
+
orchestrationName,
|
|
490
|
+
JSON.stringify(input === undefined ? null : input)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async startOrchestrationVersioned(instanceId, orchestrationName, input, version) {
|
|
495
|
+
await this._native.startOrchestrationVersioned(
|
|
496
|
+
instanceId,
|
|
497
|
+
orchestrationName,
|
|
498
|
+
JSON.stringify(input === undefined ? null : input),
|
|
499
|
+
version
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async getStatus(instanceId) {
|
|
504
|
+
return await this._native.getStatus(instanceId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async waitForOrchestration(instanceId, timeoutMs = 30000) {
|
|
508
|
+
const result = await this._native.waitForOrchestration(instanceId, timeoutMs);
|
|
509
|
+
if (result.output) {
|
|
510
|
+
try {
|
|
511
|
+
result.output = JSON.parse(result.output);
|
|
512
|
+
} catch {}
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async cancelInstance(instanceId, reason) {
|
|
518
|
+
await this._native.cancelInstance(instanceId, reason);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async raiseEvent(instanceId, eventName, data) {
|
|
522
|
+
await this._native.raiseEvent(
|
|
523
|
+
instanceId,
|
|
524
|
+
eventName,
|
|
525
|
+
JSON.stringify(data === undefined ? null : data)
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async getSystemMetrics() {
|
|
530
|
+
return await this._native.getSystemMetrics();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async getQueueDepths() {
|
|
534
|
+
return await this._native.getQueueDepths();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ─── Management / Admin API ─────────────────────────────
|
|
538
|
+
|
|
539
|
+
async listAllInstances() {
|
|
540
|
+
return await this._native.listAllInstances();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async listInstancesByStatus(status) {
|
|
544
|
+
return await this._native.listInstancesByStatus(status);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async getInstanceInfo(instanceId) {
|
|
548
|
+
const info = await this._native.getInstanceInfo(instanceId);
|
|
549
|
+
if (info.output) {
|
|
550
|
+
try { info.output = JSON.parse(info.output); } catch {}
|
|
551
|
+
}
|
|
552
|
+
return info;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async getExecutionInfo(instanceId, executionId) {
|
|
556
|
+
const info = await this._native.getExecutionInfo(instanceId, executionId);
|
|
557
|
+
if (info.output) {
|
|
558
|
+
try { info.output = JSON.parse(info.output); } catch {}
|
|
559
|
+
}
|
|
560
|
+
return info;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async listExecutions(instanceId) {
|
|
564
|
+
return await this._native.listExecutions(instanceId);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async readExecutionHistory(instanceId, executionId) {
|
|
568
|
+
return await this._native.readExecutionHistory(instanceId, executionId);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async getInstanceTree(instanceId) {
|
|
572
|
+
return await this._native.getInstanceTree(instanceId);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async deleteInstance(instanceId, force = false) {
|
|
576
|
+
return await this._native.deleteInstance(instanceId, force);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async deleteInstanceBulk(filter = {}) {
|
|
580
|
+
return await this._native.deleteInstanceBulk(filter);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async pruneExecutions(instanceId, options = {}) {
|
|
584
|
+
return await this._native.pruneExecutions(instanceId, options);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async pruneExecutionsBulk(filter = {}, options = {}) {
|
|
588
|
+
return await this._native.pruneExecutionsBulk(filter, options);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Durable execution runtime.
|
|
594
|
+
*/
|
|
595
|
+
class Runtime {
|
|
596
|
+
/**
|
|
597
|
+
* @param {SqliteProvider|PostgresProvider} provider
|
|
598
|
+
* @param {object} [options]
|
|
599
|
+
*/
|
|
600
|
+
constructor(provider, options) {
|
|
601
|
+
if (provider._type === 'postgres') {
|
|
602
|
+
this._native = JsRuntime.fromPostgres(provider._native, options);
|
|
603
|
+
} else {
|
|
604
|
+
this._native = new JsRuntime(provider._native, options);
|
|
605
|
+
}
|
|
606
|
+
// Wire up the generator driver functions
|
|
607
|
+
this._native.setGeneratorDriver(createGenerator, nextStep, disposeGenerator);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Register an activity function.
|
|
612
|
+
* @param {string} name
|
|
613
|
+
* @param {(ctx: ActivityContext, input: any) => Promise<any>} fn
|
|
614
|
+
*/
|
|
615
|
+
registerActivity(name, fn) {
|
|
616
|
+
// Wrap the JS function to match the native callback signature
|
|
617
|
+
const wrappedFn = async (payload) => {
|
|
618
|
+
const newlineIdx = payload.indexOf('\n');
|
|
619
|
+
const ctxInfoStr = payload.substring(0, newlineIdx);
|
|
620
|
+
const inputStr = payload.substring(newlineIdx + 1);
|
|
621
|
+
const ctxInfo = JSON.parse(ctxInfoStr);
|
|
622
|
+
const ctx = new ActivityContext(ctxInfo);
|
|
623
|
+
|
|
624
|
+
let input;
|
|
625
|
+
try {
|
|
626
|
+
input = JSON.parse(inputStr);
|
|
627
|
+
} catch {
|
|
628
|
+
input = inputStr;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const result = await fn(ctx, input);
|
|
632
|
+
return JSON.stringify(result === undefined ? null : result);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
this._native.registerActivity(name, wrappedFn);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Register an orchestration generator function.
|
|
640
|
+
* @param {string} name
|
|
641
|
+
* @param {GeneratorFunction} fn - function*(ctx, input) { ... }
|
|
642
|
+
*/
|
|
643
|
+
registerOrchestration(name, fn) {
|
|
644
|
+
orchestrationFunctions.set(name, fn);
|
|
645
|
+
this._native.registerOrchestration(name);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Register a versioned orchestration generator function.
|
|
650
|
+
* @param {string} name
|
|
651
|
+
* @param {string} version
|
|
652
|
+
* @param {GeneratorFunction} fn
|
|
653
|
+
*/
|
|
654
|
+
registerOrchestrationVersioned(name, version, fn) {
|
|
655
|
+
const key = `${name}@${version}`;
|
|
656
|
+
orchestrationFunctions.set(key, fn);
|
|
657
|
+
// Don't overwrite the plain name key — it serves as the fallback
|
|
658
|
+
// for versions registered via registerOrchestration() (unversioned).
|
|
659
|
+
this._native.registerOrchestrationVersioned(name, version);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Start the runtime. Blocks until shutdown is called.
|
|
664
|
+
*/
|
|
665
|
+
async start() {
|
|
666
|
+
await this._native.start();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Shutdown the runtime gracefully.
|
|
671
|
+
* @param {number} [timeoutMs]
|
|
672
|
+
*/
|
|
673
|
+
async shutdown(timeoutMs) {
|
|
674
|
+
await this._native.shutdown(timeoutMs);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Context for activity execution.
|
|
680
|
+
*/
|
|
681
|
+
class ActivityContext {
|
|
682
|
+
constructor(ctxInfo) {
|
|
683
|
+
this.instanceId = ctxInfo.instanceId;
|
|
684
|
+
this.executionId = ctxInfo.executionId;
|
|
685
|
+
this.orchestrationName = ctxInfo.orchestrationName;
|
|
686
|
+
this.orchestrationVersion = ctxInfo.orchestrationVersion;
|
|
687
|
+
this.activityName = ctxInfo.activityName;
|
|
688
|
+
this.workerId = ctxInfo.workerId;
|
|
689
|
+
this._traceToken = ctxInfo._traceToken;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
traceInfo(message) {
|
|
693
|
+
activityTraceLog(this._traceToken, 'info', String(message));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
traceWarn(message) {
|
|
697
|
+
activityTraceLog(this._traceToken, 'warn', String(message));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
traceError(message) {
|
|
701
|
+
activityTraceLog(this._traceToken, 'error', String(message));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
traceDebug(message) {
|
|
705
|
+
activityTraceLog(this._traceToken, 'debug', String(message));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Check if this activity has been cancelled (e.g., lost a race/select).
|
|
710
|
+
* Use this for cooperative cancellation in long-running activities.
|
|
711
|
+
* @returns {boolean}
|
|
712
|
+
*/
|
|
713
|
+
isCancelled() {
|
|
714
|
+
return activityIsCancelled(this._traceToken);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ─── Exports ─────────────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
module.exports = {
|
|
721
|
+
SqliteProvider,
|
|
722
|
+
PostgresProvider,
|
|
723
|
+
Client,
|
|
724
|
+
Runtime,
|
|
725
|
+
OrchestrationContext,
|
|
726
|
+
ActivityContext,
|
|
727
|
+
};
|