@dudousxd/nestjs-durable-core 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +362 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +492 -127
- package/dist/index.d.ts +492 -127
- package/dist/index.js +355 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,34 @@ declare class InMemoryAdmissionBackend implements AdmissionBackend {
|
|
|
168
168
|
* wire contract — the adonis + python SDKs build the byte-identical shape (design §7.8), and the
|
|
169
169
|
* golden fixtures in `<repo>/fixtures/wire/` pin the exact bytes all three SDKs must agree on.
|
|
170
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* One workflow a worker ANNOUNCES it can execute — the richer form of a `workflows` name.
|
|
173
|
+
*
|
|
174
|
+
* The rule is "announce only what you can run": a process publishes a registration for a workflow
|
|
175
|
+
* whose body it holds and whose queue it consumes. It never publishes one for a workflow it merely
|
|
176
|
+
* knows how to *route to* (an engine's `registerRemote`), because that claim says nothing about a
|
|
177
|
+
* live executor existing — which is precisely the ambiguity an aggregate registry has to remove.
|
|
178
|
+
*
|
|
179
|
+
* Every field but `name` is optional because the SDKs hold different amounts of this. A worker that
|
|
180
|
+
* knows only names (the Python `@worker.workflow("x")` form, or any pre-registrations SDK) publishes
|
|
181
|
+
* bare `workflows` names instead and is still a first-class announcer — see {@link WorkerDescriptor.registrations}.
|
|
182
|
+
* A reader must therefore treat an absent field as "not stated", never as a value: the aggregate in
|
|
183
|
+
* `./announced` never invents a version, a group or an origin that nobody announced.
|
|
184
|
+
*/
|
|
185
|
+
interface WorkflowRegistration {
|
|
186
|
+
/** Registered workflow name — the `workflow` a {@link WorkflowTask} carries. */
|
|
187
|
+
name: string;
|
|
188
|
+
/** Registered version, when the announcer knows one. Absent = unversioned announcement. */
|
|
189
|
+
version?: string;
|
|
190
|
+
/** The routing token this workflow's turns are dispatched to (the queue this worker consumes for
|
|
191
|
+
* it). Absent = the announcer did not say; a reader must NOT assume the by-convention token. */
|
|
192
|
+
group?: string;
|
|
193
|
+
/** Capabilities a worker must advertise to run it (design §7.5), as the announcer declares them. */
|
|
194
|
+
requires?: string[];
|
|
195
|
+
/** The package that DECLARED the workflow (see `WorkflowRun.origin`). Absent = unknown; nothing
|
|
196
|
+
* ever substitutes a default, because a wrong origin is worse than no origin. */
|
|
197
|
+
origin?: string;
|
|
198
|
+
}
|
|
171
199
|
/**
|
|
172
200
|
* A worker's (or control-plane's) advertised identity, wire-protocol support, feature capabilities
|
|
173
201
|
* and registered handlers. Published on startup + on change (design §7.2) and consumed by
|
|
@@ -200,6 +228,19 @@ interface WorkerDescriptor {
|
|
|
200
228
|
workflows: string[];
|
|
201
229
|
/** Registered step handler names → routing targets. Order-insensitive (a set). */
|
|
202
230
|
steps: string[];
|
|
231
|
+
/**
|
|
232
|
+
* The richer, per-workflow ANNOUNCEMENT of what this instance can execute (name + version + group
|
|
233
|
+
* + requires + origin). OPTIONAL and purely additive: an SDK that has only names keeps publishing
|
|
234
|
+
* {@link workflows} and is aggregated all the same, just unversioned. An SDK that publishes this
|
|
235
|
+
* SHOULD also list the same names in {@link workflows}, so a reader that predates this field still
|
|
236
|
+
* sees the workflow at all.
|
|
237
|
+
*
|
|
238
|
+
* It rides the descriptor rather than a registry of its own so it inherits the whole liveness
|
|
239
|
+
* story for free: the descriptor key is written with the heartbeat's TTL and refreshed by the same
|
|
240
|
+
* beat, so an announcement from a worker that has since died EXPIRES instead of offering a choice
|
|
241
|
+
* that fails at run time.
|
|
242
|
+
*/
|
|
243
|
+
registrations?: WorkflowRegistration[];
|
|
203
244
|
/** Optional routing partition (queue/group sharding). */
|
|
204
245
|
partition?: string;
|
|
205
246
|
/** Optional tenant namespace this instance serves. */
|
|
@@ -252,7 +293,13 @@ declare function isLegacyDescriptor(raw: RawWorkerDescriptor | WorkerDescriptor)
|
|
|
252
293
|
* - absent `workflows`/`steps` → `[]`, absent `sdk` → an `unknown` marker, absent `startedAt` → `0`.
|
|
253
294
|
*
|
|
254
295
|
* The undefined-vs-`[]` distinction is deliberate: an absent field means "legacy, doesn't advertise
|
|
255
|
-
* this axis"; an explicit empty array means "modern SDK that genuinely advertises nothing".
|
|
296
|
+
* this axis"; an explicit empty array means "modern SDK that genuinely advertises nothing". The same
|
|
297
|
+
* reasoning keeps `registrations` ABSENT when it was absent (rather than defaulting to `[]`): "this
|
|
298
|
+
* SDK does not announce registrations" and "this SDK announces none" are different facts, and only
|
|
299
|
+
* the second one licenses a reader to conclude the worker serves nothing. (No SDK publishes `[]`
|
|
300
|
+
* today — {@link describeWorker} and the Python builder both OMIT the field when there is nothing to
|
|
301
|
+
* announce, so a worker with no workflows keeps its pre-announcement bytes and ETag — but a reader
|
|
302
|
+
* must not collapse the two cases on that basis.)
|
|
256
303
|
*/
|
|
257
304
|
declare function normalizeDescriptor(raw: RawWorkerDescriptor | WorkerDescriptor): WorkerDescriptor;
|
|
258
305
|
/**
|
|
@@ -292,6 +339,35 @@ declare function heartbeatStatus(descriptor: RawWorkerDescriptor | WorkerDescrip
|
|
|
292
339
|
ts?: number;
|
|
293
340
|
status?: WorkerLifecycle;
|
|
294
341
|
}): HeartbeatStatus;
|
|
342
|
+
/**
|
|
343
|
+
* Build a worker's own {@link WorkerDescriptor} in the exact §7.1 shape every SDK publishes — the
|
|
344
|
+
* single builder both TS advertisers use (the BullMQ transport's in-app worker and the standalone
|
|
345
|
+
* `runRedisWorker`), so two processes in the same fleet cannot drift into two dialects of the same
|
|
346
|
+
* wire.
|
|
347
|
+
*
|
|
348
|
+
* Set-valued fields are sorted here (not just in the hash projection) so the published BYTES are
|
|
349
|
+
* stable across registration order too — a re-registration in a different order must not look like a
|
|
350
|
+
* change to a reader diffing the raw value. `partition` and `namespace` are omitted when empty, and
|
|
351
|
+
* `namespace` additionally when it is `default`, keeping a single-tenant deployment byte-identical
|
|
352
|
+
* to the un-namespaced scheme. `capabilities` is the canonical v1 baseline this protocol guarantees.
|
|
353
|
+
*/
|
|
354
|
+
declare function describeWorker(opts: {
|
|
355
|
+
instanceId: string;
|
|
356
|
+
runtime: 'node' | 'python';
|
|
357
|
+
sdk: {
|
|
358
|
+
name: string;
|
|
359
|
+
version: string;
|
|
360
|
+
};
|
|
361
|
+
steps: string[];
|
|
362
|
+
workflows?: string[] | undefined;
|
|
363
|
+
/** What this instance announces it can EXECUTE — see {@link WorkflowRegistration}. Omitted from
|
|
364
|
+
* the descriptor entirely when absent or empty, so a worker with nothing to announce keeps the
|
|
365
|
+
* pre-registrations bytes (and therefore the pre-registrations hash). */
|
|
366
|
+
registrations?: readonly WorkflowRegistration[] | undefined;
|
|
367
|
+
startedAt: number;
|
|
368
|
+
partition?: string | undefined;
|
|
369
|
+
namespace?: string | undefined;
|
|
370
|
+
}): WorkerDescriptor;
|
|
295
371
|
|
|
296
372
|
/**
|
|
297
373
|
* Capability- & protocol-aware dispatch planning (design §7.5/§7.6). Sits BETWEEN the pure handshake
|
|
@@ -1463,6 +1539,19 @@ interface Transport {
|
|
|
1463
1539
|
* a run can dispatch to `group` or must park `blocked`.
|
|
1464
1540
|
*/
|
|
1465
1541
|
readWorkerDescriptors?(group: string): Promise<WorkerDescriptor[]>;
|
|
1542
|
+
/**
|
|
1543
|
+
* Every live {@link WorkerDescriptor} in the deployment, across all groups — the read behind
|
|
1544
|
+
* {@link WorkflowEngine.announcedWorkflows}. Optional, same as {@link readWorkerDescriptors}: a
|
|
1545
|
+
* transport that cannot introspect the advertisement keyspace returns nothing and the registry is
|
|
1546
|
+
* simply empty.
|
|
1547
|
+
*
|
|
1548
|
+
* Deliberately NOT `listWorkerGroups()` + a read per group: a worker publishes the SAME descriptor
|
|
1549
|
+
* under every token it consumes, so per-group reads cost one round trip per group to re-fetch bytes
|
|
1550
|
+
* the caller already has. One pass over the advertisement keyspace, de-duplicated by `instanceId`,
|
|
1551
|
+
* answers with one scan and no per-group fan-out — and it also sees an instance whose groups the
|
|
1552
|
+
* caller could not have enumerated.
|
|
1553
|
+
*/
|
|
1554
|
+
readAllWorkerDescriptors?(): Promise<WorkerDescriptor[]>;
|
|
1466
1555
|
/**
|
|
1467
1556
|
* DB-less tenant worker → control plane: publish a {@link StartRunMessage} requesting a new run.
|
|
1468
1557
|
* Optional — only transports that carry the hosted-control-plane protocol (P4) implement this.
|
|
@@ -1615,6 +1704,19 @@ type BackoffStrategy = 'fixed' | 'exp';
|
|
|
1615
1704
|
interface ChildCallOptions {
|
|
1616
1705
|
/** Deterministic child run id; defaults to one derived from the parent run id + call position. */
|
|
1617
1706
|
childId?: string | undefined;
|
|
1707
|
+
/**
|
|
1708
|
+
* Pin the child to this EXACT registered version instead of the newest one — the same contract as
|
|
1709
|
+
* `StartOptions.version`, reached from inside a workflow body: an unregistered version fails the
|
|
1710
|
+
* child's start rather than quietly running a different body, and a version can only be pinned on a
|
|
1711
|
+
* workflow that is really registered (not on the synthesized remote/convention routes). A `ctx.child`
|
|
1712
|
+
* whose pinned start fails surfaces it as a failed child on the parent's waiter; a fire-and-forget
|
|
1713
|
+
* `ctx.startChild` buffers it until the parent joins the id.
|
|
1714
|
+
*
|
|
1715
|
+
* Replay-safe as long as you pass a CONSTANT (`{ version: '2' }`), which is the point — it names the
|
|
1716
|
+
* body you meant. Computing it from a live registry read at call time would make the body
|
|
1717
|
+
* non-deterministic, exactly like reading the clock.
|
|
1718
|
+
*/
|
|
1719
|
+
version?: string | undefined;
|
|
1618
1720
|
/**
|
|
1619
1721
|
* Dispatch priority for a REMOTE child workflow — stamped on the child run and carried onto every
|
|
1620
1722
|
* {@link WorkflowTask} dispatched to advance it, so an urgent child can jump ahead of enqueued
|
|
@@ -2288,6 +2390,253 @@ declare class EventAccumulators {
|
|
|
2288
2390
|
private batch;
|
|
2289
2391
|
}
|
|
2290
2392
|
|
|
2393
|
+
/**
|
|
2394
|
+
* The announced workflow registry (design §7.9) — the aggregate answer to "what workflows exist in
|
|
2395
|
+
* this deployment?", built ONLY from what live workers say about themselves.
|
|
2396
|
+
*
|
|
2397
|
+
* ## Why this is announced, not inferred
|
|
2398
|
+
*
|
|
2399
|
+
* `WorkflowEngine.workflowBody(name, version)` answers for the process that asks, and a missing body
|
|
2400
|
+
* is ambiguous by design: it means "not registered here", but equally "registered via
|
|
2401
|
+
* `registerRemote` against another SDK" or "a group this pod resolves by convention against a live
|
|
2402
|
+
* worker". A picker built on that inference would offer different options depending on which replica
|
|
2403
|
+
* served the request. So the registry is not derived from any engine's registry: a worker publishes
|
|
2404
|
+
* what it can execute ({@link WorkerDescriptor.registrations}, or bare `workflows` names), and this
|
|
2405
|
+
* module folds those statements together. Every entry is therefore a fact somebody asserted, and the
|
|
2406
|
+
* aggregate is the same on every pod because every pod reads the same published statements.
|
|
2407
|
+
*
|
|
2408
|
+
* The announcer is always the process that CONSUMES the queue, never the engine that dispatches to
|
|
2409
|
+
* it — including when the two share a process (the co-located in-app worker announces; the engine
|
|
2410
|
+
* beside it does not). One consequence is worth stating plainly rather than leaving to be
|
|
2411
|
+
* rediscovered: a pure operator (`store` with no `connection`) runs its bodies inline, consumes no
|
|
2412
|
+
* queue, and therefore announces NOTHING. Its workflows are real and startable through its own
|
|
2413
|
+
* `start()`, but they are not in this registry, because nothing about them is externally addressable
|
|
2414
|
+
* for a picker to point at.
|
|
2415
|
+
*
|
|
2416
|
+
* ## Liveness
|
|
2417
|
+
*
|
|
2418
|
+
* There is no expiry logic here because there is no state here. An announcement lives on the
|
|
2419
|
+
* descriptor key, which is written with the worker-heartbeat TTL and refreshed by the same beat; a
|
|
2420
|
+
* worker that dies stops refreshing and its key expires, taking its announcements with it. The
|
|
2421
|
+
* resolution of that liveness is therefore the TTL (35s against a 10s beat), so an entry can name a
|
|
2422
|
+
* worker that died within the last beat window — the same staleness the capability router already
|
|
2423
|
+
* accepts when it reads descriptors to decide whether to dispatch. A caller that needs a stronger
|
|
2424
|
+
* guarantee should not seek it here: dispatch to a dead worker parks the run `blocked`/`pending` on
|
|
2425
|
+
* its queue and the existing worker-health surface reports it. What this design DOES rule out is the
|
|
2426
|
+
* unbounded failure — an announcement that outlives its worker forever.
|
|
2427
|
+
*
|
|
2428
|
+
* ## Disagreement
|
|
2429
|
+
*
|
|
2430
|
+
* Two live workers can announce the same `name@version` with different groups, origins or capability
|
|
2431
|
+
* demands. The aggregate keeps EVERY distinct claim and reports the axes they differ on
|
|
2432
|
+
* ({@link AnnouncedWorkflow.disagreements}); it never merges them into one value and never picks a
|
|
2433
|
+
* winner, because either choice would make a caller act on a claim nobody made. Silence is not a
|
|
2434
|
+
* claim: a worker that announced no origin does not disagree with one that did.
|
|
2435
|
+
*
|
|
2436
|
+
* ## Steps are deliberately out of scope
|
|
2437
|
+
*
|
|
2438
|
+
* There is no equivalent registry for steps, and this is not an omission to be fixed later. A step is
|
|
2439
|
+
* not addressable from outside a run: it is identified by its `(runId, seq)` position in one
|
|
2440
|
+
* workflow's history, `ctx.step` is only callable from inside a replaying body, and no engine entry
|
|
2441
|
+
* point starts a step on its own. "Call this step" is therefore not something the engine can do, so a
|
|
2442
|
+
* picker offering steps would be offering an operation that does not exist. Step HANDLER names are
|
|
2443
|
+
* still advertised (`WorkerDescriptor.steps`) — that is a routing/capability fact used to decide
|
|
2444
|
+
* whether a dispatched step can be run, not an invocable catalog.
|
|
2445
|
+
*/
|
|
2446
|
+
|
|
2447
|
+
/** One live worker's claim about one workflow: the registration plus who announced it. */
|
|
2448
|
+
interface WorkflowAnnouncement extends WorkflowRegistration {
|
|
2449
|
+
/** The announcing worker's instance id. */
|
|
2450
|
+
instanceId: string;
|
|
2451
|
+
/** The announcing worker's runtime — how a Python-served workflow is told from a Node-served one. */
|
|
2452
|
+
runtime: 'node' | 'python';
|
|
2453
|
+
}
|
|
2454
|
+
/** An axis on which the live announcers of one workflow do not agree. `values` holds every distinct
|
|
2455
|
+
* DECLARED value (sorted); an announcer that stated nothing on the axis contributes nothing. */
|
|
2456
|
+
interface Disagreement {
|
|
2457
|
+
axis: 'group' | 'origin' | 'requires';
|
|
2458
|
+
values: string[];
|
|
2459
|
+
}
|
|
2460
|
+
/** One workflow the live fleet announces, folded across every worker that announced it. */
|
|
2461
|
+
interface AnnouncedWorkflow {
|
|
2462
|
+
/** `name@version`, or the bare `name` when no announcer stated a version. Stable sort key. */
|
|
2463
|
+
key: string;
|
|
2464
|
+
name: string;
|
|
2465
|
+
/** Absent when no announcer stated one — never inferred from another announcer's version. */
|
|
2466
|
+
version?: string;
|
|
2467
|
+
/** Every distinct routing group announced (sorted). Empty = nobody stated one. */
|
|
2468
|
+
groups: string[];
|
|
2469
|
+
/** Every distinct declaring package announced (sorted). Empty = nobody stated one. */
|
|
2470
|
+
origins: string[];
|
|
2471
|
+
/** Every distinct capability demand announced, each de-duplicated + sorted. Empty = nobody stated
|
|
2472
|
+
* one. Two entries here means two workers demand different capabilities for the same version. */
|
|
2473
|
+
requires: string[][];
|
|
2474
|
+
/** Runtimes of the live announcers (sorted, de-duplicated). */
|
|
2475
|
+
runtimes: ('node' | 'python')[];
|
|
2476
|
+
/** Instance ids of the live workers announcing it (sorted, de-duplicated). Its length is how many
|
|
2477
|
+
* workers can currently run this workflow — `1` is a single point of failure, and it is never 0
|
|
2478
|
+
* (an entry exists only because somebody announced it). */
|
|
2479
|
+
instances: string[];
|
|
2480
|
+
/** The axes the announcers differ on, empty when they speak with one voice. A caller that must
|
|
2481
|
+
* pick a single target has to resolve these itself — the registry refuses to guess. */
|
|
2482
|
+
disagreements: Disagreement[];
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* What one descriptor announces. `registrations` is authoritative when present; any `workflows` name
|
|
2486
|
+
* it does not cover is still announced as a bare, unversioned claim, so a worker that describes some
|
|
2487
|
+
* of its workflows richly and others by name alone is reported in full rather than in part.
|
|
2488
|
+
*
|
|
2489
|
+
* A descriptor with neither field announces nothing — including the control plane's own descriptor,
|
|
2490
|
+
* which is a router, not an executor.
|
|
2491
|
+
*/
|
|
2492
|
+
declare function announcementsOf(raw: RawWorkerDescriptor | WorkerDescriptor): WorkflowAnnouncement[];
|
|
2493
|
+
/**
|
|
2494
|
+
* Fold every live descriptor's announcements into one entry per `name@version`, sorted by key.
|
|
2495
|
+
*
|
|
2496
|
+
* Pure: it holds nothing and caches nothing, so no pod ever carries a growing table of the fleet's
|
|
2497
|
+
* registrations — the caller reads the live descriptors it already knows how to read, folds them
|
|
2498
|
+
* here, and drops the result. The work is O(descriptors × announcements) per call, on demand.
|
|
2499
|
+
*
|
|
2500
|
+
* The same instance announcing the same workflow twice (a worker publishes its descriptor under
|
|
2501
|
+
* every routing token it consumes, so the same bytes arrive several times) folds to ONE announcer:
|
|
2502
|
+
* `instances` is a set, so it counts workers, not keys.
|
|
2503
|
+
*/
|
|
2504
|
+
declare function aggregateAnnouncements(descriptors: readonly (RawWorkerDescriptor | WorkerDescriptor)[]): AnnouncedWorkflow[];
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* Bilateral compatibility negotiation between two handshake descriptors (design §7.3/§7.4). Pure
|
|
2508
|
+
* logic: given a local and a remote {@link WorkerDescriptor}, compute the negotiated session (highest
|
|
2509
|
+
* common protocol major + capability intersection) and classify it into one of three outcomes with a
|
|
2510
|
+
* precise, structured reason — never a bare boolean (design §7.6).
|
|
2511
|
+
*/
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* The three compatibility outcomes (design §7.4):
|
|
2515
|
+
* - `compatible` — protocol ranges intersect + full capability parity → dispatch freely.
|
|
2516
|
+
* - `degraded` — ranges intersect but a capability is missing on one side → dispatch, but route
|
|
2517
|
+
* capability-requiring work only to capable workers (soft warning).
|
|
2518
|
+
* - `incompatible` — no protocol-range overlap → do NOT dispatch; red flag with the exact reason.
|
|
2519
|
+
*/
|
|
2520
|
+
type NegotiationOutcome = 'compatible' | 'degraded' | 'incompatible';
|
|
2521
|
+
/** Structured failure/warning payload — carries the precise delta so a diagnostics event can render
|
|
2522
|
+
* it on the timeline + dashboard health panel (design §7.6). Never a bare boolean. */
|
|
2523
|
+
interface NegotiationReason {
|
|
2524
|
+
/** Stable machine code for alerting/telescope. */
|
|
2525
|
+
code: 'protocol.incompatible' | 'capability.unavailable';
|
|
2526
|
+
/** Human-readable one-liner (dashboard/telescope copy). */
|
|
2527
|
+
message: string;
|
|
2528
|
+
/** The exact delta: which protocol ranges failed to overlap, or which capabilities are missing. */
|
|
2529
|
+
detail: {
|
|
2530
|
+
localRange?: [number, number];
|
|
2531
|
+
remoteRange?: [number, number];
|
|
2532
|
+
/** Capabilities the local side advertises that the remote lacks. */
|
|
2533
|
+
missingOnRemote?: string[];
|
|
2534
|
+
/** Capabilities the remote side advertises that the local lacks. */
|
|
2535
|
+
missingOnLocal?: string[];
|
|
2536
|
+
/** Explicitly-required capabilities not satisfied by the negotiated session. */
|
|
2537
|
+
missingRequired?: string[];
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
/** The negotiated session + full delta between two sides. Carries both normalized descriptors so a
|
|
2541
|
+
* downstream diagnostics event can attach them wholesale (design §7.6). */
|
|
2542
|
+
interface NegotiationResult {
|
|
2543
|
+
outcome: NegotiationOutcome;
|
|
2544
|
+
/** Highest common protocol major, or `null` when ranges do not overlap (incompatible). */
|
|
2545
|
+
negotiatedProtocol: number | null;
|
|
2546
|
+
protocol: {
|
|
2547
|
+
localRange: [number, number];
|
|
2548
|
+
remoteRange: [number, number];
|
|
2549
|
+
};
|
|
2550
|
+
capabilities: {
|
|
2551
|
+
/** Intersection — the capabilities the negotiated session can rely on everywhere. */
|
|
2552
|
+
shared: string[];
|
|
2553
|
+
/** Local advertises, remote lacks. */
|
|
2554
|
+
missingOnRemote: string[];
|
|
2555
|
+
/** Remote advertises, local lacks. */
|
|
2556
|
+
missingOnLocal: string[];
|
|
2557
|
+
};
|
|
2558
|
+
/** Present for `degraded` + `incompatible`; absent for `compatible`. */
|
|
2559
|
+
reason?: NegotiationReason;
|
|
2560
|
+
local: WorkerDescriptor;
|
|
2561
|
+
remote: WorkerDescriptor;
|
|
2562
|
+
}
|
|
2563
|
+
interface NegotiateOptions {
|
|
2564
|
+
/**
|
|
2565
|
+
* Capabilities the local side needs the negotiated session to provide. A required capability the
|
|
2566
|
+
* remote lacks degrades the session (dispatch generally, but capability-requiring runs park via
|
|
2567
|
+
* the router, design §7.5) — it never, on its own, makes the pair *incompatible*: only a protocol
|
|
2568
|
+
* range gap does that (design §7.4).
|
|
2569
|
+
*/
|
|
2570
|
+
required?: string[];
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Negotiate compatibility between `local` and `remote` (design §7.3/§7.4). Accepts raw/partial
|
|
2574
|
+
* descriptors (a missing `protocol`/`capabilities` is normalized to the legacy-v1 baseline, so a
|
|
2575
|
+
* legacy peer negotiates as **assume-compatible**, design §7.7).
|
|
2576
|
+
*
|
|
2577
|
+
* Outcome logic:
|
|
2578
|
+
* 1. no protocol-range overlap → `incompatible` (`protocol.incompatible`);
|
|
2579
|
+
* 2. else if a `required` capability is missing on the remote → `degraded` (`capability.unavailable`);
|
|
2580
|
+
* 3. else if the capability sets differ at all (either direction) → `degraded`;
|
|
2581
|
+
* 4. else full parity → `compatible`.
|
|
2582
|
+
*
|
|
2583
|
+
* **Bilateral-symmetric** on outcome for the parity path: `negotiate(a, b).outcome ===
|
|
2584
|
+
* negotiate(b, a).outcome` (the `missingOnLocal`/`missingOnRemote` delta swaps). `required` is the
|
|
2585
|
+
* caller's own need and intentionally asymmetric.
|
|
2586
|
+
*/
|
|
2587
|
+
declare function negotiate(localRaw: RawWorkerDescriptor | WorkerDescriptor, remoteRaw: RawWorkerDescriptor | WorkerDescriptor, opts?: NegotiateOptions): NegotiationResult;
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* Capability-aware routing (design §7.5). A workflow/step handler may declare
|
|
2591
|
+
* `requires: ['saga', 'search-attr-v2']`; the control-plane dispatches only to workers whose
|
|
2592
|
+
* descriptor advertises every required capability. If no live capable worker exists the run must
|
|
2593
|
+
* **park as `blocked`** with a precise reason (visible in the dashboard) — never a silent hang.
|
|
2594
|
+
*
|
|
2595
|
+
* Pure logic: no transport, no store. Consumers (the dispatcher/provider) feed live descriptors in.
|
|
2596
|
+
*/
|
|
2597
|
+
|
|
2598
|
+
/** A handler's capability demand — either a bare list, or anything carrying a `requires` array
|
|
2599
|
+
* (a workflow/step ref). An absent/empty list means "runs anywhere" (design §7.5). */
|
|
2600
|
+
type CapabilityRequirement = string[] | {
|
|
2601
|
+
requires?: string[];
|
|
2602
|
+
};
|
|
2603
|
+
/** Normalize the two accepted shapes to a de-duplicated list of required capability names. */
|
|
2604
|
+
declare function requiredCapabilities(handler: CapabilityRequirement): string[];
|
|
2605
|
+
/**
|
|
2606
|
+
* Can `worker` run a handler with these requirements? True iff the worker's (normalized) descriptor
|
|
2607
|
+
* advertises **every** required capability. A legacy worker (no `capabilities` field) is normalized
|
|
2608
|
+
* to the v1 baseline first (design §7.7), so it can still run baseline-only work.
|
|
2609
|
+
*/
|
|
2610
|
+
declare function canRoute(handler: CapabilityRequirement, worker: RawWorkerDescriptor | WorkerDescriptor): boolean;
|
|
2611
|
+
/**
|
|
2612
|
+
* The capabilities a handler requires that `worker` does NOT advertise — the precise routing delta.
|
|
2613
|
+
* Empty iff {@link canRoute} is true.
|
|
2614
|
+
*/
|
|
2615
|
+
declare function missingCapabilities(handler: CapabilityRequirement, worker: RawWorkerDescriptor | WorkerDescriptor): string[];
|
|
2616
|
+
/** Filter a fleet down to the workers that can run a handler (design §7.5). */
|
|
2617
|
+
declare function capableWorkers<T extends RawWorkerDescriptor | WorkerDescriptor>(handler: CapabilityRequirement, workers: readonly T[]): T[];
|
|
2618
|
+
/** A run is dispatchable — at least one live worker can execute the handler. */
|
|
2619
|
+
interface RoutableResolution {
|
|
2620
|
+
status: 'routable';
|
|
2621
|
+
/** The normalized subset of the fleet that can run it. */
|
|
2622
|
+
workers: WorkerDescriptor[];
|
|
2623
|
+
requires: string[];
|
|
2624
|
+
}
|
|
2625
|
+
/** No live capable worker — the run parks as `blocked` with a human reason (design §7.5). */
|
|
2626
|
+
interface BlockedResolution {
|
|
2627
|
+
status: 'blocked';
|
|
2628
|
+
/** Exactly the dashboard copy: `blocked: no compatible worker (requires <caps>)`. */
|
|
2629
|
+
reason: string;
|
|
2630
|
+
requires: string[];
|
|
2631
|
+
}
|
|
2632
|
+
type RoutingResolution = RoutableResolution | BlockedResolution;
|
|
2633
|
+
/**
|
|
2634
|
+
* Decide whether a handler can be dispatched to the given live fleet (design §7.5). Returns
|
|
2635
|
+
* `routable` with the capable subset, or `blocked` with a precise reason when nobody can run it —
|
|
2636
|
+
* so the run parks visibly instead of hanging silently. An empty fleet always blocks.
|
|
2637
|
+
*/
|
|
2638
|
+
declare function resolveRouting(handler: CapabilityRequirement, workers: readonly (RawWorkerDescriptor | WorkerDescriptor)[]): RoutingResolution;
|
|
2639
|
+
|
|
2291
2640
|
type WorkflowFn = (ctx: InternalWorkflowCtx, input: unknown) => Promise<unknown>;
|
|
2292
2641
|
/**
|
|
2293
2642
|
* Options for {@link WorkflowEngine.start}.
|
|
@@ -2298,6 +2647,26 @@ type WorkflowFn = (ctx: InternalWorkflowCtx, input: unknown) => Promise<unknown>
|
|
|
2298
2647
|
* another lib's, and an origin filter would quietly lie.
|
|
2299
2648
|
*/
|
|
2300
2649
|
interface StartOptions {
|
|
2650
|
+
/**
|
|
2651
|
+
* Start THIS registered version instead of the newest one. Omit (the default) and the run starts on
|
|
2652
|
+
* the newest registered version, exactly as it always has — this option only ever narrows.
|
|
2653
|
+
*
|
|
2654
|
+
* Resolved against the by-name@version registry, so it pins anything explicitly registered:
|
|
2655
|
+
* {@link WorkflowEngine.register}, {@link WorkflowEngine.registerRemote}, {@link WorkflowEngine.remote}.
|
|
2656
|
+
* A version that is NOT registered throws before a run row is created and NEVER falls back to
|
|
2657
|
+
* `latest` — a silent fallback is the exact failure this option exists to prevent (a caller that
|
|
2658
|
+
* asked for `name@3` and got `name@4` has no way to tell, and its run is already going).
|
|
2659
|
+
*
|
|
2660
|
+
* NOT available on the two SYNTHESIZED registration paths, which exist precisely because nothing is
|
|
2661
|
+
* registered: a child inheriting its remote ancestor's routing (see `findInheritedRegistration`),
|
|
2662
|
+
* and convention routing to a live worker group of the same name (`resolveRemoteByConvention`).
|
|
2663
|
+
* Both INVENT the version they stamp — the ancestor's, and the `'1'` convention default — as a
|
|
2664
|
+
* routing placeholder; neither is evidence that the worker has a body at the version you asked for.
|
|
2665
|
+
* A pinned start that reaches them therefore throws rather than pretending to have honoured the pin.
|
|
2666
|
+
* To pin a cross-SDK workflow, register it for real — `engine.remote('name', { version: '2', group })`
|
|
2667
|
+
* — which puts `name@2` in the registry and pins like any other registration.
|
|
2668
|
+
*/
|
|
2669
|
+
version?: string | undefined;
|
|
2301
2670
|
/** Run-scoped tags, merged with the workflow's static `@Workflow({ tags })` onto the run. */
|
|
2302
2671
|
tags?: string[] | undefined;
|
|
2303
2672
|
/** Typed, queryable run data stamped on the run (e.g. `{ amount: 200, tier: 'pro' }`). */
|
|
@@ -2645,6 +3014,10 @@ declare class WorkflowEngine {
|
|
|
2645
3014
|
* remote (a `registerRemote` / synthesized child carries only a throwing stub, never handed out).
|
|
2646
3015
|
*/
|
|
2647
3016
|
workflowBody(name: string, version: string): WorkflowFn | undefined;
|
|
3017
|
+
/** The registered versions of `name`, for the pinned-start error — "2 is not registered" is a
|
|
3018
|
+
* guess-the-typo error without them, and the answer (which versions this deploy actually carries)
|
|
3019
|
+
* is right here. `none` when the name is unknown entirely. */
|
|
3020
|
+
private registeredVersions;
|
|
2648
3021
|
/**
|
|
2649
3022
|
* Register a workflow whose body runs in another SDK (e.g. Python). The engine owns the run exactly
|
|
2650
3023
|
* as for a TS workflow — it persists checkpoints, recovers, runs timers — but advances it by handing
|
|
@@ -2800,7 +3173,32 @@ declare class WorkflowEngine {
|
|
|
2800
3173
|
/**
|
|
2801
3174
|
* Cancel in-flight runs that have outlived their workflow's `executionTimeout`. Call it from the
|
|
2802
3175
|
* timer poller alongside {@link resumeDueTimers}. A timed-out run is moved to `cancelled` with an
|
|
2803
|
-
* `execution_timeout` error (terminal, so a late step result can't resurrect it)
|
|
3176
|
+
* `execution_timeout` error (terminal, so a late step result can't resurrect it), AND its children
|
|
3177
|
+
* are cascaded exactly as an explicit {@link cancel} cascades them.
|
|
3178
|
+
*
|
|
3179
|
+
* WHY THE TERMINAL WRITE IS DIRECT, not a call to {@link cancel}. Two reasons, both still standing:
|
|
3180
|
+
* the run must land with the `execution_timeout` error `cancel` knows nothing about (`cancel` writes
|
|
3181
|
+
* a plain `cancelled`, and the code is what a dashboard/alert distinguishes a timeout by), and this
|
|
3182
|
+
* is a SCAN — it walks every in-flight run of every timed workflow on every poller tick, so it must
|
|
3183
|
+
* not pay `cancel`'s per-run re-read and singleton/control-plane work on runs it isn't cancelling.
|
|
3184
|
+
* The cascade below is charged only to the runs that ACTUALLY timed out, which are rare by
|
|
3185
|
+
* construction; the scan itself is unchanged.
|
|
3186
|
+
*
|
|
3187
|
+
* The subtree, though, has no reason to differ, and before this it did: a timed-out parent's
|
|
3188
|
+
* children kept running with the parent gone and nothing pointing at them — a silent orphan you
|
|
3189
|
+
* could only find by reading the runs table by hand. {@link cancelChildren} is the SAME recursive
|
|
3190
|
+
* walk `cancel` uses, so children of children go too, and it inherits that path's two guards:
|
|
3191
|
+
* `cancel` returns early on a run that is already `completed`/`cancelled`/`dead` (a child that beat
|
|
3192
|
+
* the sweep to the finish is never clobbered, and a re-cancel is a no-op), and it writes the
|
|
3193
|
+
* parent's terminal status BEFORE recursing, so a cyclic parent→child graph terminates on the
|
|
3194
|
+
* second visit instead of looping. Two pollers sweeping the same run concurrently therefore write
|
|
3195
|
+
* the same terminal state twice and cascade into already-cancelled children — idempotent, not
|
|
3196
|
+
* corrupting. The one window it does NOT close is the pre-existing check-then-write race an
|
|
3197
|
+
* explicit `cancel` has always had (a child that completes between `cancel`'s read and its write);
|
|
3198
|
+
* closing that needs a compare-and-set in the store contract, not a change here.
|
|
3199
|
+
*
|
|
3200
|
+
* Cascade failures are swallowed per subtree for the same reason `cancelChildren` swallows them per
|
|
3201
|
+
* child: one unreachable child must not abort the sweep and leave every LATER timed-out run alive.
|
|
2804
3202
|
*/
|
|
2805
3203
|
sweepTimeouts(now?: number): Promise<void>;
|
|
2806
3204
|
/**
|
|
@@ -3064,6 +3462,10 @@ declare class WorkflowEngine {
|
|
|
3064
3462
|
* `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
|
|
3065
3463
|
* checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
|
|
3066
3464
|
* at finished / already-cancelled runs (no loops, no re-cancel).
|
|
3465
|
+
*
|
|
3466
|
+
* Two callers, deliberately the same walk: an explicit {@link cancel}, and {@link sweepTimeouts}
|
|
3467
|
+
* when a parent hits its `executionTimeout` — a timed-out parent's subtree dies the same way an
|
|
3468
|
+
* explicitly cancelled one's does, or the children are orphans nobody is holding a reference to.
|
|
3067
3469
|
*/
|
|
3068
3470
|
private cancelChildren;
|
|
3069
3471
|
/**
|
|
@@ -3090,6 +3492,25 @@ declare class WorkflowEngine {
|
|
|
3090
3492
|
* from registrations, shows once its workers beat). Empty when no transport can introspect health
|
|
3091
3493
|
* (only the BullMQ transport implements `groupHealth`). */
|
|
3092
3494
|
workerHealth(extra?: string[]): Promise<GroupHealth[]>;
|
|
3495
|
+
/**
|
|
3496
|
+
* What workflows the LIVE fleet announces it can execute — the deployment-wide registry a picker
|
|
3497
|
+
* ("call this existing workflow") needs, and the one question this engine's own registry cannot
|
|
3498
|
+
* answer. {@link workflowBody} answers only for this process, and a missing body is ambiguous: it
|
|
3499
|
+
* means "not registered here", but equally "registered via {@link registerRemote} against another
|
|
3500
|
+
* SDK" or "a group resolved by convention against a worker". An inferred answer would therefore
|
|
3501
|
+
* differ per replica. This one does not: every pod folds the same published announcements.
|
|
3502
|
+
*
|
|
3503
|
+
* Scoped like every other poll surface — an engine WITH a `namespace` sees only workers in it, and
|
|
3504
|
+
* an operator (namespace unset) sees the whole deployment ("ver tudo = ausência de namespace"). A
|
|
3505
|
+
* descriptor that states no namespace is `default`, matching how a single-tenant fleet publishes.
|
|
3506
|
+
*
|
|
3507
|
+
* Cost: one advertisement scan per CALL, on demand. Nothing is added to the poll loop and nothing
|
|
3508
|
+
* is retained — no pod holds a table of the fleet's registrations. Empty when no transport can
|
|
3509
|
+
* introspect the advertisement keyspace (a pure in-process transport advertises nothing).
|
|
3510
|
+
*
|
|
3511
|
+
* Workflows only, and deliberately: see the "steps are out of scope" note in `handshake/announced`.
|
|
3512
|
+
*/
|
|
3513
|
+
announcedWorkflows(): Promise<AnnouncedWorkflow[]>;
|
|
3093
3514
|
/**
|
|
3094
3515
|
* The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
|
|
3095
3516
|
* dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
|
|
@@ -3177,8 +3598,22 @@ declare class WorkflowEngine {
|
|
|
3177
3598
|
* cancel flag stays too, re-derived from the status on any later re-drive regardless.
|
|
3178
3599
|
*/
|
|
3179
3600
|
private compensateAndCancel;
|
|
3180
|
-
/**
|
|
3601
|
+
/**
|
|
3602
|
+
* The run body, lease held + renewed by {@link execute}, wrapped in the registered
|
|
3603
|
+
* {@link runDurableExecution} wrappers so an observability scope covers ONE WHOLE TURN.
|
|
3604
|
+
*
|
|
3605
|
+
* The wrap goes here — around the entire turn — rather than tightly around the `fn(ctx, input)`
|
|
3606
|
+
* call, because the store writes that bracket the body (the `pending` → `running` transition, the
|
|
3607
|
+
* step checkpoints, the terminal `settleRun` update) are exactly the queries an operator wants
|
|
3608
|
+
* correlated to the turn that issued them. Narrowing the wrap to the body would leave them
|
|
3609
|
+
* uncorrelated for the sake of a purity that buys nothing.
|
|
3610
|
+
*
|
|
3611
|
+
* Only the TS path is wrapped. {@link runRemoteExecution} does not execute a body at all — it
|
|
3612
|
+
* dispatches a workflow task and awaits the worker's decision — so a scope opened there would
|
|
3613
|
+
* describe a dispatch, not an execution, and would mislead anyone reading the trace.
|
|
3614
|
+
*/
|
|
3181
3615
|
private runExecution;
|
|
3616
|
+
private runExecutionTurn;
|
|
3182
3617
|
/**
|
|
3183
3618
|
* Undo every registered saga compensation in strict reverse order — the shared unwind both the
|
|
3184
3619
|
* run-failure path (`runExecution`'s catch) and the compensating-cancel path
|
|
@@ -3537,138 +3972,68 @@ declare class ContinueAsNew extends Error {
|
|
|
3537
3972
|
}
|
|
3538
3973
|
|
|
3539
3974
|
/**
|
|
3540
|
-
*
|
|
3541
|
-
*
|
|
3542
|
-
*
|
|
3543
|
-
*
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
/**
|
|
3547
|
-
* The three compatibility outcomes (design §7.4):
|
|
3548
|
-
* - `compatible` — protocol ranges intersect + full capability parity → dispatch freely.
|
|
3549
|
-
* - `degraded` — ranges intersect but a capability is missing on one side → dispatch, but route
|
|
3550
|
-
* capability-requiring work only to capable workers (soft warning).
|
|
3551
|
-
* - `incompatible` — no protocol-range overlap → do NOT dispatch; red flag with the exact reason.
|
|
3552
|
-
*/
|
|
3553
|
-
type NegotiationOutcome = 'compatible' | 'degraded' | 'incompatible';
|
|
3554
|
-
/** Structured failure/warning payload — carries the precise delta so a diagnostics event can render
|
|
3555
|
-
* it on the timeline + dashboard health panel (design §7.6). Never a bare boolean. */
|
|
3556
|
-
interface NegotiationReason {
|
|
3557
|
-
/** Stable machine code for alerting/telescope. */
|
|
3558
|
-
code: 'protocol.incompatible' | 'capability.unavailable';
|
|
3559
|
-
/** Human-readable one-liner (dashboard/telescope copy). */
|
|
3560
|
-
message: string;
|
|
3561
|
-
/** The exact delta: which protocol ranges failed to overlap, or which capabilities are missing. */
|
|
3562
|
-
detail: {
|
|
3563
|
-
localRange?: [number, number];
|
|
3564
|
-
remoteRange?: [number, number];
|
|
3565
|
-
/** Capabilities the local side advertises that the remote lacks. */
|
|
3566
|
-
missingOnRemote?: string[];
|
|
3567
|
-
/** Capabilities the remote side advertises that the local lacks. */
|
|
3568
|
-
missingOnLocal?: string[];
|
|
3569
|
-
/** Explicitly-required capabilities not satisfied by the negotiated session. */
|
|
3570
|
-
missingRequired?: string[];
|
|
3571
|
-
};
|
|
3572
|
-
}
|
|
3573
|
-
/** The negotiated session + full delta between two sides. Carries both normalized descriptors so a
|
|
3574
|
-
* downstream diagnostics event can attach them wholesale (design §7.6). */
|
|
3575
|
-
interface NegotiationResult {
|
|
3576
|
-
outcome: NegotiationOutcome;
|
|
3577
|
-
/** Highest common protocol major, or `null` when ranges do not overlap (incompatible). */
|
|
3578
|
-
negotiatedProtocol: number | null;
|
|
3579
|
-
protocol: {
|
|
3580
|
-
localRange: [number, number];
|
|
3581
|
-
remoteRange: [number, number];
|
|
3582
|
-
};
|
|
3583
|
-
capabilities: {
|
|
3584
|
-
/** Intersection — the capabilities the negotiated session can rely on everywhere. */
|
|
3585
|
-
shared: string[];
|
|
3586
|
-
/** Local advertises, remote lacks. */
|
|
3587
|
-
missingOnRemote: string[];
|
|
3588
|
-
/** Remote advertises, local lacks. */
|
|
3589
|
-
missingOnLocal: string[];
|
|
3590
|
-
};
|
|
3591
|
-
/** Present for `degraded` + `incompatible`; absent for `compatible`. */
|
|
3592
|
-
reason?: NegotiationReason;
|
|
3593
|
-
local: WorkerDescriptor;
|
|
3594
|
-
remote: WorkerDescriptor;
|
|
3595
|
-
}
|
|
3596
|
-
interface NegotiateOptions {
|
|
3597
|
-
/**
|
|
3598
|
-
* Capabilities the local side needs the negotiated session to provide. A required capability the
|
|
3599
|
-
* remote lacks degrades the session (dispatch generally, but capability-requiring runs park via
|
|
3600
|
-
* the router, design §7.5) — it never, on its own, makes the pair *incompatible*: only a protocol
|
|
3601
|
-
* range gap does that (design §7.4).
|
|
3602
|
-
*/
|
|
3603
|
-
required?: string[];
|
|
3604
|
-
}
|
|
3605
|
-
/**
|
|
3606
|
-
* Negotiate compatibility between `local` and `remote` (design §7.3/§7.4). Accepts raw/partial
|
|
3607
|
-
* descriptors (a missing `protocol`/`capabilities` is normalized to the legacy-v1 baseline, so a
|
|
3608
|
-
* legacy peer negotiates as **assume-compatible**, design §7.7).
|
|
3975
|
+
* Wrappers around the **real execution** of a unit of durable work — a workflow turn, or a step
|
|
3976
|
+
* handler body. Distinct from {@link StepInterceptor} (`engine.use`), which only sees the in-process
|
|
3977
|
+
* `localStep` primitives (`ctx.now`, `ctx.sideEffect`, `ctx.task`'s dispatch step, saga
|
|
3978
|
+
* compensations): a user's `ctx.step` is ALWAYS dispatched, so its handler runs inside a transport
|
|
3979
|
+
* callback or a worker process where no engine is in scope at all.
|
|
3609
3980
|
*
|
|
3610
|
-
*
|
|
3611
|
-
*
|
|
3612
|
-
*
|
|
3613
|
-
*
|
|
3614
|
-
* 4. else full parity → `compatible`.
|
|
3615
|
-
*
|
|
3616
|
-
* **Bilateral-symmetric** on outcome for the parity path: `negotiate(a, b).outcome ===
|
|
3617
|
-
* negotiate(b, a).outcome` (the `missingOnLocal`/`missingOnRemote` delta swaps). `required` is the
|
|
3618
|
-
* caller's own need and intentionally asymmetric.
|
|
3619
|
-
*/
|
|
3620
|
-
declare function negotiate(localRaw: RawWorkerDescriptor | WorkerDescriptor, remoteRaw: RawWorkerDescriptor | WorkerDescriptor, opts?: NegotiateOptions): NegotiationResult;
|
|
3621
|
-
|
|
3622
|
-
/**
|
|
3623
|
-
* Capability-aware routing (design §7.5). A workflow/step handler may declare
|
|
3624
|
-
* `requires: ['saga', 'search-attr-v2']`; the control-plane dispatches only to workers whose
|
|
3625
|
-
* descriptor advertises every required capability. If no live capable worker exists the run must
|
|
3626
|
-
* **park as `blocked`** with a precise reason (visible in the dashboard) — never a silent hang.
|
|
3981
|
+
* That is exactly why this registry is process-level rather than per-engine. `runStepHandler` is a
|
|
3982
|
+
* free function every transport shares, and a thin worker (`DurableModule` with a `connection` and
|
|
3983
|
+
* no `store`) holds no `WorkflowEngine` to hang a hook off. A per-engine registry could not reach
|
|
3984
|
+
* either, so the seam that covers every deployment shape has to sit above the engine.
|
|
3627
3985
|
*
|
|
3628
|
-
*
|
|
3986
|
+
* The intended consumer is an observability bridge (Telescope, OTel, a logger) that needs to
|
|
3987
|
+
* establish an ambient scope — an AsyncLocalStorage batch, a span — that the executing code records
|
|
3988
|
+
* into. It is deliberately NOT a transform point: see {@link runDurableExecution}.
|
|
3629
3989
|
*/
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3990
|
+
/** The unit of durable work a wrapper is being invoked around. */
|
|
3991
|
+
interface DurableExecution {
|
|
3992
|
+
/** `'workflow'` = one turn of a run's body; `'step'` = one step handler body. */
|
|
3993
|
+
readonly unit: 'workflow' | 'step';
|
|
3994
|
+
readonly runId: string;
|
|
3995
|
+
/** The workflow name. Present on a turn; a dispatched step task does not carry it on the wire. */
|
|
3996
|
+
readonly workflow: string | undefined;
|
|
3997
|
+
/** The step name, for `unit: 'step'`. Undefined for a turn. */
|
|
3998
|
+
readonly name: string | undefined;
|
|
3999
|
+
/** The step's logical position within the run, for `unit: 'step'`. */
|
|
4000
|
+
readonly seq: number | undefined;
|
|
4001
|
+
/** 1-based dispatch attempt, when the task carries one. */
|
|
4002
|
+
readonly attempt: number | undefined;
|
|
4003
|
+
}
|
|
3638
4004
|
/**
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3641
|
-
* to the v1 baseline first (design §7.7), so it can still run baseline-only work.
|
|
4005
|
+
* Wraps one unit of durable work. Call `next()` to run the body (or the next wrapper). Whatever the
|
|
4006
|
+
* wrapper returns or throws is discarded — see {@link runDurableExecution} for why.
|
|
3642
4007
|
*/
|
|
3643
|
-
|
|
4008
|
+
type DurableExecutionWrapper = (execution: DurableExecution, next: () => Promise<unknown>) => Promise<unknown>;
|
|
3644
4009
|
/**
|
|
3645
|
-
*
|
|
3646
|
-
*
|
|
4010
|
+
* Register a {@link DurableExecutionWrapper}. Returns an unregister function.
|
|
4011
|
+
*
|
|
4012
|
+
* Registering is the ONLY cost an unused hook imposes: with an empty registry
|
|
4013
|
+
* {@link runDurableExecution} returns the body's own promise without allocating a chain, so a host
|
|
4014
|
+
* that wires no observability pays nothing at all on the execution path.
|
|
3647
4015
|
*/
|
|
3648
|
-
declare function
|
|
3649
|
-
/**
|
|
3650
|
-
declare function
|
|
3651
|
-
/**
|
|
3652
|
-
|
|
3653
|
-
status: 'routable';
|
|
3654
|
-
/** The normalized subset of the fleet that can run it. */
|
|
3655
|
-
workers: WorkerDescriptor[];
|
|
3656
|
-
requires: string[];
|
|
3657
|
-
}
|
|
3658
|
-
/** No live capable worker — the run parks as `blocked` with a human reason (design §7.5). */
|
|
3659
|
-
interface BlockedResolution {
|
|
3660
|
-
status: 'blocked';
|
|
3661
|
-
/** Exactly the dashboard copy: `blocked: no compatible worker (requires <caps>)`. */
|
|
3662
|
-
reason: string;
|
|
3663
|
-
requires: string[];
|
|
3664
|
-
}
|
|
3665
|
-
type RoutingResolution = RoutableResolution | BlockedResolution;
|
|
4016
|
+
declare function useDurableExecution(wrapper: DurableExecutionWrapper): () => void;
|
|
4017
|
+
/** Drop every registered wrapper. For tests that must not leak a wrapper into the next file. */
|
|
4018
|
+
declare function clearDurableExecutionWrappers(): void;
|
|
4019
|
+
/** Whether any wrapper is registered — lets a hot path skip building an execution descriptor. */
|
|
4020
|
+
declare function hasDurableExecutionWrappers(): boolean;
|
|
3666
4021
|
/**
|
|
3667
|
-
*
|
|
3668
|
-
*
|
|
3669
|
-
*
|
|
4022
|
+
* Run `body` inside every registered wrapper and settle with **the body's own outcome**.
|
|
4023
|
+
*
|
|
4024
|
+
* The wrappers' return values and throws are deliberately ignored. An observer that misbehaves —
|
|
4025
|
+
* returns early without calling `next()` is the one case we cannot paper over, but one that throws
|
|
4026
|
+
* *after* the body settled, or swallows the body's error and returns normally, is common enough in
|
|
4027
|
+
* telemetry code — must not be able to change what the workflow saw. A wrapper that turned a failed
|
|
4028
|
+
* step into a completed one would corrupt the run's history and its checkpoints, which is a far
|
|
4029
|
+
* worse failure than a missing trace. This is the same reasoning the bullmq/schedule Telescope
|
|
4030
|
+
* watchers apply when they re-throw the host's error rather than swallowing it, pushed down into
|
|
4031
|
+
* the seam so every wrapper gets it for free instead of having to remember.
|
|
4032
|
+
*
|
|
4033
|
+
* The one unrecoverable case is a wrapper that never calls `next()`: the body did not run, so there
|
|
4034
|
+
* is no outcome to return and we throw rather than invent one.
|
|
3670
4035
|
*/
|
|
3671
|
-
declare function
|
|
4036
|
+
declare function runDurableExecution<T>(execution: DurableExecution, body: () => Promise<T>): Promise<T>;
|
|
3672
4037
|
|
|
3673
4038
|
/** Canonical step id — the stable identity of a step within a run, used for dedupe and
|
|
3674
4039
|
* correlation. The format is part of the cross-language wire contract (Python builds the same). */
|
|
@@ -4345,4 +4710,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
|
|
|
4345
4710
|
onControl(handler: (msg: ControlMessage) => void): void;
|
|
4346
4711
|
}
|
|
4347
4712
|
|
|
4348
|
-
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DispatchDiagnostics, type DispatchPlan, type DurableTopology, type DurableWebhook, DurableWorkflow, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, canRoute, capableWorkers, classifyWaiterToken, collectMetrics, controlPlaneDescriptor, createStepLogger, currentStep, currentWorkflowCtx, debug, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, heartbeatStatus, indexWaitersByRun, info, isEventToken, isLegacyDescriptor, isWorkflowControlFlowSignal, log, matchesAttributes, missingCapabilities, negotiate, normalizeAttributeRows, normalizeDescriptor, parseDuration, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveRouting, resolveRunWaiting, runInStepLogger, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, warn, workflowName };
|
|
4713
|
+
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AnnouncedWorkflow, type AttributeFilter, type AttributeOp, type BackoffStrategy, type BlockedDispatch, type BlockedResolution, CONTROL_FLOW_SIGNAL, CURRENT_PROTOCOL_VERSION, type CapabilityRequirement, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type Disagreement, type DispatchDiagnostics, type DispatchPlan, type DurableExecution, type DurableExecutionWrapper, type DurableTopology, type DurableWebhook, DurableWorkflow, EVENT_BUFFER_SCAN_LIMIT, EVENT_TOKEN_PREFIX, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HeartbeatStatus, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type InferSearchAttributes, LEGACY_V1_CAPABILITIES, LEGACY_V1_PROTOCOL, type MetricsCollector, type MetricsSnapshot, type NamedTransport, type NegotiateOptions, type NegotiationOutcome, type NegotiationReason, type NegotiationResult, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, type RawWorkerDescriptor, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RoutableDispatch, type RoutableResolution, type RoutingResolution, type RunAttributeRow, type RunDetail, type RunDispatcher, RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, type SearchAttributesSchema, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, StandardSchemaV1, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WireDates, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerDescriptor, type WorkerHeartbeat, type WorkerLifecycle, type WorkerStatus, type WorkflowAnnouncement, WorkflowBlocked, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowHandler, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRegistration, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, aggregateAnnouncements, announcementsOf, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, canRoute, capableWorkers, classifyWaiterToken, clearDurableExecutionWrappers, collectMetrics, controlPlaneDescriptor, createStepLogger, currentStep, currentWorkflowCtx, debug, describeWorker, descriptorHash, error, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, hasDurableExecutionWrappers, heartbeatStatus, indexWaitersByRun, info, isEventToken, isLegacyDescriptor, isWorkflowControlFlowSignal, log, matchesAttributes, missingCapabilities, negotiate, normalizeAttributeRows, normalizeDescriptor, parseDuration, planDispatch, prevCronFireMs, readSearchAttributes, requiredCapabilities, resolveRouting, resolveRunWaiting, runDurableExecution, runInStepLogger, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, sub, subEvent, subProcess, tenantGroup, useDurableExecution, warn, workflowName };
|