@resonatehq/sdk 0.11.3 → 0.11.5
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/README.md +1 -1
- package/dist/{async → src/async}/context.d.ts +5 -7
- package/dist/{async → src/async}/context.js +39 -31
- package/dist/{async → src/async}/core.d.ts +0 -1
- package/dist/{async → src/async}/core.js +47 -9
- package/dist/{async → src/async}/index.d.ts +0 -1
- package/dist/{async → src/async}/index.js +0 -1
- package/dist/{async → src/async}/resonate.d.ts +1 -4
- package/dist/{async → src/async}/resonate.js +40 -18
- package/dist/{clock.d.ts → src/clock.d.ts} +0 -1
- package/dist/{clock.js → src/clock.js} +0 -1
- package/dist/{codec.d.ts → src/codec.d.ts} +0 -1
- package/dist/{codec.js → src/codec.js} +0 -1
- package/dist/{computation.d.ts → src/computation.d.ts} +0 -1
- package/dist/{computation.js → src/computation.js} +8 -8
- package/dist/{context.d.ts → src/context.d.ts} +3 -6
- package/dist/{context.js → src/context.js} +46 -39
- package/dist/{core.d.ts → src/core.d.ts} +0 -1
- package/dist/{core.js → src/core.js} +40 -2
- package/dist/{coroutine.d.ts → src/coroutine.d.ts} +0 -1
- package/dist/{coroutine.js → src/coroutine.js} +0 -1
- package/dist/{decorator.d.ts → src/decorator.d.ts} +0 -1
- package/dist/{decorator.js → src/decorator.js} +0 -1
- package/dist/{encryptor.d.ts → src/encryptor.d.ts} +0 -1
- package/dist/{encryptor.js → src/encryptor.js} +0 -1
- package/dist/{exceptions.d.ts → src/exceptions.d.ts} +1 -1
- package/dist/{exceptions.js → src/exceptions.js} +5 -1
- package/dist/src/heartbeat.d.ts +26 -0
- package/dist/src/heartbeat.js +54 -0
- package/dist/src/ids.d.ts +46 -0
- package/dist/src/ids.js +85 -0
- package/dist/{index.d.ts → src/index.d.ts} +0 -1
- package/dist/{index.js → src/index.js} +0 -1
- package/dist/{logger.d.ts → src/logger.d.ts} +0 -1
- package/dist/{logger.js → src/logger.js} +0 -1
- package/dist/{network → src/network}/http.d.ts +0 -1
- package/dist/{network → src/network}/http.js +0 -1
- package/dist/{network → src/network}/local.d.ts +0 -1
- package/dist/{network → src/network}/local.js +26 -3
- package/dist/{network → src/network}/nats.d.ts +0 -1
- package/dist/{network → src/network}/nats.js +5 -6
- package/dist/{network → src/network}/network.d.ts +0 -1
- package/dist/src/network/network.js +1 -0
- package/dist/{network → src/network}/postgres.d.ts +0 -1
- package/dist/{network → src/network}/postgres.js +0 -1
- package/dist/{network → src/network}/types.d.ts +4 -1
- package/dist/{network → src/network}/types.js +0 -1
- package/dist/{options.d.ts → src/options.d.ts} +1 -4
- package/dist/{options.js → src/options.js} +5 -5
- package/dist/{platform.d.ts → src/platform.d.ts} +0 -1
- package/dist/{platform.js → src/platform.js} +0 -1
- package/dist/{promises.d.ts → src/promises.d.ts} +0 -1
- package/dist/{promises.js → src/promises.js} +0 -1
- package/dist/{registry.d.ts → src/registry.d.ts} +0 -1
- package/dist/{registry.js → src/registry.js} +0 -1
- package/dist/{resonate.d.ts → src/resonate.d.ts} +1 -6
- package/dist/{resonate.js → src/resonate.js} +40 -20
- package/dist/{retries.d.ts → src/retries.d.ts} +0 -1
- package/dist/{retries.js → src/retries.js} +0 -1
- package/dist/{schedules.d.ts → src/schedules.d.ts} +0 -1
- package/dist/{schedules.js → src/schedules.js} +0 -1
- package/dist/{trace.d.ts → src/trace.d.ts} +0 -1
- package/dist/{trace.js → src/trace.js} +0 -1
- package/dist/{types.d.ts → src/types.d.ts} +0 -1
- package/dist/src/types.js +1 -0
- package/dist/{util.d.ts → src/util.d.ts} +1 -2
- package/dist/{util.js → src/util.js} +10 -7
- package/package.json +40 -30
- package/dist/async/context.d.ts.map +0 -1
- package/dist/async/context.js.map +0 -1
- package/dist/async/core.d.ts.map +0 -1
- package/dist/async/core.js.map +0 -1
- package/dist/async/index.d.ts.map +0 -1
- package/dist/async/index.js.map +0 -1
- package/dist/async/resonate.d.ts.map +0 -1
- package/dist/async/resonate.js.map +0 -1
- package/dist/clock.d.ts.map +0 -1
- package/dist/clock.js.map +0 -1
- package/dist/codec.d.ts.map +0 -1
- package/dist/codec.js.map +0 -1
- package/dist/computation.d.ts.map +0 -1
- package/dist/computation.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/coroutine.d.ts.map +0 -1
- package/dist/coroutine.js.map +0 -1
- package/dist/decorator.d.ts.map +0 -1
- package/dist/decorator.js.map +0 -1
- package/dist/encryptor.d.ts.map +0 -1
- package/dist/encryptor.js.map +0 -1
- package/dist/exceptions.d.ts.map +0 -1
- package/dist/exceptions.js.map +0 -1
- package/dist/heartbeat.d.ts +0 -24
- package/dist/heartbeat.d.ts.map +0 -1
- package/dist/heartbeat.js +0 -50
- package/dist/heartbeat.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/network/http.d.ts.map +0 -1
- package/dist/network/http.js.map +0 -1
- package/dist/network/local.d.ts.map +0 -1
- package/dist/network/local.js.map +0 -1
- package/dist/network/nats.d.ts.map +0 -1
- package/dist/network/nats.js.map +0 -1
- package/dist/network/network.d.ts.map +0 -1
- package/dist/network/network.js +0 -2
- package/dist/network/network.js.map +0 -1
- package/dist/network/postgres.d.ts.map +0 -1
- package/dist/network/postgres.js.map +0 -1
- package/dist/network/types.d.ts.map +0 -1
- package/dist/network/types.js.map +0 -1
- package/dist/options.d.ts.map +0 -1
- package/dist/options.js.map +0 -1
- package/dist/platform.d.ts.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/promises.d.ts.map +0 -1
- package/dist/promises.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/resonate.d.ts.map +0 -1
- package/dist/resonate.js.map +0 -1
- package/dist/retries.d.ts.map +0 -1
- package/dist/retries.js.map +0 -1
- package/dist/schedules.d.ts.map +0 -1
- package/dist/schedules.js.map +0 -1
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import exceptions from "./exceptions.js";
|
|
2
|
+
import { joinId } from "./ids.js";
|
|
2
3
|
import { randomUUID } from "./platform.js";
|
|
3
4
|
import { Exponential, Never } from "./retries.js";
|
|
4
5
|
import * as util from "./util.js";
|
|
@@ -144,7 +145,6 @@ export class InnerContext {
|
|
|
144
145
|
retryPolicy;
|
|
145
146
|
nonRetryableErrors;
|
|
146
147
|
originId;
|
|
147
|
-
prefixId;
|
|
148
148
|
branchId;
|
|
149
149
|
parentId;
|
|
150
150
|
clock;
|
|
@@ -156,16 +156,14 @@ export class InnerContext {
|
|
|
156
156
|
rpc = this.rfc.bind(this);
|
|
157
157
|
beginRun = this.lfi.bind(this);
|
|
158
158
|
beginRpc = this.rfi.bind(this);
|
|
159
|
-
constructor({ id, oId = id,
|
|
159
|
+
constructor({ id, oId = id, bId = id, pId = id, func, clock, registry, dependencies, optsBuilder, timeout, version, retryPolicy, nonRetryableErrors = [], }) {
|
|
160
160
|
this.id = id;
|
|
161
|
+
// The lineage origin: everything before the first `:` of every id in this
|
|
162
|
+
// workflow, set at the top (resonate.run/rpc) and carried through the
|
|
163
|
+
// resonate:origin tag unchanged forever -- including into detached
|
|
164
|
+
// children, which mint `${originId}:d${hash}` off it. Also the anchor
|
|
165
|
+
// every descendant id extends, so it doubles as the id-generation root.
|
|
161
166
|
this.originId = oId;
|
|
162
|
-
// The id-generation prefix (resonate:prefix). Set once at the top
|
|
163
|
-
// (resonate.run/rpc) and propagated down unchanged forever -- through every
|
|
164
|
-
// child AND across detached re-roots -- never tracking the (diverging)
|
|
165
|
-
// lineage origin. Bounds recursive detached ids: each level mints
|
|
166
|
-
// `${prefixId}.d${hash}` off this fixed prefix. Mirrors the Python SDK's
|
|
167
|
-
// `Context.prefix_id`.
|
|
168
|
-
this.prefixId = prId;
|
|
169
167
|
this.branchId = bId;
|
|
170
168
|
this.parentId = pId;
|
|
171
169
|
this.func = func;
|
|
@@ -185,7 +183,6 @@ export class InnerContext {
|
|
|
185
183
|
return new InnerContext({
|
|
186
184
|
id,
|
|
187
185
|
oId: this.originId,
|
|
188
|
-
prId: this.prefixId,
|
|
189
186
|
bId: this.branchId,
|
|
190
187
|
pId: this.id,
|
|
191
188
|
func,
|
|
@@ -290,7 +287,7 @@ export class InnerContext {
|
|
|
290
287
|
return new DIE(true, exceptions.REGISTRY_FUNCTION_NOT_REGISTERED(funcOrName.name, opts.version));
|
|
291
288
|
}
|
|
292
289
|
const idChanged = opts.id !== undefined;
|
|
293
|
-
const id = idChanged ? opts.id : util.detachedId(this.
|
|
290
|
+
const id = idChanged ? opts.id : util.detachedId(this.originId, this.seqid());
|
|
294
291
|
this.seq++;
|
|
295
292
|
const func = registered ? registered.name : funcOrName;
|
|
296
293
|
const version = registered ? registered.version : 1;
|
|
@@ -301,16 +298,23 @@ export class InnerContext {
|
|
|
301
298
|
version: registered ? registered.version : opts.version || 1,
|
|
302
299
|
};
|
|
303
300
|
return new RFI(id, func, version,
|
|
304
|
-
// detached
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
this.remoteCreateReq({
|
|
301
|
+
// A detached child keeps the *parent's* origin rather than re-rooting
|
|
302
|
+
// onto its own id: the server derives the origin as everything before
|
|
303
|
+
// the first `:` and requires every id in a lineage to start with
|
|
304
|
+
// `{origin}:`. Its id hangs off the origin (`${origin}:d${hash}`, one
|
|
305
|
+
// segment past it, bounding recursion), so the origin is also the
|
|
306
|
+
// resonate:parent it declares -- a parent of the spawning context would
|
|
307
|
+
// be rejected. resonate:branch stays the child's own id: a detached
|
|
308
|
+
// child roots its own branch. An explicit opts.id still starts a fresh
|
|
309
|
+
// self-anchored lineage (breaksLineage).
|
|
310
|
+
this.remoteCreateReq({
|
|
311
|
+
id,
|
|
312
|
+
data,
|
|
313
|
+
opts,
|
|
314
|
+
maxTimeout: Number.MAX_SAFE_INTEGER,
|
|
315
|
+
breaksLineage: idChanged,
|
|
316
|
+
parent: idChanged ? id : this.originId,
|
|
317
|
+
}), "detached");
|
|
314
318
|
}
|
|
315
319
|
promise({ timeout, data, tags } = {}) {
|
|
316
320
|
const id = this.seqid();
|
|
@@ -366,18 +370,18 @@ export class InnerContext {
|
|
|
366
370
|
param: { headers: {}, data },
|
|
367
371
|
tags: {
|
|
368
372
|
"resonate:scope": "local",
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
// An explicit-id child (breaksLineage) starts a fresh self-anchored
|
|
374
|
+
// lineage: the server requires an id to extend its declared branch /
|
|
375
|
+
// parent / origin, which only the id itself satisfies.
|
|
376
|
+
"resonate:branch": breaksLineage ? id : this.branchId,
|
|
377
|
+
"resonate:parent": breaksLineage ? id : this.id,
|
|
371
378
|
"resonate:origin": breaksLineage ? id : this.originId,
|
|
372
|
-
// Prefix is set at the top and propagates down unchanged forever,
|
|
373
|
-
// independent of origin (which detached/explicit-id may break).
|
|
374
|
-
"resonate:prefix": this.prefixId,
|
|
375
379
|
...opts.tags,
|
|
376
380
|
},
|
|
377
381
|
},
|
|
378
382
|
};
|
|
379
383
|
}
|
|
380
|
-
remoteCreateReq({ id, data, opts, breaksLineage, maxTimeout = this.info.timeout, }) {
|
|
384
|
+
remoteCreateReq({ id, data, opts, breaksLineage, parent, maxTimeout = this.info.timeout, }) {
|
|
381
385
|
// timeout cannot be greater than parent timeout (unless detached)
|
|
382
386
|
const timeoutAt = Math.min(this.clock.now() + opts.timeout, maxTimeout);
|
|
383
387
|
return {
|
|
@@ -390,11 +394,8 @@ export class InnerContext {
|
|
|
390
394
|
"resonate:scope": "global",
|
|
391
395
|
"resonate:target": opts.target,
|
|
392
396
|
"resonate:branch": id,
|
|
393
|
-
"resonate:parent": this.id,
|
|
397
|
+
"resonate:parent": parent ?? (breaksLineage ? id : this.id),
|
|
394
398
|
"resonate:origin": breaksLineage ? id : this.originId,
|
|
395
|
-
// Prefix is set at the top and propagates down unchanged forever,
|
|
396
|
-
// independent of origin (which detached/explicit-id may break).
|
|
397
|
-
"resonate:prefix": this.prefixId,
|
|
398
399
|
...opts.tags,
|
|
399
400
|
},
|
|
400
401
|
param: { headers: {}, data },
|
|
@@ -414,24 +415,29 @@ export class InnerContext {
|
|
|
414
415
|
tags: {
|
|
415
416
|
"resonate:scope": "global",
|
|
416
417
|
"resonate:branch": id,
|
|
417
|
-
"resonate:parent": this.id,
|
|
418
|
+
"resonate:parent": breaksLineage ? id : this.id,
|
|
418
419
|
"resonate:origin": breaksLineage ? id : this.originId,
|
|
419
|
-
// Prefix is set at the top and propagates down unchanged forever,
|
|
420
|
-
// independent of origin (which detached/explicit-id may break).
|
|
421
|
-
"resonate:prefix": this.prefixId,
|
|
422
420
|
...tags,
|
|
423
421
|
},
|
|
424
422
|
},
|
|
425
423
|
};
|
|
426
424
|
}
|
|
427
425
|
sleepCreateOpts({ id, time }) {
|
|
426
|
+
// The timer is the promise's own deadline: resonate:timer makes the server
|
|
427
|
+
// settle it RESOLVED (rather than timed out) when the deadline arrives,
|
|
428
|
+
// which fires the callbacks every sleeper is suspended on. The
|
|
429
|
+
// resonate:target is what makes that deadline *happen*: the server only
|
|
430
|
+
// schedules timeouts for promises carrying an address, so a target-less
|
|
431
|
+
// timer would simply never fire. The flip side is that a target also
|
|
432
|
+
// spawns a task, dispatched immediately rather than at the wake; a timer
|
|
433
|
+
// names no function to run, so the worker that receives it drops it and
|
|
434
|
+
// lets the deadline do the waking (see Core.executeUntilBlocked).
|
|
428
435
|
const tags = {
|
|
429
436
|
"resonate:scope": "global",
|
|
437
|
+
"resonate:target": this.options().target,
|
|
430
438
|
"resonate:branch": id,
|
|
431
439
|
"resonate:parent": this.id,
|
|
432
440
|
"resonate:origin": this.originId,
|
|
433
|
-
// Prefix is set at the top and propagates down unchanged forever.
|
|
434
|
-
"resonate:prefix": this.prefixId,
|
|
435
441
|
"resonate:timer": "true",
|
|
436
442
|
};
|
|
437
443
|
// timeout cannot be greater than parent timeout
|
|
@@ -448,7 +454,8 @@ export class InnerContext {
|
|
|
448
454
|
};
|
|
449
455
|
}
|
|
450
456
|
seqid() {
|
|
451
|
-
|
|
457
|
+
// A bare root joins its first lineage segment with ':', deeper segments
|
|
458
|
+
// join with '.' -- see ids.joinId.
|
|
459
|
+
return joinId(this.id, `${this.seq}`);
|
|
452
460
|
}
|
|
453
461
|
}
|
|
454
|
-
//# sourceMappingURL=context.js.map
|
|
@@ -3,6 +3,7 @@ import exceptions, { ResonateError } from "./exceptions.js";
|
|
|
3
3
|
import { isRedirect, isSuccess } from "./network/types.js";
|
|
4
4
|
import { randomUUID } from "./platform.js";
|
|
5
5
|
import { Constant, Exponential, Linear, Never } from "./retries.js";
|
|
6
|
+
import { TraceCollector } from "./trace.js";
|
|
6
7
|
import * as util from "./util.js";
|
|
7
8
|
export class Core {
|
|
8
9
|
pid;
|
|
@@ -37,6 +38,37 @@ export class Core {
|
|
|
37
38
|
}
|
|
38
39
|
async executeUntilBlocked(task, rootPromise, preload) {
|
|
39
40
|
util.assert(task.state === "acquired", `expected task state to be 'acquired', got '${task.state}'`);
|
|
41
|
+
// A resonate:timer promise is a durable sleep. It names no function to
|
|
42
|
+
// run; it only carries a task at all because the target that gets the
|
|
43
|
+
// server to *schedule* its deadline also spawns one.
|
|
44
|
+
if (rootPromise.tags["resonate:timer"] === "true") {
|
|
45
|
+
const collector = new TraceCollector();
|
|
46
|
+
if (rootPromise.state === "pending") {
|
|
47
|
+
// Not yet due: drop the task and let the deadline settle the promise,
|
|
48
|
+
// which is what wakes the sleepers. Dropping means exactly that: no
|
|
49
|
+
// fulfill (that would end the sleep early), no suspend (a task cannot
|
|
50
|
+
// await its own promise), and no release (the server re-dispatches a
|
|
51
|
+
// released task immediately, which would spin). The lease simply
|
|
52
|
+
// lapses; a re-delivery before the wake is dropped again, and one
|
|
53
|
+
// after it takes the settled branch below.
|
|
54
|
+
this.logger.debug({ component: "core", taskId: task.id, timeoutAt: rootPromise.timeoutAt }, "dropping not-yet-due timer task");
|
|
55
|
+
return { kind: "suspended", awaited: [], trace: collector.getTrace() };
|
|
56
|
+
}
|
|
57
|
+
// Settled: a durable sleep whose wake has passed. It carries no
|
|
58
|
+
// function/args, so it cannot go through the computation; fulfill the
|
|
59
|
+
// task with the promise's settlement. (The server normally fulfills a
|
|
60
|
+
// timer's task itself when the deadline settles the promise, so this
|
|
61
|
+
// only catches a delivery already in flight at that moment.)
|
|
62
|
+
const status = {
|
|
63
|
+
kind: "done",
|
|
64
|
+
id: rootPromise.id,
|
|
65
|
+
state: rootPromise.state === "resolved" ? "resolved" : "rejected",
|
|
66
|
+
value: rootPromise.value?.data,
|
|
67
|
+
trace: collector.getTrace(),
|
|
68
|
+
};
|
|
69
|
+
await this.fulfillTask(task, rootPromise, status);
|
|
70
|
+
return status;
|
|
71
|
+
}
|
|
40
72
|
const computation = this.createComputation(rootPromise.id, util.buildEffects(this.send, this.codec, { id: task.id, version: task.version }, preload));
|
|
41
73
|
try {
|
|
42
74
|
const status = await computation.executeUntilBlocked(rootPromise);
|
|
@@ -140,7 +172,13 @@ export class Core {
|
|
|
140
172
|
});
|
|
141
173
|
}
|
|
142
174
|
const rootPromise = this.codec.decodePromise(res.data.promise);
|
|
143
|
-
|
|
175
|
+
const acquiredTask = res.data.task;
|
|
176
|
+
this.heartbeat.start(acquiredTask);
|
|
177
|
+
try {
|
|
178
|
+
return await this.executeUntilBlocked(acquiredTask, rootPromise, res.data.preload);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.heartbeat.stop(acquiredTask);
|
|
182
|
+
}
|
|
144
183
|
}
|
|
145
184
|
}
|
|
146
|
-
//# sourceMappingURL=core.js.map
|
|
@@ -32,8 +32,8 @@ declare const _default: {
|
|
|
32
32
|
ENCODING_ARGS_UNDECODEABLE: (f: string, c: any) => ResonateError;
|
|
33
33
|
ENCODING_RETV_UNENCODEABLE: (f: string, c: any) => ResonateError;
|
|
34
34
|
ENCODING_RETV_UNDECODEABLE: (f: string, c: any) => ResonateError;
|
|
35
|
+
INVALID_ID: (id: string, reason: string) => ResonateError;
|
|
35
36
|
PANIC: (src: string, msg: string | undefined) => ResonateError;
|
|
36
37
|
SERVER_ERROR: (m: any, r?: boolean, e?: ResonateServerError) => ResonateError;
|
|
37
38
|
};
|
|
38
39
|
export default _default;
|
|
39
|
-
//# sourceMappingURL=exceptions.d.ts.map
|
|
@@ -75,6 +75,11 @@ export default {
|
|
|
75
75
|
cause: c,
|
|
76
76
|
});
|
|
77
77
|
},
|
|
78
|
+
INVALID_ID: (id, reason) => {
|
|
79
|
+
// A caller-supplied root id the server's id format cannot carry — see
|
|
80
|
+
// `ids.validateRootId`.
|
|
81
|
+
return new ResonateError("10", "InvalidId", `Invalid id '${id}': ${reason}`);
|
|
82
|
+
},
|
|
78
83
|
PANIC: (src, msg) => {
|
|
79
84
|
src = src.charAt(0).toUpperCase() + src.slice(1);
|
|
80
85
|
msg = msg ? `: ${msg}` : "";
|
|
@@ -86,4 +91,3 @@ export default {
|
|
|
86
91
|
return new ResonateError("99", "Server", m, { retriable: r, serverError: e });
|
|
87
92
|
},
|
|
88
93
|
};
|
|
89
|
-
//# sourceMappingURL=exceptions.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Logger } from "./logger.js";
|
|
2
|
+
import type { Send } from "./types.js";
|
|
3
|
+
export type HeartbeatTask = {
|
|
4
|
+
id: string;
|
|
5
|
+
version: number;
|
|
6
|
+
};
|
|
7
|
+
export interface Heartbeat {
|
|
8
|
+
start(task: HeartbeatTask): void;
|
|
9
|
+
stop(task?: HeartbeatTask): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class AsyncHeartbeat implements Heartbeat {
|
|
12
|
+
private readonly pid;
|
|
13
|
+
private readonly delay;
|
|
14
|
+
private readonly send;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private intervalId;
|
|
17
|
+
private readonly tasks;
|
|
18
|
+
constructor(pid: string, delay: number, send: Send, logger: Logger);
|
|
19
|
+
start(task: HeartbeatTask): void;
|
|
20
|
+
private heartbeat;
|
|
21
|
+
stop(task?: HeartbeatTask): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class NoopHeartbeat implements Heartbeat {
|
|
24
|
+
start(_task: HeartbeatTask): void;
|
|
25
|
+
stop(_task?: HeartbeatTask): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { randomUUID } from "./platform.js";
|
|
2
|
+
import { VERSION } from "./util.js";
|
|
3
|
+
export class AsyncHeartbeat {
|
|
4
|
+
pid;
|
|
5
|
+
delay;
|
|
6
|
+
send;
|
|
7
|
+
logger;
|
|
8
|
+
intervalId;
|
|
9
|
+
tasks = new Map();
|
|
10
|
+
constructor(pid, delay, send, logger) {
|
|
11
|
+
this.pid = pid;
|
|
12
|
+
this.delay = delay;
|
|
13
|
+
this.send = send;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
start(task) {
|
|
17
|
+
this.tasks.set(task.id, task.version);
|
|
18
|
+
if (!this.intervalId) {
|
|
19
|
+
this.intervalId = setInterval(() => this.heartbeat(), this.delay);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
heartbeat() {
|
|
23
|
+
if (this.tasks.size === 0)
|
|
24
|
+
return;
|
|
25
|
+
this.send({
|
|
26
|
+
kind: "task.heartbeat",
|
|
27
|
+
head: { corrId: randomUUID(), version: VERSION },
|
|
28
|
+
data: {
|
|
29
|
+
pid: this.pid,
|
|
30
|
+
tasks: [...this.tasks].map(([id, version]) => ({ id, version })),
|
|
31
|
+
},
|
|
32
|
+
}).catch((err) => {
|
|
33
|
+
this.logger.warn({ component: "heartbeat", pid: this.pid, error: err instanceof Error ? err.message : String(err) }, "Failed to send heartbeat");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
stop(task) {
|
|
37
|
+
if (task) {
|
|
38
|
+
if (this.tasks.get(task.id) === task.version) {
|
|
39
|
+
this.tasks.delete(task.id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.tasks.clear();
|
|
44
|
+
}
|
|
45
|
+
if (this.tasks.size === 0 && this.intervalId) {
|
|
46
|
+
clearInterval(this.intervalId);
|
|
47
|
+
this.intervalId = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class NoopHeartbeat {
|
|
52
|
+
start(_task) { }
|
|
53
|
+
stop(_task) { }
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Separates the origin from the lineage below it. A bare root joins its
|
|
2
|
+
* first lineage segment with this. */
|
|
3
|
+
export declare const ORIGIN_SEP = ":";
|
|
4
|
+
/** Separates lineage segments below the origin. */
|
|
5
|
+
export declare const LINEAGE_SEP = ".";
|
|
6
|
+
/**
|
|
7
|
+
* Append a lineage `segment` to `ancestor`.
|
|
8
|
+
*
|
|
9
|
+
* A bare root joins its *first* segment with `:`; an ancestor that already
|
|
10
|
+
* carries lineage joins deeper segments with `.`, keeping the whole subtree
|
|
11
|
+
* under one origin:
|
|
12
|
+
*
|
|
13
|
+
* joinId("root", "1") -> "root:1"
|
|
14
|
+
* joinId("root:1", "2") -> "root:1.2"
|
|
15
|
+
* joinId("root:1.2", "3") -> "root:1.2.3"
|
|
16
|
+
*
|
|
17
|
+
* This is exactly the separator rule the server's `resonate:branch` /
|
|
18
|
+
* `resonate:parent` validation applies.
|
|
19
|
+
*/
|
|
20
|
+
export declare function joinId(ancestor: string, segment: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The lineage origin of `id`: everything before the first `:`.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors the server's `origin()`. An id with no lineage below it (a root) is
|
|
25
|
+
* its own origin.
|
|
26
|
+
*/
|
|
27
|
+
export declare function originOf(id: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Validate a caller-supplied root id (`run` / `rpc` / `schedule` / an explicit
|
|
30
|
+
* `options({ id })`), returning it.
|
|
31
|
+
*
|
|
32
|
+
* Only `:` is **reserved**: a root becomes the origin of its whole lineage,
|
|
33
|
+
* and the origin is everything before an id's *first* `:`, so an origin
|
|
34
|
+
* holding one could never be split back out of any id. The server rejects it
|
|
35
|
+
* outright (`colon_in_origin`).
|
|
36
|
+
*
|
|
37
|
+
* `.` is allowed. It separates lineage segments *below* the origin, which is
|
|
38
|
+
* only ever read after the origin has been split off, so a dotted root id
|
|
39
|
+
* (`my.app.workflow`) is unambiguous:
|
|
40
|
+
*
|
|
41
|
+
* my.app.workflow -> my.app.workflow:1 -> my.app.workflow:1.1
|
|
42
|
+
*
|
|
43
|
+
* @throws {ResonateError} here, at the call site that named the workflow,
|
|
44
|
+
* rather than surfacing later as an opaque 400 from a background create.
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateRootId(id: string): string;
|
package/dist/src/ids.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The promise id format, in one place.
|
|
2
|
+
//
|
|
3
|
+
// The server treats a promise id as `<origin>:<lineage>`: the **origin** is
|
|
4
|
+
// everything before the first `:`, and the lineage segments below it are
|
|
5
|
+
// `.`-separated:
|
|
6
|
+
//
|
|
7
|
+
// root -> root:1 -> root:1.1 -> root:1.1.1
|
|
8
|
+
//
|
|
9
|
+
// The origin is load-bearing. `promise.register_callback` and `task.suspend`
|
|
10
|
+
// require an awaiter and its awaited promise to share one, it selects the
|
|
11
|
+
// origin-state partition a request is routed to, and `promise.create` rejects
|
|
12
|
+
// an id that does not extend the `resonate:origin` / `resonate:branch` /
|
|
13
|
+
// `resonate:parent` it declares. So the SDK mints ids with `joinId` and reads
|
|
14
|
+
// them back with `originOf`, both of which mirror the server's own rules.
|
|
15
|
+
//
|
|
16
|
+
// A root id is supplied by the caller and becomes the origin of its whole
|
|
17
|
+
// lineage, so `validateRootId` keeps `:` out of it, exactly as the server does
|
|
18
|
+
// for the origin tag itself. `.` is *not* reserved there: it only separates
|
|
19
|
+
// segments below the origin, and the origin is recovered by splitting on the
|
|
20
|
+
// first `:`, so a dotted root (`my.app.workflow`) survives the round trip
|
|
21
|
+
// intact.
|
|
22
|
+
import exceptions from "./exceptions.js";
|
|
23
|
+
/** Separates the origin from the lineage below it. A bare root joins its
|
|
24
|
+
* first lineage segment with this. */
|
|
25
|
+
export const ORIGIN_SEP = ":";
|
|
26
|
+
/** Separates lineage segments below the origin. */
|
|
27
|
+
export const LINEAGE_SEP = ".";
|
|
28
|
+
/**
|
|
29
|
+
* Append a lineage `segment` to `ancestor`.
|
|
30
|
+
*
|
|
31
|
+
* A bare root joins its *first* segment with `:`; an ancestor that already
|
|
32
|
+
* carries lineage joins deeper segments with `.`, keeping the whole subtree
|
|
33
|
+
* under one origin:
|
|
34
|
+
*
|
|
35
|
+
* joinId("root", "1") -> "root:1"
|
|
36
|
+
* joinId("root:1", "2") -> "root:1.2"
|
|
37
|
+
* joinId("root:1.2", "3") -> "root:1.2.3"
|
|
38
|
+
*
|
|
39
|
+
* This is exactly the separator rule the server's `resonate:branch` /
|
|
40
|
+
* `resonate:parent` validation applies.
|
|
41
|
+
*/
|
|
42
|
+
export function joinId(ancestor, segment) {
|
|
43
|
+
const sep = ancestor.includes(ORIGIN_SEP) ? LINEAGE_SEP : ORIGIN_SEP;
|
|
44
|
+
return `${ancestor}${sep}${segment}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The lineage origin of `id`: everything before the first `:`.
|
|
48
|
+
*
|
|
49
|
+
* Mirrors the server's `origin()`. An id with no lineage below it (a root) is
|
|
50
|
+
* its own origin.
|
|
51
|
+
*/
|
|
52
|
+
export function originOf(id) {
|
|
53
|
+
const sep = id.indexOf(ORIGIN_SEP);
|
|
54
|
+
return sep === -1 ? id : id.slice(0, sep);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a caller-supplied root id (`run` / `rpc` / `schedule` / an explicit
|
|
58
|
+
* `options({ id })`), returning it.
|
|
59
|
+
*
|
|
60
|
+
* Only `:` is **reserved**: a root becomes the origin of its whole lineage,
|
|
61
|
+
* and the origin is everything before an id's *first* `:`, so an origin
|
|
62
|
+
* holding one could never be split back out of any id. The server rejects it
|
|
63
|
+
* outright (`colon_in_origin`).
|
|
64
|
+
*
|
|
65
|
+
* `.` is allowed. It separates lineage segments *below* the origin, which is
|
|
66
|
+
* only ever read after the origin has been split off, so a dotted root id
|
|
67
|
+
* (`my.app.workflow`) is unambiguous:
|
|
68
|
+
*
|
|
69
|
+
* my.app.workflow -> my.app.workflow:1 -> my.app.workflow:1.1
|
|
70
|
+
*
|
|
71
|
+
* @throws {ResonateError} here, at the call site that named the workflow,
|
|
72
|
+
* rather than surfacing later as an opaque 400 from a background create.
|
|
73
|
+
*/
|
|
74
|
+
export function validateRootId(id) {
|
|
75
|
+
if (!id) {
|
|
76
|
+
throw exceptions.INVALID_ID(id, "id must not be empty");
|
|
77
|
+
}
|
|
78
|
+
if (id.includes("\0")) {
|
|
79
|
+
throw exceptions.INVALID_ID(id, "id must not contain null bytes");
|
|
80
|
+
}
|
|
81
|
+
if (id.includes(ORIGIN_SEP)) {
|
|
82
|
+
throw exceptions.INVALID_ID(id, `id must not contain '${ORIGIN_SEP}': it separates the origin from the lineage in the ids the SDK mints below this one, so an id holding one could never be split back out`);
|
|
83
|
+
}
|
|
84
|
+
return id;
|
|
85
|
+
}
|
|
@@ -17,4 +17,3 @@ export { Resonate, type ResonateFunc, type ResonateHandle } from "./resonate.js"
|
|
|
17
17
|
export { Constant, Exponential, Linear, Never, type RetryPolicy } from "./retries.js";
|
|
18
18
|
export { awaitThenResumeOrSuspend, blockIsSole, dedupIsSole, type Event, exclusiveLifecycle, isWellFormed, rootSpawn, rpcHasCallee, runHasCallee, spawnIsFirst, type Trace, TraceCollector, terminalIsLast, uniqueSpawn, uniqueTerminal, } from "./trace.js";
|
|
19
19
|
export type { Effects, Func } from "./types.js";
|
|
20
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,4 +13,3 @@ export { Registry } from "./registry.js";
|
|
|
13
13
|
export { Resonate } from "./resonate.js";
|
|
14
14
|
export { Constant, Exponential, Linear, Never } from "./retries.js";
|
|
15
15
|
export { awaitThenResumeOrSuspend, blockIsSole, dedupIsSole, exclusiveLifecycle, isWellFormed, rootSpawn, rpcHasCallee, runHasCallee, spawnIsFirst, TraceCollector, terminalIsLast, uniqueSpawn, uniqueTerminal, } from "./trace.js";
|
|
16
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -6,6 +6,13 @@ import { isResponse } from "./types.js";
|
|
|
6
6
|
// CONSTANTS
|
|
7
7
|
// =============================================================================
|
|
8
8
|
const PENDING_RETRY_TTL = 30000;
|
|
9
|
+
function isExternalPromise(promise) {
|
|
10
|
+
const { tags } = promise;
|
|
11
|
+
return (tags["resonate:scope"] === "global" ||
|
|
12
|
+
tags["resonate:external"] === "true" ||
|
|
13
|
+
tags["resonate:target"] !== undefined ||
|
|
14
|
+
tags["resonate:timer"] === "true");
|
|
15
|
+
}
|
|
9
16
|
export class Server {
|
|
10
17
|
promises = new Map();
|
|
11
18
|
tasks = new Map();
|
|
@@ -262,9 +269,15 @@ export class Server {
|
|
|
262
269
|
listeners: new Set(),
|
|
263
270
|
};
|
|
264
271
|
changes.push(this.setPromise(promise));
|
|
265
|
-
changes.push(this.setPTimeout({ id: req.data.id, timeout: req.data.timeoutAt }));
|
|
266
272
|
const address = req.data.tags["resonate:target"];
|
|
267
273
|
if (address) {
|
|
274
|
+
// Only a promise carrying an address is expired by the tick loop. A
|
|
275
|
+
// target-less promise (a bare ctx.promise) is never timed out by the
|
|
276
|
+
// scheduler, so a durable timer has to carry a target to fire at all —
|
|
277
|
+
// see Context.sleep. Scheduling one here regardless would make this
|
|
278
|
+
// simulation *more* permissive than the server, which is invisible to
|
|
279
|
+
// every test that only asserts success.
|
|
280
|
+
changes.push(this.setPTimeout({ id: req.data.id, timeout: req.data.timeoutAt }));
|
|
268
281
|
const delay = Number(req.data.tags["resonate:delay"]);
|
|
269
282
|
const deferred = Number.isFinite(delay) && now < delay;
|
|
270
283
|
const task = {
|
|
@@ -354,6 +367,12 @@ export class Server {
|
|
|
354
367
|
return { response: this.response("promise.register_listener", 404, "Promise not found"), changes: [] };
|
|
355
368
|
}
|
|
356
369
|
const changes = [];
|
|
370
|
+
if (!isExternalPromise(promise)) {
|
|
371
|
+
return {
|
|
372
|
+
response: this.response("promise.register_listener", 422, "Awaited promise is not awaitable"),
|
|
373
|
+
changes: [],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
357
376
|
// Project for the logically-pending check and for the response — the
|
|
358
377
|
// listener is registered only when the promise is logically pending,
|
|
359
378
|
// and the response always reflects the projected awaited promise.
|
|
@@ -487,7 +506,12 @@ export class Server {
|
|
|
487
506
|
listeners: new Set(),
|
|
488
507
|
};
|
|
489
508
|
changes.push(this.setPromise(promise));
|
|
490
|
-
|
|
509
|
+
// Same scheduler invariant as promise.create: only a promise carrying a
|
|
510
|
+
// target is expired by the tick loop. A task.create root always carries
|
|
511
|
+
// one in practice, but the guard keeps the invariant explicit.
|
|
512
|
+
if (actionData.tags["resonate:target"]) {
|
|
513
|
+
changes.push(this.setPTimeout({ id: actionData.id, timeout: actionData.timeoutAt }));
|
|
514
|
+
}
|
|
491
515
|
// Step 2: Create task in acquired state
|
|
492
516
|
const task = {
|
|
493
517
|
id: actionData.id,
|
|
@@ -1355,4 +1379,3 @@ export class LocalNetwork {
|
|
|
1355
1379
|
}
|
|
1356
1380
|
}
|
|
1357
1381
|
}
|
|
1358
|
-
//# sourceMappingURL=local.js.map
|