@resonatehq/sdk 0.11.4 → 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/dist/src/async/context.d.ts +5 -6
- package/dist/src/async/context.js +39 -30
- package/dist/src/async/core.js +47 -8
- package/dist/src/async/resonate.d.ts +1 -3
- package/dist/src/async/resonate.js +40 -17
- package/dist/src/computation.js +8 -7
- package/dist/src/context.d.ts +3 -5
- package/dist/src/context.js +46 -38
- package/dist/src/core.js +40 -1
- package/dist/src/exceptions.d.ts +1 -0
- package/dist/src/exceptions.js +5 -0
- package/dist/src/heartbeat.d.ts +15 -12
- package/dist/src/heartbeat.js +30 -25
- package/dist/src/ids.d.ts +46 -0
- package/dist/src/ids.js +85 -0
- package/dist/src/network/local.js +26 -2
- package/dist/src/network/nats.js +5 -5
- package/dist/src/network/types.d.ts +4 -0
- package/dist/src/options.d.ts +1 -3
- package/dist/src/options.js +5 -4
- package/dist/src/resonate.d.ts +1 -5
- package/dist/src/resonate.js +40 -19
- package/dist/src/util.d.ts +1 -1
- package/dist/src/util.js +10 -6
- package/package.json +1 -1
|
@@ -17,11 +17,12 @@ export type Return<F> = F extends (...args: any[]) => infer R ? Awaited<R> : nev
|
|
|
17
17
|
export interface Info {
|
|
18
18
|
readonly id: string;
|
|
19
19
|
readonly parentId: string;
|
|
20
|
+
/** The lineage origin: everything before the first `:` of every id in this
|
|
21
|
+
* workflow, set at the top (resonate.run/rpc) and carried through the
|
|
22
|
+
* resonate:origin tag unchanged forever — including into detached children,
|
|
23
|
+
* which mint `${originId}:d${hash}` off it. Also the anchor every descendant
|
|
24
|
+
* id extends, so it doubles as the id-generation root. */
|
|
20
25
|
readonly originId: string;
|
|
21
|
-
/** The id-generation prefix (resonate:prefix). Set once at the top and
|
|
22
|
-
* propagated down unchanged — through every child AND across detached
|
|
23
|
-
* re-roots — so recursive detached ids stay bounded. */
|
|
24
|
-
readonly prefixId: string;
|
|
25
26
|
readonly branchId: string;
|
|
26
27
|
readonly timeoutAt: number;
|
|
27
28
|
readonly attempt: number;
|
|
@@ -149,7 +150,6 @@ export declare class DurablePromise<T> implements Promise<T> {
|
|
|
149
150
|
interface AsyncContextConfig {
|
|
150
151
|
id: string;
|
|
151
152
|
oId?: string;
|
|
152
|
-
prId?: string;
|
|
153
153
|
bId?: string;
|
|
154
154
|
pId?: string;
|
|
155
155
|
func: string;
|
|
@@ -164,7 +164,6 @@ interface AsyncContextConfig {
|
|
|
164
164
|
export declare class AsyncContext implements Context {
|
|
165
165
|
readonly id: string;
|
|
166
166
|
readonly originId: string;
|
|
167
|
-
readonly prefixId: string;
|
|
168
167
|
readonly branchId: string;
|
|
169
168
|
readonly parentId: string;
|
|
170
169
|
readonly func: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import exceptions from "../exceptions.js";
|
|
2
|
+
import { joinId } from "../ids.js";
|
|
2
3
|
import { delay, randomUUID } from "../platform.js";
|
|
3
4
|
import { Never } from "../retries.js";
|
|
4
5
|
import * as util from "../util.js";
|
|
@@ -37,7 +38,6 @@ export class DurablePromise {
|
|
|
37
38
|
export class AsyncContext {
|
|
38
39
|
id;
|
|
39
40
|
originId;
|
|
40
|
-
prefixId;
|
|
41
41
|
branchId;
|
|
42
42
|
parentId;
|
|
43
43
|
func;
|
|
@@ -69,7 +69,6 @@ export class AsyncContext {
|
|
|
69
69
|
constructor(cfg, effects, trace) {
|
|
70
70
|
this.id = cfg.id;
|
|
71
71
|
this.originId = cfg.oId ?? cfg.id;
|
|
72
|
-
this.prefixId = cfg.prId ?? cfg.id;
|
|
73
72
|
this.branchId = cfg.bId ?? cfg.id;
|
|
74
73
|
this.parentId = cfg.pId ?? cfg.id;
|
|
75
74
|
this.func = cfg.func;
|
|
@@ -118,7 +117,6 @@ export class AsyncContext {
|
|
|
118
117
|
return new AsyncContext({
|
|
119
118
|
id: cfg.id,
|
|
120
119
|
oId: this.originId,
|
|
121
|
-
prId: this.prefixId,
|
|
122
120
|
bId: this.branchId,
|
|
123
121
|
pId: this.id,
|
|
124
122
|
func: cfg.func,
|
|
@@ -262,15 +260,16 @@ export class AsyncContext {
|
|
|
262
260
|
return this.track("", errOutcome);
|
|
263
261
|
}
|
|
264
262
|
const idChanged = opts.id !== undefined;
|
|
265
|
-
// Detached ids are minted off the
|
|
266
|
-
//
|
|
267
|
-
// segment
|
|
268
|
-
|
|
263
|
+
// Detached ids are minted off the lineage origin (`${origin}:d${hash}`,
|
|
264
|
+
// one segment past it), so recursive detached stays bounded instead of
|
|
265
|
+
// growing a segment per level. Mirrors the generator engine
|
|
266
|
+
// (src/context.ts).
|
|
267
|
+
const id = idChanged ? opts.id : util.detachedId(this.originId, this.seqid());
|
|
269
268
|
this.seq++;
|
|
270
269
|
const func = registered ? registered.name : funcOrName;
|
|
271
270
|
const version = registered ? registered.version : opts.version || 1;
|
|
272
271
|
const data = { func, args: argu, retry: opts.retryPolicy?.encode(), version };
|
|
273
|
-
const createReq = this.detachedCreateReq({ id, data, opts });
|
|
272
|
+
const createReq = this.detachedCreateReq({ id, data, opts, breaksLineage: idChanged });
|
|
274
273
|
const slot = this.claimCreateSlot();
|
|
275
274
|
// Fire-and-forget: the parent waits only for the durable CREATE to land
|
|
276
275
|
// (so the spawn survives), never for the detached workflow's result, and
|
|
@@ -399,12 +398,12 @@ export class AsyncContext {
|
|
|
399
398
|
data,
|
|
400
399
|
tags: {
|
|
401
400
|
"resonate:scope": "local",
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
// An explicit-id child (breaksLineage) starts a fresh self-anchored
|
|
402
|
+
// lineage: the server requires an id to extend its declared branch /
|
|
403
|
+
// parent / origin, which only the id itself satisfies.
|
|
404
|
+
"resonate:branch": breaksLineage ? id : this.branchId,
|
|
405
|
+
"resonate:parent": breaksLineage ? id : this.id,
|
|
404
406
|
"resonate:origin": breaksLineage ? id : this.originId,
|
|
405
|
-
// Prefix is set at the top and propagates down unchanged forever,
|
|
406
|
-
// independent of origin (which detached/explicit-id may break).
|
|
407
|
-
"resonate:prefix": this.prefixId,
|
|
408
407
|
...opts.tags,
|
|
409
408
|
},
|
|
410
409
|
});
|
|
@@ -418,18 +417,23 @@ export class AsyncContext {
|
|
|
418
417
|
"resonate:scope": "global",
|
|
419
418
|
"resonate:target": opts.target,
|
|
420
419
|
"resonate:branch": id,
|
|
421
|
-
"resonate:parent": this.id,
|
|
420
|
+
"resonate:parent": breaksLineage ? id : this.id,
|
|
422
421
|
"resonate:origin": breaksLineage ? id : this.originId,
|
|
423
|
-
// Prefix is set at the top and propagates down unchanged forever,
|
|
424
|
-
// independent of origin (which detached/explicit-id may break).
|
|
425
|
-
"resonate:prefix": this.prefixId,
|
|
426
422
|
...opts.tags,
|
|
427
423
|
},
|
|
428
424
|
});
|
|
429
425
|
}
|
|
430
|
-
// Detached: a globally-scoped promise with
|
|
431
|
-
//
|
|
432
|
-
|
|
426
|
+
// Detached: a globally-scoped promise with an independent lifetime — its
|
|
427
|
+
// timeout is NOT clamped to the parent's. The child keeps the *parent's*
|
|
428
|
+
// origin rather than re-rooting onto its own id: the server derives the
|
|
429
|
+
// origin as everything before the first `:` and requires every id in a
|
|
430
|
+
// lineage to start with `{origin}:`. Its id hangs off the origin
|
|
431
|
+
// (`${origin}:d${hash}`, bounding recursion), so the origin is also the
|
|
432
|
+
// resonate:parent it declares — a parent of the spawning context would be
|
|
433
|
+
// rejected. resonate:branch stays the child's own id: a detached child
|
|
434
|
+
// roots its own branch. An explicit opts.id still starts a fresh
|
|
435
|
+
// self-anchored lineage (breaksLineage).
|
|
436
|
+
detachedCreateReq({ id, data, opts, breaksLineage, }) {
|
|
433
437
|
return this.createReq({
|
|
434
438
|
id,
|
|
435
439
|
timeoutAt: this.clock.now() + opts.timeout,
|
|
@@ -438,11 +442,8 @@ export class AsyncContext {
|
|
|
438
442
|
"resonate:scope": "global",
|
|
439
443
|
"resonate:target": opts.target,
|
|
440
444
|
"resonate:branch": id,
|
|
441
|
-
"resonate:parent": this.
|
|
442
|
-
"resonate:origin": id,
|
|
443
|
-
// Prefix carries forward unchanged across the detached re-root (origin
|
|
444
|
-
// breaks, prefix does not) — this is what keeps recursive ids bounded.
|
|
445
|
-
"resonate:prefix": this.prefixId,
|
|
445
|
+
"resonate:parent": breaksLineage ? id : this.originId,
|
|
446
|
+
"resonate:origin": breaksLineage ? id : this.originId,
|
|
446
447
|
...opts.tags,
|
|
447
448
|
},
|
|
448
449
|
});
|
|
@@ -458,30 +459,38 @@ export class AsyncContext {
|
|
|
458
459
|
"resonate:branch": id,
|
|
459
460
|
"resonate:parent": this.id,
|
|
460
461
|
"resonate:origin": this.originId,
|
|
461
|
-
// Prefix is set at the top and propagates down unchanged forever.
|
|
462
|
-
"resonate:prefix": this.prefixId,
|
|
463
462
|
...tags,
|
|
464
463
|
},
|
|
465
464
|
});
|
|
466
465
|
}
|
|
467
466
|
sleepCreateReq({ id, time }) {
|
|
467
|
+
// The timer is the promise's own deadline: resonate:timer makes the server
|
|
468
|
+
// settle it RESOLVED (rather than timed out) when the deadline arrives,
|
|
469
|
+
// which fires the callbacks every sleeper is suspended on. The
|
|
470
|
+
// resonate:target is what makes that deadline *happen*: the server only
|
|
471
|
+
// schedules timeouts for promises carrying an address, so a target-less
|
|
472
|
+
// timer would simply never fire. The flip side is that a target also
|
|
473
|
+
// spawns a task, dispatched immediately rather than at the wake; a timer
|
|
474
|
+
// names no function to run, so the worker that receives it drops it and
|
|
475
|
+
// lets the deadline do the waking (see AsyncCore.executeUntilBlocked).
|
|
468
476
|
return this.createReq({
|
|
469
477
|
id,
|
|
470
478
|
timeoutAt: Math.min(time, this.timeoutAt),
|
|
471
479
|
data: "",
|
|
472
480
|
tags: {
|
|
473
481
|
"resonate:scope": "global",
|
|
482
|
+
"resonate:target": this.options().target,
|
|
474
483
|
"resonate:branch": id,
|
|
475
484
|
"resonate:parent": this.id,
|
|
476
485
|
"resonate:origin": this.originId,
|
|
477
|
-
// Prefix is set at the top and propagates down unchanged forever.
|
|
478
|
-
"resonate:prefix": this.prefixId,
|
|
479
486
|
"resonate:timer": "true",
|
|
480
487
|
},
|
|
481
488
|
});
|
|
482
489
|
}
|
|
483
490
|
seqid() {
|
|
484
|
-
|
|
491
|
+
// A bare root joins its first lineage segment with ':', deeper segments
|
|
492
|
+
// join with '.' -- see ids.joinId.
|
|
493
|
+
return joinId(this.id, `${this.seq}`);
|
|
485
494
|
}
|
|
486
495
|
}
|
|
487
496
|
// Map a settled durable promise record to an outcome.
|
package/dist/src/async/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import exceptions, { ResonateError } from "../exceptions.js";
|
|
2
|
+
import { originOf } from "../ids.js";
|
|
2
3
|
import { isRedirect, isSuccess } from "../network/types.js";
|
|
3
4
|
import { randomUUID } from "../platform.js";
|
|
4
5
|
import { Constant, Exponential, Linear, Never } from "../retries.js";
|
|
@@ -53,6 +54,37 @@ export class Core {
|
|
|
53
54
|
}
|
|
54
55
|
async executeUntilBlocked(task, rootPromise, preload) {
|
|
55
56
|
util.assert(task.state === "acquired", `expected task state to be 'acquired', got '${task.state}'`);
|
|
57
|
+
// A resonate:timer promise is a durable sleep. It names no function to
|
|
58
|
+
// run; it only carries a task at all because the target that gets the
|
|
59
|
+
// server to *schedule* its deadline also spawns one.
|
|
60
|
+
if (rootPromise.tags["resonate:timer"] === "true") {
|
|
61
|
+
const collector = new TraceCollector();
|
|
62
|
+
if (rootPromise.state === "pending") {
|
|
63
|
+
// Not yet due: drop the task and let the deadline settle the promise,
|
|
64
|
+
// which is what wakes the sleepers. Dropping means exactly that: no
|
|
65
|
+
// fulfill (that would end the sleep early), no suspend (a task cannot
|
|
66
|
+
// await its own promise), and no release (the server re-dispatches a
|
|
67
|
+
// released task immediately, which would spin). The lease simply
|
|
68
|
+
// lapses; a re-delivery before the wake is dropped again, and one
|
|
69
|
+
// after it takes the settled branch below.
|
|
70
|
+
this.logger.debug({ component: "async-core", taskId: task.id, timeoutAt: rootPromise.timeoutAt }, "dropping not-yet-due timer task");
|
|
71
|
+
return { kind: "suspended", awaited: [], trace: collector.getTrace() };
|
|
72
|
+
}
|
|
73
|
+
// Settled: a durable sleep whose wake has passed. It carries no
|
|
74
|
+
// function/args, so it cannot go through the run driver; fulfill the
|
|
75
|
+
// task with the promise's settlement. (The server normally fulfills a
|
|
76
|
+
// timer's task itself when the deadline settles the promise, so this
|
|
77
|
+
// only catches a delivery already in flight at that moment.)
|
|
78
|
+
const status = {
|
|
79
|
+
kind: "done",
|
|
80
|
+
id: rootPromise.id,
|
|
81
|
+
state: rootPromise.state === "resolved" ? "resolved" : "rejected",
|
|
82
|
+
value: rootPromise.value?.data,
|
|
83
|
+
trace: collector.getTrace(),
|
|
84
|
+
};
|
|
85
|
+
await this.fulfillTask(task, rootPromise, status);
|
|
86
|
+
return status;
|
|
87
|
+
}
|
|
56
88
|
const effects = util.buildEffects(this.send, this.codec, { id: task.id, version: task.version }, preload);
|
|
57
89
|
try {
|
|
58
90
|
const status = await this.executeUntilBlockedInner(rootPromise, effects);
|
|
@@ -96,7 +128,6 @@ export class Core {
|
|
|
96
128
|
if (version !== 0)
|
|
97
129
|
util.assert(version === registered.version, "versions must match");
|
|
98
130
|
util.assert(func === registered.name, "names must match");
|
|
99
|
-
this.heartbeat.start();
|
|
100
131
|
// One trace per pass (mirrors Computation.processGenerator); threaded into
|
|
101
132
|
// the root context and propagated to every child via AsyncContext.child.
|
|
102
133
|
const trace = new TraceCollector();
|
|
@@ -117,12 +148,13 @@ export class Core {
|
|
|
117
148
|
// promise) — only the decoded retry policy applies.
|
|
118
149
|
const outcome = await runWithRetry((attempt) => new AsyncContext({
|
|
119
150
|
id: rootPromise.id,
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// the id
|
|
125
|
-
|
|
151
|
+
// Take the lineage origin from the promise's resonate:origin tag,
|
|
152
|
+
// which the dispatcher set: every id in the lineage is
|
|
153
|
+
// `{origin}:{lineage}`, so this is both the ancestry root and the
|
|
154
|
+
// anchor this workflow's own child ids extend. A tag-less promise
|
|
155
|
+
// falls back to deriving it from the id the same way the server
|
|
156
|
+
// does -- which for a genuine top-level root is the id itself.
|
|
157
|
+
oId: rootPromise.tags["resonate:origin"] ?? originOf(rootPromise.id),
|
|
126
158
|
func: registered.func.name,
|
|
127
159
|
clock: this.clock,
|
|
128
160
|
registry: this.registry,
|
|
@@ -214,7 +246,14 @@ export class Core {
|
|
|
214
246
|
});
|
|
215
247
|
}
|
|
216
248
|
const rootPromise = this.codec.decodePromise(res.data.promise);
|
|
217
|
-
|
|
249
|
+
const acquiredTask = res.data.task;
|
|
250
|
+
this.heartbeat.start(acquiredTask);
|
|
251
|
+
try {
|
|
252
|
+
return await this.executeUntilBlocked(acquiredTask, rootPromise, res.data.preload);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
this.heartbeat.stop(acquiredTask);
|
|
256
|
+
}
|
|
218
257
|
}
|
|
219
258
|
}
|
|
220
259
|
// Validates the root promise's decoded `param.data` shape (copied from
|
|
@@ -27,7 +27,6 @@ export declare class Resonate {
|
|
|
27
27
|
private clock;
|
|
28
28
|
private pid;
|
|
29
29
|
private ttl;
|
|
30
|
-
private idPrefix;
|
|
31
30
|
private core;
|
|
32
31
|
private codec;
|
|
33
32
|
private network;
|
|
@@ -42,7 +41,7 @@ export declare class Resonate {
|
|
|
42
41
|
private intervalId;
|
|
43
42
|
readonly promises: Promises;
|
|
44
43
|
readonly schedules: Schedules;
|
|
45
|
-
constructor({ url, group, pid, ttl, token, timeout, verbose, logLevel, logger, encryptor, network,
|
|
44
|
+
constructor({ url, group, pid, ttl, token, timeout, verbose, logLevel, logger, encryptor, network, }?: {
|
|
46
45
|
url?: string;
|
|
47
46
|
group?: string;
|
|
48
47
|
pid?: string;
|
|
@@ -54,7 +53,6 @@ export declare class Resonate {
|
|
|
54
53
|
logger?: Logger;
|
|
55
54
|
encryptor?: Encryptor;
|
|
56
55
|
network?: Network;
|
|
57
|
-
prefix?: string;
|
|
58
56
|
});
|
|
59
57
|
/** Registers an async function (leaf or workflow) for execution. */
|
|
60
58
|
register<F extends AnyFunc>(name: string, func: F, options?: {
|
|
@@ -3,6 +3,7 @@ import { Codec } from "../codec.js";
|
|
|
3
3
|
import { NoopEncryptor } from "../encryptor.js";
|
|
4
4
|
import exceptions, { ResonateTimeoutException } from "../exceptions.js";
|
|
5
5
|
import { AsyncHeartbeat, NoopHeartbeat } from "../heartbeat.js";
|
|
6
|
+
import { validateRootId } from "../ids.js";
|
|
6
7
|
import { ConsoleLogger } from "../logger.js";
|
|
7
8
|
import { HttpNetwork, PollMessageSource } from "../network/http.js";
|
|
8
9
|
import { LocalNetwork } from "../network/local.js";
|
|
@@ -23,7 +24,6 @@ export class Resonate {
|
|
|
23
24
|
clock;
|
|
24
25
|
pid;
|
|
25
26
|
ttl;
|
|
26
|
-
idPrefix;
|
|
27
27
|
core;
|
|
28
28
|
codec;
|
|
29
29
|
network;
|
|
@@ -38,12 +38,10 @@ export class Resonate {
|
|
|
38
38
|
intervalId;
|
|
39
39
|
promises;
|
|
40
40
|
schedules;
|
|
41
|
-
constructor({ url = undefined, group = "default", pid = undefined, ttl = 1 * util.MIN, token = undefined, timeout = undefined, verbose = false, logLevel = undefined, logger = undefined, encryptor = undefined, network = undefined,
|
|
41
|
+
constructor({ url = undefined, group = "default", pid = undefined, ttl = 1 * util.MIN, token = undefined, timeout = undefined, verbose = false, logLevel = undefined, logger = undefined, encryptor = undefined, network = undefined, } = {}) {
|
|
42
42
|
this.clock = new WallClock();
|
|
43
43
|
this.ttl = ttl;
|
|
44
44
|
this.codec = new Codec(encryptor ?? new NoopEncryptor());
|
|
45
|
-
const resolvedPrefix = prefix ?? getEnv("RESONATE_PREFIX");
|
|
46
|
-
this.idPrefix = resolvedPrefix ? `${resolvedPrefix}:` : "";
|
|
47
45
|
const resolvedLogLevel = logLevel ?? (verbose ? "debug" : "warn");
|
|
48
46
|
this.logger = logger ?? new ConsoleLogger(resolvedLogLevel);
|
|
49
47
|
this.subscribeEvery = util.MIN;
|
|
@@ -83,7 +81,7 @@ export class Resonate {
|
|
|
83
81
|
}
|
|
84
82
|
this.registry = new Registry();
|
|
85
83
|
this.dependencies = new Map();
|
|
86
|
-
this.optsBuilder = new OptionsBuilder({ match: this.network.match.bind(this.network)
|
|
84
|
+
this.optsBuilder = new OptionsBuilder({ match: this.network.match.bind(this.network) });
|
|
87
85
|
this.core = new Core({
|
|
88
86
|
pid: this.pid,
|
|
89
87
|
ttl: this.ttl,
|
|
@@ -146,7 +144,11 @@ export class Resonate {
|
|
|
146
144
|
if (!registered) {
|
|
147
145
|
throw exceptions.REGISTRY_FUNCTION_NOT_REGISTERED(typeof funcOrName === "string" ? funcOrName : funcOrName.name, opts.version);
|
|
148
146
|
}
|
|
149
|
-
|
|
147
|
+
// Validated at the call site that named the workflow, rather than
|
|
148
|
+
// surfacing later as an opaque 400 from the server: the id becomes the
|
|
149
|
+
// origin of its whole lineage, so ':' is rejected outright ('.' is only
|
|
150
|
+
// read below the origin).
|
|
151
|
+
validateRootId(id);
|
|
150
152
|
util.assert(registered.version > 0, "function version must be greater than zero");
|
|
151
153
|
const { promise, task } = await this.taskCreate({
|
|
152
154
|
kind: "task.create",
|
|
@@ -166,10 +168,10 @@ export class Resonate {
|
|
|
166
168
|
},
|
|
167
169
|
tags: {
|
|
168
170
|
...opts.tags,
|
|
171
|
+
// A genuine top-level root is its own lineage origin, so
|
|
172
|
+
// origin == branch == parent == id here. Every descendant id
|
|
173
|
+
// extends it as `{id}:{lineage}`.
|
|
169
174
|
"resonate:origin": id,
|
|
170
|
-
// A genuine top-level root is its own lineage origin AND its own
|
|
171
|
-
// id-generation prefix; the prefix then propagates down unchanged.
|
|
172
|
-
"resonate:prefix": id,
|
|
173
175
|
"resonate:branch": id,
|
|
174
176
|
"resonate:parent": id,
|
|
175
177
|
"resonate:scope": "global",
|
|
@@ -192,7 +194,7 @@ export class Resonate {
|
|
|
192
194
|
if (typeof funcOrName === "function" && !registered) {
|
|
193
195
|
throw exceptions.REGISTRY_FUNCTION_NOT_REGISTERED(funcOrName.name, opts.version);
|
|
194
196
|
}
|
|
195
|
-
id
|
|
197
|
+
validateRootId(id);
|
|
196
198
|
const func = registered ? registered.name : funcOrName;
|
|
197
199
|
const version = registered ? registered.version : opts.version || 1;
|
|
198
200
|
const promise = await this.promiseCreate({
|
|
@@ -204,10 +206,10 @@ export class Resonate {
|
|
|
204
206
|
param: { data: { func, args, retry: opts.retryPolicy?.encode(), version }, headers: {} },
|
|
205
207
|
tags: {
|
|
206
208
|
...opts.tags,
|
|
209
|
+
// A genuine top-level root is its own lineage origin, so
|
|
210
|
+
// origin == branch == parent == id here. Every descendant id
|
|
211
|
+
// extends it as `{id}:{lineage}`.
|
|
207
212
|
"resonate:origin": id,
|
|
208
|
-
// A genuine top-level root is its own lineage origin AND its own
|
|
209
|
-
// id-generation prefix; the prefix then propagates down unchanged.
|
|
210
|
-
"resonate:prefix": id,
|
|
211
213
|
"resonate:branch": id,
|
|
212
214
|
"resonate:parent": id,
|
|
213
215
|
"resonate:scope": "global",
|
|
@@ -228,7 +230,15 @@ export class Resonate {
|
|
|
228
230
|
args,
|
|
229
231
|
version: registered ? registered.version : opts.version || 1,
|
|
230
232
|
});
|
|
231
|
-
|
|
233
|
+
// Each firing creates a root promise named from this id, so the schedule
|
|
234
|
+
// name is bound by the same rules as a run/rpc id. The server stamps the
|
|
235
|
+
// *whole* templated id onto the fired promise's resonate:origin tag, so
|
|
236
|
+
// the template must join with a plain '-': a ':' would hide the timestamp
|
|
237
|
+
// below the origin, collapsing every firing onto one lineage. A '.' is
|
|
238
|
+
// fine now — it is only read below the origin — but '-' keeps the
|
|
239
|
+
// timestamp clearly distinct from any dots in the schedule name.
|
|
240
|
+
validateRootId(name);
|
|
241
|
+
await this.schedules.create(name, cron, "{{.id}}-{{.timestamp}}", opts.timeout, {
|
|
232
242
|
promiseHeaders: headers,
|
|
233
243
|
promiseData: data,
|
|
234
244
|
promiseTags: { ...opts.tags, "resonate:target": opts.target },
|
|
@@ -239,7 +249,8 @@ export class Resonate {
|
|
|
239
249
|
}
|
|
240
250
|
/** Returns a handle to an existing durable promise by id. */
|
|
241
251
|
async get(id) {
|
|
242
|
-
id
|
|
252
|
+
// get is a lookup, not a create: it takes any id, including a child's
|
|
253
|
+
// (e.g. "wf:1.2"), so it deliberately does NOT validate.
|
|
243
254
|
const promise = await this.promiseGet({
|
|
244
255
|
kind: "promise.get",
|
|
245
256
|
head: { corrId: randomUUID(), version: util.VERSION },
|
|
@@ -292,6 +303,14 @@ export class Resonate {
|
|
|
292
303
|
try {
|
|
293
304
|
const res = await this.send(req);
|
|
294
305
|
if (!isSuccess(res)) {
|
|
306
|
+
// Listener registration can be retried for transient server pressure,
|
|
307
|
+
// but semantic client errors cannot become valid by waiting.
|
|
308
|
+
if (res.head.status !== 429 && res.head.status !== 500) {
|
|
309
|
+
throw exceptions.SERVER_ERROR(res.data, false, {
|
|
310
|
+
code: res.head.status,
|
|
311
|
+
message: res.data,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
295
314
|
await delay(retryDelay);
|
|
296
315
|
continue;
|
|
297
316
|
}
|
|
@@ -321,8 +340,12 @@ export class Resonate {
|
|
|
321
340
|
};
|
|
322
341
|
return {
|
|
323
342
|
id: promise.id,
|
|
324
|
-
done: () =>
|
|
325
|
-
|
|
343
|
+
done: () => promise.state === "pending"
|
|
344
|
+
? this.promiseRegisterListener(registerListenerReq).then((res) => res.state !== "pending")
|
|
345
|
+
: Promise.resolve(true),
|
|
346
|
+
result: () => promise.state === "pending"
|
|
347
|
+
? this.promiseRegisterListener(registerListenerReq).then((res) => this.subscribe(promise.id, res))
|
|
348
|
+
: this.subscribe(promise.id, promise),
|
|
326
349
|
};
|
|
327
350
|
}
|
|
328
351
|
onMessage(msg) {
|
package/dist/src/computation.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InnerContext } from "./context.js";
|
|
2
2
|
import { Coroutine } from "./coroutine.js";
|
|
3
3
|
import exceptions from "./exceptions.js";
|
|
4
|
+
import { originOf } from "./ids.js";
|
|
4
5
|
import { Exponential, Never } from "./retries.js";
|
|
5
6
|
import { TraceCollector } from "./trace.js";
|
|
6
7
|
import * as util from "./util.js";
|
|
@@ -50,8 +51,6 @@ export class Computation {
|
|
|
50
51
|
if (version !== 0)
|
|
51
52
|
util.assert(version === registered.version, "versions must match");
|
|
52
53
|
util.assert(func === registered.name, "names must match");
|
|
53
|
-
// start heartbeat
|
|
54
|
-
this.heartbeat.start();
|
|
55
54
|
const retryCtor = retry ? this.retries.get(retry.type) : undefined;
|
|
56
55
|
const retryPolicy = retryCtor
|
|
57
56
|
? new retryCtor(retry?.data)
|
|
@@ -63,11 +62,13 @@ export class Computation {
|
|
|
63
62
|
}
|
|
64
63
|
const ctxConfig = {
|
|
65
64
|
id: this.id,
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
65
|
+
// Take the lineage origin from the promise's resonate:origin tag, which
|
|
66
|
+
// the dispatcher set: every id in the lineage is `{origin}:{lineage}`,
|
|
67
|
+
// so this is both the ancestry root and the anchor this computation's
|
|
68
|
+
// own child ids extend. A tag-less promise falls back to deriving it
|
|
69
|
+
// from the id the same way the server does -- which for a genuine
|
|
70
|
+
// top-level root is the id itself.
|
|
71
|
+
oId: rootPromise.tags["resonate:origin"] ?? originOf(this.id),
|
|
71
72
|
func: registered.func.name,
|
|
72
73
|
clock: this.clock,
|
|
73
74
|
registry: this.registry,
|
package/dist/src/context.d.ts
CHANGED
|
@@ -65,7 +65,6 @@ export declare class Future<T> implements Iterable<Future<T>> {
|
|
|
65
65
|
export interface Context {
|
|
66
66
|
readonly id: string;
|
|
67
67
|
readonly originId: string;
|
|
68
|
-
readonly prefixId: string;
|
|
69
68
|
readonly parentId: string;
|
|
70
69
|
readonly branchId: string;
|
|
71
70
|
readonly info: {
|
|
@@ -132,7 +131,6 @@ export declare class InnerContext implements Context {
|
|
|
132
131
|
readonly retryPolicy: RetryPolicy;
|
|
133
132
|
readonly nonRetryableErrors: Array<new (...args: any[]) => Error>;
|
|
134
133
|
readonly originId: string;
|
|
135
|
-
readonly prefixId: string;
|
|
136
134
|
readonly branchId: string;
|
|
137
135
|
readonly parentId: string;
|
|
138
136
|
readonly clock: Clock;
|
|
@@ -156,10 +154,9 @@ export declare class InnerContext implements Context {
|
|
|
156
154
|
<F extends Func>(func: F, ...args: ParamsWithOptions<F>): RFI<Return<F>>;
|
|
157
155
|
<T>(func: string, ...args: any[]): RFI<T>;
|
|
158
156
|
};
|
|
159
|
-
constructor({ id, oId,
|
|
157
|
+
constructor({ id, oId, bId, pId, func, clock, registry, dependencies, optsBuilder, timeout, version, retryPolicy, nonRetryableErrors, }: {
|
|
160
158
|
id: string;
|
|
161
159
|
oId?: string;
|
|
162
|
-
prId?: string;
|
|
163
160
|
bId?: string;
|
|
164
161
|
pId?: string;
|
|
165
162
|
func: string;
|
|
@@ -256,11 +253,12 @@ export declare class InnerContext implements Context {
|
|
|
256
253
|
opts: Options;
|
|
257
254
|
breaksLineage: boolean;
|
|
258
255
|
}): PromiseCreateReq;
|
|
259
|
-
remoteCreateReq({ id, data, opts, breaksLineage, maxTimeout, }: {
|
|
256
|
+
remoteCreateReq({ id, data, opts, breaksLineage, parent, maxTimeout, }: {
|
|
260
257
|
id: string;
|
|
261
258
|
data: any;
|
|
262
259
|
opts: Options;
|
|
263
260
|
breaksLineage: boolean;
|
|
261
|
+
parent?: string;
|
|
264
262
|
maxTimeout?: number;
|
|
265
263
|
}): PromiseCreateReq;
|
|
266
264
|
latentCreateOpts({ id, timeout, data, tags, breaksLineage, }: {
|