@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.
@@ -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 fixed id-generation prefix (NOT the grown
266
- // id or the diverging origin), so recursive detached stays bounded at one
267
- // segment past the prefix. Mirrors the generator engine (src/context.ts).
268
- const id = idChanged ? opts.id : util.detachedId(this.prefixId, this.seqid());
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
- "resonate:branch": this.branchId,
403
- "resonate:parent": this.id,
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 its own origin (lineage break) and
431
- // an independent lifetime — its timeout is NOT clamped to the parent's.
432
- detachedCreateReq({ id, data, opts }) {
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.id,
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
- return `${this.id}.${this.seq}`;
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.
@@ -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
- oId: rootPromise.tags["resonate:origin"] ?? rootPromise.id,
121
- // The id-generation prefix, propagated unchanged across re-roots (a
122
- // detached child resets origin to its own id but carries prefix
123
- // forward), so recursive detached ids stay bounded. Falls back to
124
- // the id like oId.
125
- prId: rootPromise.tags["resonate:prefix"] ?? rootPromise.id,
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
- return this.executeUntilBlocked(res.data.task, rootPromise, res.data.preload);
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, prefix, }?: {
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, prefix = 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), idPrefix: this.idPrefix });
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
- id = `${this.idPrefix}${id}`;
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 = `${this.idPrefix}${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
- await this.schedules.create(name, cron, `${this.idPrefix}{{.id}}.{{.timestamp}}`, opts.timeout, {
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 = `${this.idPrefix}${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: () => this.promiseRegisterListener(registerListenerReq).then((res) => res.state !== "pending"),
325
- result: () => this.promiseRegisterListener(registerListenerReq).then((res) => this.subscribe(promise.id, res)),
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) {
@@ -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
- oId: rootPromise.tags["resonate:origin"] ?? this.id,
67
- // The id-generation prefix, propagated unchanged across re-roots (a
68
- // detached child resets origin to its own id but carries prefix forward),
69
- // so recursive detached ids stay bounded. Falls back to the id like oId.
70
- prId: rootPromise.tags["resonate:prefix"] ?? this.id,
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,
@@ -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, prId, bId, pId, func, clock, registry, dependencies, optsBuilder, timeout, version, retryPolicy, nonRetryableErrors, }: {
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, }: {