@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.
Files changed (136) hide show
  1. package/README.md +1 -1
  2. package/dist/{async → src/async}/context.d.ts +5 -7
  3. package/dist/{async → src/async}/context.js +39 -31
  4. package/dist/{async → src/async}/core.d.ts +0 -1
  5. package/dist/{async → src/async}/core.js +47 -9
  6. package/dist/{async → src/async}/index.d.ts +0 -1
  7. package/dist/{async → src/async}/index.js +0 -1
  8. package/dist/{async → src/async}/resonate.d.ts +1 -4
  9. package/dist/{async → src/async}/resonate.js +40 -18
  10. package/dist/{clock.d.ts → src/clock.d.ts} +0 -1
  11. package/dist/{clock.js → src/clock.js} +0 -1
  12. package/dist/{codec.d.ts → src/codec.d.ts} +0 -1
  13. package/dist/{codec.js → src/codec.js} +0 -1
  14. package/dist/{computation.d.ts → src/computation.d.ts} +0 -1
  15. package/dist/{computation.js → src/computation.js} +8 -8
  16. package/dist/{context.d.ts → src/context.d.ts} +3 -6
  17. package/dist/{context.js → src/context.js} +46 -39
  18. package/dist/{core.d.ts → src/core.d.ts} +0 -1
  19. package/dist/{core.js → src/core.js} +40 -2
  20. package/dist/{coroutine.d.ts → src/coroutine.d.ts} +0 -1
  21. package/dist/{coroutine.js → src/coroutine.js} +0 -1
  22. package/dist/{decorator.d.ts → src/decorator.d.ts} +0 -1
  23. package/dist/{decorator.js → src/decorator.js} +0 -1
  24. package/dist/{encryptor.d.ts → src/encryptor.d.ts} +0 -1
  25. package/dist/{encryptor.js → src/encryptor.js} +0 -1
  26. package/dist/{exceptions.d.ts → src/exceptions.d.ts} +1 -1
  27. package/dist/{exceptions.js → src/exceptions.js} +5 -1
  28. package/dist/src/heartbeat.d.ts +26 -0
  29. package/dist/src/heartbeat.js +54 -0
  30. package/dist/src/ids.d.ts +46 -0
  31. package/dist/src/ids.js +85 -0
  32. package/dist/{index.d.ts → src/index.d.ts} +0 -1
  33. package/dist/{index.js → src/index.js} +0 -1
  34. package/dist/{logger.d.ts → src/logger.d.ts} +0 -1
  35. package/dist/{logger.js → src/logger.js} +0 -1
  36. package/dist/{network → src/network}/http.d.ts +0 -1
  37. package/dist/{network → src/network}/http.js +0 -1
  38. package/dist/{network → src/network}/local.d.ts +0 -1
  39. package/dist/{network → src/network}/local.js +26 -3
  40. package/dist/{network → src/network}/nats.d.ts +0 -1
  41. package/dist/{network → src/network}/nats.js +5 -6
  42. package/dist/{network → src/network}/network.d.ts +0 -1
  43. package/dist/src/network/network.js +1 -0
  44. package/dist/{network → src/network}/postgres.d.ts +0 -1
  45. package/dist/{network → src/network}/postgres.js +0 -1
  46. package/dist/{network → src/network}/types.d.ts +4 -1
  47. package/dist/{network → src/network}/types.js +0 -1
  48. package/dist/{options.d.ts → src/options.d.ts} +1 -4
  49. package/dist/{options.js → src/options.js} +5 -5
  50. package/dist/{platform.d.ts → src/platform.d.ts} +0 -1
  51. package/dist/{platform.js → src/platform.js} +0 -1
  52. package/dist/{promises.d.ts → src/promises.d.ts} +0 -1
  53. package/dist/{promises.js → src/promises.js} +0 -1
  54. package/dist/{registry.d.ts → src/registry.d.ts} +0 -1
  55. package/dist/{registry.js → src/registry.js} +0 -1
  56. package/dist/{resonate.d.ts → src/resonate.d.ts} +1 -6
  57. package/dist/{resonate.js → src/resonate.js} +40 -20
  58. package/dist/{retries.d.ts → src/retries.d.ts} +0 -1
  59. package/dist/{retries.js → src/retries.js} +0 -1
  60. package/dist/{schedules.d.ts → src/schedules.d.ts} +0 -1
  61. package/dist/{schedules.js → src/schedules.js} +0 -1
  62. package/dist/{trace.d.ts → src/trace.d.ts} +0 -1
  63. package/dist/{trace.js → src/trace.js} +0 -1
  64. package/dist/{types.d.ts → src/types.d.ts} +0 -1
  65. package/dist/src/types.js +1 -0
  66. package/dist/{util.d.ts → src/util.d.ts} +1 -2
  67. package/dist/{util.js → src/util.js} +10 -7
  68. package/package.json +40 -30
  69. package/dist/async/context.d.ts.map +0 -1
  70. package/dist/async/context.js.map +0 -1
  71. package/dist/async/core.d.ts.map +0 -1
  72. package/dist/async/core.js.map +0 -1
  73. package/dist/async/index.d.ts.map +0 -1
  74. package/dist/async/index.js.map +0 -1
  75. package/dist/async/resonate.d.ts.map +0 -1
  76. package/dist/async/resonate.js.map +0 -1
  77. package/dist/clock.d.ts.map +0 -1
  78. package/dist/clock.js.map +0 -1
  79. package/dist/codec.d.ts.map +0 -1
  80. package/dist/codec.js.map +0 -1
  81. package/dist/computation.d.ts.map +0 -1
  82. package/dist/computation.js.map +0 -1
  83. package/dist/context.d.ts.map +0 -1
  84. package/dist/context.js.map +0 -1
  85. package/dist/core.d.ts.map +0 -1
  86. package/dist/core.js.map +0 -1
  87. package/dist/coroutine.d.ts.map +0 -1
  88. package/dist/coroutine.js.map +0 -1
  89. package/dist/decorator.d.ts.map +0 -1
  90. package/dist/decorator.js.map +0 -1
  91. package/dist/encryptor.d.ts.map +0 -1
  92. package/dist/encryptor.js.map +0 -1
  93. package/dist/exceptions.d.ts.map +0 -1
  94. package/dist/exceptions.js.map +0 -1
  95. package/dist/heartbeat.d.ts +0 -24
  96. package/dist/heartbeat.d.ts.map +0 -1
  97. package/dist/heartbeat.js +0 -50
  98. package/dist/heartbeat.js.map +0 -1
  99. package/dist/index.d.ts.map +0 -1
  100. package/dist/index.js.map +0 -1
  101. package/dist/logger.d.ts.map +0 -1
  102. package/dist/logger.js.map +0 -1
  103. package/dist/network/http.d.ts.map +0 -1
  104. package/dist/network/http.js.map +0 -1
  105. package/dist/network/local.d.ts.map +0 -1
  106. package/dist/network/local.js.map +0 -1
  107. package/dist/network/nats.d.ts.map +0 -1
  108. package/dist/network/nats.js.map +0 -1
  109. package/dist/network/network.d.ts.map +0 -1
  110. package/dist/network/network.js +0 -2
  111. package/dist/network/network.js.map +0 -1
  112. package/dist/network/postgres.d.ts.map +0 -1
  113. package/dist/network/postgres.js.map +0 -1
  114. package/dist/network/types.d.ts.map +0 -1
  115. package/dist/network/types.js.map +0 -1
  116. package/dist/options.d.ts.map +0 -1
  117. package/dist/options.js.map +0 -1
  118. package/dist/platform.d.ts.map +0 -1
  119. package/dist/platform.js.map +0 -1
  120. package/dist/promises.d.ts.map +0 -1
  121. package/dist/promises.js.map +0 -1
  122. package/dist/registry.d.ts.map +0 -1
  123. package/dist/registry.js.map +0 -1
  124. package/dist/resonate.d.ts.map +0 -1
  125. package/dist/resonate.js.map +0 -1
  126. package/dist/retries.d.ts.map +0 -1
  127. package/dist/retries.js.map +0 -1
  128. package/dist/schedules.d.ts.map +0 -1
  129. package/dist/schedules.js.map +0 -1
  130. package/dist/trace.d.ts.map +0 -1
  131. package/dist/trace.js.map +0 -1
  132. package/dist/types.d.ts.map +0 -1
  133. package/dist/types.js +0 -2
  134. package/dist/types.js.map +0 -1
  135. package/dist/util.d.ts.map +0 -1
  136. 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, prId = id, bId = id, pId = id, func, clock, registry, dependencies, optsBuilder, timeout, version, retryPolicy, nonRetryableErrors = [], }) {
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.prefixId, this.seqid());
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 ALWAYS breaks the origin lineage (breaksLineage: true): it is
305
- // fire-and-forget and runs as its own root, so resonate:origin must equal
306
- // its own id (origin == id == branch), starting a fresh lineage. Recursive
307
- // detached ids stay bounded NOT via origin but via resonate:prefix, which
308
- // is propagated down unchanged (set in remoteCreateReq = this.prefixId) and
309
- // is what `util.detachedId(this.prefixId, ...)` roots the id on. So each
310
- // level mints `${prefix}.d${hash}` off the same fixed prefix instead of
311
- // off its own grown id. Mirrors the Python SDK (detached origin = its id,
312
- // prefix carried forward).
313
- this.remoteCreateReq({ id, data, opts, maxTimeout: Number.MAX_SAFE_INTEGER, breaksLineage: true }), "detached");
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
- "resonate:branch": this.branchId,
370
- "resonate:parent": this.id,
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
- return `${this.id}.${this.seq}`;
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
@@ -42,4 +42,3 @@ export declare class Core {
42
42
  private fulfillTask;
43
43
  onMessage(msg: Message): Promise<Status | undefined>;
44
44
  }
45
- //# sourceMappingURL=core.d.ts.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
- return this.executeUntilBlocked(res.data.task, rootPromise, res.data.preload);
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
@@ -34,4 +34,3 @@ export declare class Coroutine<T> {
34
34
  private settle;
35
35
  private completedPromise;
36
36
  }
37
- //# sourceMappingURL=coroutine.d.ts.map
@@ -256,4 +256,3 @@ export class Coroutine {
256
256
  };
257
257
  }
258
258
  }
259
- //# sourceMappingURL=coroutine.js.map
@@ -66,4 +66,3 @@ export declare class Decorator<TRet> {
66
66
  private toLiteral;
67
67
  private safeGeneratorNext;
68
68
  }
69
- //# sourceMappingURL=decorator.d.ts.map
@@ -147,4 +147,3 @@ export class Decorator {
147
147
  }
148
148
  }
149
149
  }
150
- //# sourceMappingURL=decorator.js.map
@@ -7,4 +7,3 @@ export declare class NoopEncryptor implements Encryptor {
7
7
  encrypt(plaintext: Value): Value;
8
8
  decrypt(ciphertext: Value): Value;
9
9
  }
10
- //# sourceMappingURL=encryptor.d.ts.map
@@ -6,4 +6,3 @@ export class NoopEncryptor {
6
6
  return ciphertext;
7
7
  }
8
8
  }
9
- //# sourceMappingURL=encryptor.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;
@@ -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
@@ -30,4 +30,3 @@ export declare class ConsoleLogger implements Logger {
30
30
  private shouldLog;
31
31
  private emit;
32
32
  }
33
- //# sourceMappingURL=logger.d.ts.map
@@ -51,4 +51,3 @@ export class ConsoleLogger {
51
51
  writer(JSON.stringify(entry));
52
52
  }
53
53
  }
54
- //# sourceMappingURL=logger.js.map
@@ -63,4 +63,3 @@ export declare class PollMessageSource implements HttpAdapter {
63
63
  stop(): Promise<void>;
64
64
  onReceive(callback: (msg: Message) => void): void;
65
65
  }
66
- //# sourceMappingURL=http.d.ts.map
@@ -253,4 +253,3 @@ export class PollMessageSource {
253
253
  this.callbacks.push(callback);
254
254
  }
255
255
  }
256
- //# sourceMappingURL=http.js.map
@@ -185,4 +185,3 @@ export declare class LocalNetwork implements Network {
185
185
  recv(callback: (msg: Message) => void): void;
186
186
  private dispatchMessages;
187
187
  }
188
- //# sourceMappingURL=local.d.ts.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
- changes.push(this.setPTimeout({ id: actionData.id, timeout: actionData.timeoutAt }));
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
@@ -56,4 +56,3 @@ export declare class NatsNetwork implements Network {
56
56
  recv(callback: (msg: Message) => void): void;
57
57
  private onMsg;
58
58
  }
59
- //# sourceMappingURL=nats.d.ts.map