@adonis-agora/durable 0.1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/assets/dashboard.html +446 -0
- package/dist/commands/main.d.ts +15 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +15 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/retry.d.ts +16 -0
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/commands/retry.js +34 -0
- package/dist/commands/retry.js.map +1 -0
- package/dist/commands/runs.d.ts +17 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/runs.js +40 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/work.d.ts +18 -0
- package/dist/commands/work.d.ts.map +1 -0
- package/dist/commands/work.js +58 -0
- package/dist/commands/work.js.map +1 -0
- package/dist/configure.d.ts +15 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +26 -0
- package/dist/configure.js.map +1 -0
- package/dist/providers/dashboard_provider.d.ts +27 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -0
- package/dist/providers/dashboard_provider.js +94 -0
- package/dist/providers/dashboard_provider.js.map +1 -0
- package/dist/providers/durable_provider.d.ts +36 -0
- package/dist/providers/durable_provider.d.ts.map +1 -0
- package/dist/providers/durable_provider.js +113 -0
- package/dist/providers/durable_provider.js.map +1 -0
- package/dist/src/admission-redis/index.d.ts +2 -0
- package/dist/src/admission-redis/index.d.ts.map +1 -0
- package/dist/src/admission-redis/index.js +2 -0
- package/dist/src/admission-redis/index.js.map +1 -0
- package/dist/src/admission-redis/redis-admission-backend.d.ts +83 -0
- package/dist/src/admission-redis/redis-admission-backend.d.ts.map +1 -0
- package/dist/src/admission-redis/redis-admission-backend.js +200 -0
- package/dist/src/admission-redis/redis-admission-backend.js.map +1 -0
- package/dist/src/admission.d.ts +49 -0
- package/dist/src/admission.d.ts.map +1 -0
- package/dist/src/admission.js +30 -0
- package/dist/src/admission.js.map +1 -0
- package/dist/src/backoff.d.ts +5 -0
- package/dist/src/backoff.d.ts.map +1 -0
- package/dist/src/backoff.js +11 -0
- package/dist/src/backoff.js.map +1 -0
- package/dist/src/checkpoints.d.ts +42 -0
- package/dist/src/checkpoints.d.ts.map +1 -0
- package/dist/src/checkpoints.js +57 -0
- package/dist/src/checkpoints.js.map +1 -0
- package/dist/src/codec-state-store.d.ts +60 -0
- package/dist/src/codec-state-store.d.ts.map +1 -0
- package/dist/src/codec-state-store.js +129 -0
- package/dist/src/codec-state-store.js.map +1 -0
- package/dist/src/commands/index.d.ts +3 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/index.js +3 -0
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/runs.d.ts +24 -0
- package/dist/src/commands/runs.d.ts.map +1 -0
- package/dist/src/commands/runs.js +39 -0
- package/dist/src/commands/runs.js.map +1 -0
- package/dist/src/commands/worker.d.ts +61 -0
- package/dist/src/commands/worker.d.ts.map +1 -0
- package/dist/src/commands/worker.js +74 -0
- package/dist/src/commands/worker.js.map +1 -0
- package/dist/src/completion.d.ts +15 -0
- package/dist/src/completion.d.ts.map +1 -0
- package/dist/src/completion.js +10 -0
- package/dist/src/completion.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +48 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -0
- package/dist/src/dashboard/define_config.js +57 -0
- package/dist/src/dashboard/define_config.js.map +1 -0
- package/dist/src/dashboard/handlers.d.ts +47 -0
- package/dist/src/dashboard/handlers.d.ts.map +1 -0
- package/dist/src/dashboard/handlers.js +163 -0
- package/dist/src/dashboard/handlers.js.map +1 -0
- package/dist/src/dashboard/html.d.ts +7 -0
- package/dist/src/dashboard/html.d.ts.map +1 -0
- package/dist/src/dashboard/html.js +24 -0
- package/dist/src/dashboard/html.js.map +1 -0
- package/dist/src/dashboard/index.d.ts +8 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +6 -0
- package/dist/src/dashboard/index.js.map +1 -0
- package/dist/src/define_config.d.ts +72 -0
- package/dist/src/define_config.d.ts.map +1 -0
- package/dist/src/define_config.js +8 -0
- package/dist/src/define_config.js.map +1 -0
- package/dist/src/diagnostics-bridge.d.ts +22 -0
- package/dist/src/diagnostics-bridge.d.ts.map +1 -0
- package/dist/src/diagnostics-bridge.js +25 -0
- package/dist/src/diagnostics-bridge.js.map +1 -0
- package/dist/src/duration.d.ts +6 -0
- package/dist/src/duration.d.ts.map +1 -0
- package/dist/src/duration.js +39 -0
- package/dist/src/duration.js.map +1 -0
- package/dist/src/engine.d.ts +583 -0
- package/dist/src/engine.d.ts.map +1 -0
- package/dist/src/engine.js +1894 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/entities.d.ts +44 -0
- package/dist/src/entities.d.ts.map +1 -0
- package/dist/src/entities.js +64 -0
- package/dist/src/entities.js.map +1 -0
- package/dist/src/errors.d.ts +68 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +102 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/event-accumulators.d.ts +39 -0
- package/dist/src/event-accumulators.d.ts.map +1 -0
- package/dist/src/event-accumulators.js +70 -0
- package/dist/src/event-accumulators.js.map +1 -0
- package/dist/src/events.d.ts +9 -0
- package/dist/src/events.d.ts.map +1 -0
- package/dist/src/events.js +35 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/index.d.ts +36 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +37 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces.d.ts +909 -0
- package/dist/src/interfaces.d.ts.map +1 -0
- package/dist/src/interfaces.js +2 -0
- package/dist/src/interfaces.js.map +1 -0
- package/dist/src/metrics.d.ts +44 -0
- package/dist/src/metrics.d.ts.map +1 -0
- package/dist/src/metrics.js +78 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/otel/durable-metrics.d.ts +42 -0
- package/dist/src/otel/durable-metrics.d.ts.map +1 -0
- package/dist/src/otel/durable-metrics.js +75 -0
- package/dist/src/otel/durable-metrics.js.map +1 -0
- package/dist/src/otel/durable-otel.d.ts +20 -0
- package/dist/src/otel/durable-otel.d.ts.map +1 -0
- package/dist/src/otel/durable-otel.js +74 -0
- package/dist/src/otel/durable-otel.js.map +1 -0
- package/dist/src/otel/index.d.ts +4 -0
- package/dist/src/otel/index.d.ts.map +1 -0
- package/dist/src/otel/index.js +4 -0
- package/dist/src/otel/index.js.map +1 -0
- package/dist/src/otel/traceparent.d.ts +17 -0
- package/dist/src/otel/traceparent.d.ts.map +1 -0
- package/dist/src/otel/traceparent.js +21 -0
- package/dist/src/otel/traceparent.js.map +1 -0
- package/dist/src/pollers.d.ts +33 -0
- package/dist/src/pollers.d.ts.map +1 -0
- package/dist/src/pollers.js +76 -0
- package/dist/src/pollers.js.map +1 -0
- package/dist/src/protocol.d.ts +17 -0
- package/dist/src/protocol.d.ts.map +1 -0
- package/dist/src/protocol.js +47 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/queue.d.ts +113 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +128 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/remote-step-factory.d.ts +15 -0
- package/dist/src/remote-step-factory.d.ts.map +1 -0
- package/dist/src/remote-step-factory.js +12 -0
- package/dist/src/remote-step-factory.js.map +1 -0
- package/dist/src/remote-workflow-executor.d.ts +24 -0
- package/dist/src/remote-workflow-executor.d.ts.map +1 -0
- package/dist/src/remote-workflow-executor.js +70 -0
- package/dist/src/remote-workflow-executor.js.map +1 -0
- package/dist/src/scheduler.d.ts +67 -0
- package/dist/src/scheduler.d.ts.map +1 -0
- package/dist/src/scheduler.js +111 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/search-attributes.d.ts +48 -0
- package/dist/src/search-attributes.d.ts.map +1 -0
- package/dist/src/search-attributes.js +98 -0
- package/dist/src/search-attributes.js.map +1 -0
- package/dist/src/singleton-gate.d.ts +44 -0
- package/dist/src/singleton-gate.d.ts.map +1 -0
- package/dist/src/singleton-gate.js +90 -0
- package/dist/src/singleton-gate.js.map +1 -0
- package/dist/src/step-logger.d.ts +9 -0
- package/dist/src/step-logger.d.ts.map +1 -0
- package/dist/src/step-logger.js +89 -0
- package/dist/src/step-logger.js.map +1 -0
- package/dist/src/stores/factory.d.ts +42 -0
- package/dist/src/stores/factory.d.ts.map +1 -0
- package/dist/src/stores/factory.js +29 -0
- package/dist/src/stores/factory.js.map +1 -0
- package/dist/src/stores/lucid-helpers.d.ts +11 -0
- package/dist/src/stores/lucid-helpers.d.ts.map +1 -0
- package/dist/src/stores/lucid-helpers.js +38 -0
- package/dist/src/stores/lucid-helpers.js.map +1 -0
- package/dist/src/stores/lucid-mappers.d.ts +58 -0
- package/dist/src/stores/lucid-mappers.d.ts.map +1 -0
- package/dist/src/stores/lucid-mappers.js +174 -0
- package/dist/src/stores/lucid-mappers.js.map +1 -0
- package/dist/src/stores/lucid-schema.d.ts +28 -0
- package/dist/src/stores/lucid-schema.d.ts.map +1 -0
- package/dist/src/stores/lucid-schema.js +115 -0
- package/dist/src/stores/lucid-schema.js.map +1 -0
- package/dist/src/stores/lucid.d.ts +69 -0
- package/dist/src/stores/lucid.d.ts.map +1 -0
- package/dist/src/stores/lucid.js +342 -0
- package/dist/src/stores/lucid.js.map +1 -0
- package/dist/src/telescope/dashboard.d.ts +15 -0
- package/dist/src/telescope/dashboard.d.ts.map +1 -0
- package/dist/src/telescope/dashboard.js +113 -0
- package/dist/src/telescope/dashboard.js.map +1 -0
- package/dist/src/telescope/data-providers.d.ts +20 -0
- package/dist/src/telescope/data-providers.d.ts.map +1 -0
- package/dist/src/telescope/data-providers.js +322 -0
- package/dist/src/telescope/data-providers.js.map +1 -0
- package/dist/src/telescope/extension.d.ts +22 -0
- package/dist/src/telescope/extension.d.ts.map +1 -0
- package/dist/src/telescope/extension.js +38 -0
- package/dist/src/telescope/extension.js.map +1 -0
- package/dist/src/telescope/index.d.ts +5 -0
- package/dist/src/telescope/index.d.ts.map +1 -0
- package/dist/src/telescope/index.js +4 -0
- package/dist/src/telescope/index.js.map +1 -0
- package/dist/src/telescope/telescope-sdk.d.ts +123 -0
- package/dist/src/telescope/telescope-sdk.d.ts.map +1 -0
- package/dist/src/telescope/telescope-sdk.js +12 -0
- package/dist/src/telescope/telescope-sdk.js.map +1 -0
- package/dist/src/test-helpers.d.ts +11 -0
- package/dist/src/test-helpers.d.ts.map +1 -0
- package/dist/src/test-helpers.js +12 -0
- package/dist/src/test-helpers.js.map +1 -0
- package/dist/src/testing/in-memory-state-store.d.ts +64 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -0
- package/dist/src/testing/in-memory-state-store.js +259 -0
- package/dist/src/testing/in-memory-state-store.js.map +1 -0
- package/dist/src/testing/in-memory-transport.d.ts +25 -0
- package/dist/src/testing/in-memory-transport.d.ts.map +1 -0
- package/dist/src/testing/in-memory-transport.js +49 -0
- package/dist/src/testing/in-memory-transport.js.map +1 -0
- package/dist/src/testing-kit/admission-backend-conformance.d.ts +18 -0
- package/dist/src/testing-kit/admission-backend-conformance.d.ts.map +1 -0
- package/dist/src/testing-kit/admission-backend-conformance.js +81 -0
- package/dist/src/testing-kit/admission-backend-conformance.js.map +1 -0
- package/dist/src/testing-kit/assertions.d.ts +10 -0
- package/dist/src/testing-kit/assertions.d.ts.map +1 -0
- package/dist/src/testing-kit/assertions.js +41 -0
- package/dist/src/testing-kit/assertions.js.map +1 -0
- package/dist/src/testing-kit/harness.d.ts +28 -0
- package/dist/src/testing-kit/harness.d.ts.map +1 -0
- package/dist/src/testing-kit/harness.js +42 -0
- package/dist/src/testing-kit/harness.js.map +1 -0
- package/dist/src/testing-kit/index.d.ts +8 -0
- package/dist/src/testing-kit/index.d.ts.map +1 -0
- package/dist/src/testing-kit/index.js +8 -0
- package/dist/src/testing-kit/index.js.map +1 -0
- package/dist/src/testing-kit/replay.d.ts +22 -0
- package/dist/src/testing-kit/replay.d.ts.map +1 -0
- package/dist/src/testing-kit/replay.js +38 -0
- package/dist/src/testing-kit/replay.js.map +1 -0
- package/dist/src/testing-kit/state-store-conformance.d.ts +51 -0
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -0
- package/dist/src/testing-kit/state-store-conformance.js +430 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -0
- package/dist/src/testing-kit/steps.d.ts +5 -0
- package/dist/src/testing-kit/steps.d.ts.map +1 -0
- package/dist/src/testing-kit/steps.js +15 -0
- package/dist/src/testing-kit/steps.js.map +1 -0
- package/dist/src/testing-kit/transport-conformance.d.ts +16 -0
- package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -0
- package/dist/src/testing-kit/transport-conformance.js +64 -0
- package/dist/src/testing-kit/transport-conformance.js.map +1 -0
- package/dist/src/tokens.d.ts +18 -0
- package/dist/src/tokens.d.ts.map +1 -0
- package/dist/src/tokens.js +18 -0
- package/dist/src/tokens.js.map +1 -0
- package/dist/src/transport-pool.d.ts +30 -0
- package/dist/src/transport-pool.d.ts.map +1 -0
- package/dist/src/transport-pool.js +82 -0
- package/dist/src/transport-pool.js.map +1 -0
- package/dist/src/transports/db-helpers.d.ts +10 -0
- package/dist/src/transports/db-helpers.d.ts.map +1 -0
- package/dist/src/transports/db-helpers.js +33 -0
- package/dist/src/transports/db-helpers.js.map +1 -0
- package/dist/src/transports/db-schema.d.ts +28 -0
- package/dist/src/transports/db-schema.d.ts.map +1 -0
- package/dist/src/transports/db-schema.js +98 -0
- package/dist/src/transports/db-schema.js.map +1 -0
- package/dist/src/transports/db.d.ts +81 -0
- package/dist/src/transports/db.d.ts.map +1 -0
- package/dist/src/transports/db.js +340 -0
- package/dist/src/transports/db.js.map +1 -0
- package/dist/src/transports/factory.d.ts +79 -0
- package/dist/src/transports/factory.d.ts.map +1 -0
- package/dist/src/transports/factory.js +57 -0
- package/dist/src/transports/factory.js.map +1 -0
- package/dist/src/transports/queue-mock-adapter.d.ts +39 -0
- package/dist/src/transports/queue-mock-adapter.d.ts.map +1 -0
- package/dist/src/transports/queue-mock-adapter.js +86 -0
- package/dist/src/transports/queue-mock-adapter.js.map +1 -0
- package/dist/src/transports/queue.d.ts +84 -0
- package/dist/src/transports/queue.d.ts.map +1 -0
- package/dist/src/transports/queue.js +210 -0
- package/dist/src/transports/queue.js.map +1 -0
- package/dist/src/workflow-ctx.d.ts +64 -0
- package/dist/src/workflow-ctx.d.ts.map +1 -0
- package/dist/src/workflow-ctx.js +457 -0
- package/dist/src/workflow-ctx.js.map +1 -0
- package/dist/src/workflow-ref.d.ts +34 -0
- package/dist/src/workflow-ref.d.ts.map +1 -0
- package/dist/src/workflow-ref.js +28 -0
- package/dist/src/workflow-ref.js.map +1 -0
- package/dist/stubs/config/durable.stub +49 -0
- package/dist/stubs/config/durable_dashboard.stub +17 -0
- package/dist/stubs/database/migrations/create_durable_tables.stub +87 -0
- package/dist/stubs/database/migrations/create_durable_transport_tables.stub +75 -0
- package/dist/stubs/main.d.ts +3 -0
- package/dist/stubs/main.d.ts.map +1 -0
- package/dist/stubs/main.js +4 -0
- package/dist/stubs/main.js.map +1 -0
- package/package.json +154 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createStepLogger } from './step-logger.js';
|
|
2
|
+
/** Canonical step id — the stable identity of a step within a run, used for dedupe and
|
|
3
|
+
* correlation. The format is part of the cross-language wire contract (Python builds the same). */
|
|
4
|
+
export function stepId(runId, seq) {
|
|
5
|
+
return `${runId}:${seq}`;
|
|
6
|
+
}
|
|
7
|
+
/** Deterministic signal token a breakpoint suspends on — derived from its logical position. Shared
|
|
8
|
+
* so `ctx.breakpoint` (which records it) and `engine.continue` (which signals it) agree. */
|
|
9
|
+
export const breakpointToken = (runId, seq) => `bp:${runId}:${seq}`;
|
|
10
|
+
/**
|
|
11
|
+
* Run `handler` for `task` and produce the wire-format {@link StepResult}. Pure (no transport,
|
|
12
|
+
* no I/O beyond the handler), so every transport — and any language port — can share the exact
|
|
13
|
+
* same completed / failed / no-handler contract instead of re-deriving it.
|
|
14
|
+
*/
|
|
15
|
+
export async function runStepHandler(task, handler) {
|
|
16
|
+
// Stamp the worker's pickup time so the engine can report queue-wait (startedAt − enqueuedAt).
|
|
17
|
+
// This is the one place every transport funnels through, so timing comes for free everywhere.
|
|
18
|
+
const base = { runId: task.runId, seq: task.seq, stepId: task.stepId, startedAt: Date.now() };
|
|
19
|
+
if (!handler) {
|
|
20
|
+
return {
|
|
21
|
+
...base,
|
|
22
|
+
status: 'failed',
|
|
23
|
+
error: { message: `no handler for ${task.name}`, retryable: false },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const events = [];
|
|
27
|
+
const withEvents = (result) => events.length > 0 ? { ...result, events } : result;
|
|
28
|
+
try {
|
|
29
|
+
const output = await handler(task.input, createStepLogger(events, Date.now));
|
|
30
|
+
return withEvents({ ...base, status: 'completed', output });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
// Carry `code`/`retryable` off the thrown error if present, so the engine's durable retry can
|
|
34
|
+
// honour a worker's "don't retry this" verdict (e.g. a declined card).
|
|
35
|
+
const e = err;
|
|
36
|
+
return withEvents({
|
|
37
|
+
...base,
|
|
38
|
+
status: 'failed',
|
|
39
|
+
error: {
|
|
40
|
+
message: err instanceof Error ? err.message : String(err),
|
|
41
|
+
...(typeof e?.code === 'string' ? { code: e.code } : {}),
|
|
42
|
+
...(typeof e?.retryable === 'boolean' ? { retryable: e.retryable } : {}),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD;oGACoG;AACpG,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,GAAW;IAC/C,OAAO,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED;6FAC6F;AAC7F,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAa,EAAE,GAAW,EAAU,EAAE,CAAC,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;AAM5F;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAgB,EAChB,OAAgC;IAEhC,+FAA+F;IAC/F,8FAA8F;IAC9F,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC9F,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,GAAG,IAAI;YACP,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,EAAE,OAAO,EAAE,kBAAkB,IAAI,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;SACpE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,MAAM,UAAU,GAAG,CAAC,MAAkB,EAAc,EAAE,CACpD,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,OAAO,UAAU,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,8FAA8F;QAC9F,uEAAuE;QACvE,MAAM,CAAC,GAAG,GAA+D,CAAC;QAC1E,OAAO,UAAU,CAAC;YAChB,GAAG,IAAI;YACP,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE;gBACL,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;gBACzD,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,GAAG,CAAC,OAAO,CAAC,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzE;SACF,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow control for remote steps. A queue caps how much work `ctx.call(step, input, { queue })`
|
|
3
|
+
* admits at once — a concurrency limit and/or a fixed-window rate limit. When a call can't be
|
|
4
|
+
* admitted it doesn't dispatch: the run re-suspends with a near-future wake time, and the timer
|
|
5
|
+
* poller re-tries admission later — so flow control is durable (survives crashes) without holding
|
|
6
|
+
* the run in memory.
|
|
7
|
+
*
|
|
8
|
+
* Accounting is per engine instance (the DBOS `workerConcurrency` tier): correct for the common
|
|
9
|
+
* single-orchestrator deployment. Global, cross-instance limits would need a durable counter in the
|
|
10
|
+
* store — a deliberate follow-up, not built here.
|
|
11
|
+
*
|
|
12
|
+
* Beyond raw caps, the controller can admit contended slots by PRIORITY and by per-KEY FAIRNESS
|
|
13
|
+
* (round-robin), so a single noisy key/tenant can't monopolize the concurrency budget. This is the
|
|
14
|
+
* DISPATCH/admission layer — it never touches workflow replay (engine positional logic). Default
|
|
15
|
+
* behaviour is unchanged FIFO when neither priority nor fairness is configured/passed.
|
|
16
|
+
*
|
|
17
|
+
* Design (vs Temporal/Inngest/DBOS): Temporal task queues are FIFO with separate priority levels;
|
|
18
|
+
* Inngest concurrency keys + throttle give per-key fairness; DBOS queues are FIFO with a
|
|
19
|
+
* `workerConcurrency` cap. We fold both knobs into the same in-process admission gate: an optional
|
|
20
|
+
* per-call `priority` (higher first) and an optional queue-level `fairness: 'key'` (round-robin by
|
|
21
|
+
* the per-call `key`). Because blocked calls re-suspend durably and retry independently (there is no
|
|
22
|
+
* resident FIFO list to reorder), the controller remembers the set of WAITERS that asked-and-were-
|
|
23
|
+
* blocked, and on a later retry only admits the one that is rightful-next under the policy.
|
|
24
|
+
*/
|
|
25
|
+
export interface QueueConfig {
|
|
26
|
+
/** Queue name, referenced by `ctx.call(step, input, { queue: name })`. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Max steps in flight at once for this queue (this instance). Omit for unlimited. */
|
|
29
|
+
concurrency?: number;
|
|
30
|
+
/** Fixed-window rate limit: at most `limit` admissions per `periodMs`. Omit for unlimited. */
|
|
31
|
+
rateLimit?: {
|
|
32
|
+
limit: number;
|
|
33
|
+
periodMs: number;
|
|
34
|
+
};
|
|
35
|
+
/** Delay (ms) before a concurrency-blocked call re-checks for a free slot. Default 1000. */
|
|
36
|
+
retryMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Fairness policy for a contended concurrency slot. `'key'` round-robins across distinct per-call
|
|
39
|
+
* keys (the least-recently-served key wins the next slot), so one key can't monopolize the budget.
|
|
40
|
+
* Omit (or `'fifo'`) for plain first-come ordering. Priority always wins over fairness; fairness
|
|
41
|
+
* only breaks ties within the same priority.
|
|
42
|
+
*/
|
|
43
|
+
fairness?: 'fifo' | 'key';
|
|
44
|
+
/**
|
|
45
|
+
* Arrival-order tiebreak among otherwise-equal waiters (same priority, same fairness rank).
|
|
46
|
+
* `'fifo'` (default) admits the earliest arrival; `'lifo'` admits the most recent (a stack).
|
|
47
|
+
* Orthogonal to `priority` and `fairness` — those still win first; this only breaks their ties.
|
|
48
|
+
*/
|
|
49
|
+
order?: 'fifo' | 'lifo';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Per-call admission hints. All optional — omitting them keeps the legacy FIFO behaviour.
|
|
53
|
+
* - `priority`: higher is admitted first (default 0).
|
|
54
|
+
* - `key`: the fairness bucket (e.g. a tenant id) for `fairness: 'key'`.
|
|
55
|
+
* - `waiterId`: a stable id for THIS call across its retries (the engine passes the stepId), so the
|
|
56
|
+
* controller can track it as one waiter instead of counting each retry as a new contender.
|
|
57
|
+
*/
|
|
58
|
+
export interface AdmissionItem {
|
|
59
|
+
priority?: number | undefined;
|
|
60
|
+
key?: string | undefined;
|
|
61
|
+
waiterId?: string | undefined;
|
|
62
|
+
}
|
|
63
|
+
/** Admission decision: either admitted, or blocked until `retryAt` (epoch ms). */
|
|
64
|
+
export type Admission = {
|
|
65
|
+
ok: true;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
retryAt: number;
|
|
69
|
+
};
|
|
70
|
+
/** In-process admission controller for one {@link QueueConfig}. */
|
|
71
|
+
export declare class QueueController {
|
|
72
|
+
readonly config: QueueConfig;
|
|
73
|
+
private readonly clock;
|
|
74
|
+
private inFlight;
|
|
75
|
+
private windowStart;
|
|
76
|
+
private windowCount;
|
|
77
|
+
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
78
|
+
private readonly waiters;
|
|
79
|
+
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
80
|
+
private waiterSeq;
|
|
81
|
+
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
82
|
+
private readonly keyServed;
|
|
83
|
+
private servedTick;
|
|
84
|
+
constructor(config: QueueConfig, clock: () => number);
|
|
85
|
+
/** Whether this controller does any priority/fairness/arrival ordering (else a plain FIFO gate). */
|
|
86
|
+
private get ordered();
|
|
87
|
+
/**
|
|
88
|
+
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
89
|
+
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
90
|
+
*
|
|
91
|
+
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
92
|
+
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
93
|
+
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
94
|
+
*/
|
|
95
|
+
tryAdmit(item?: AdmissionItem): Admission;
|
|
96
|
+
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
97
|
+
release(): void;
|
|
98
|
+
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
99
|
+
private register;
|
|
100
|
+
/**
|
|
101
|
+
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
102
|
+
* 1. higher `priority`;
|
|
103
|
+
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
104
|
+
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
105
|
+
* A bare item (no waiterId) is compared on its hints directly.
|
|
106
|
+
*/
|
|
107
|
+
private isNext;
|
|
108
|
+
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
109
|
+
private better;
|
|
110
|
+
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
111
|
+
private servedOf;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAUtE,mEAAmE;AACnE,qBAAa,eAAe;IAaxB,QAAQ,CAAC,MAAM,EAAE,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAbxB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,WAAW,CAAK;IACxB,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,oFAAoF;IACpF,OAAO,CAAC,SAAS,CAAK;IACtB,4FAA4F;IAC5F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,UAAU,CAAK;gBAGZ,MAAM,EAAE,WAAW,EACX,KAAK,EAAE,MAAM,MAAM;IAGtC,oGAAoG;IACpG,OAAO,KAAK,OAAO,GAElB;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS;IA0CzC,yEAAyE;IACzE,OAAO,IAAI,IAAI;IAIf,kGAAkG;IAClG,OAAO,CAAC,QAAQ;IAchB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM;IAWd,iFAAiF;IACjF,OAAO,CAAC,MAAM;IAUd,qGAAqG;IACrG,OAAO,CAAC,QAAQ;CAIjB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** In-process admission controller for one {@link QueueConfig}. */
|
|
2
|
+
export class QueueController {
|
|
3
|
+
config;
|
|
4
|
+
clock;
|
|
5
|
+
inFlight = 0;
|
|
6
|
+
windowStart = 0;
|
|
7
|
+
windowCount = 0;
|
|
8
|
+
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
9
|
+
waiters = new Map();
|
|
10
|
+
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
11
|
+
waiterSeq = 0;
|
|
12
|
+
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
13
|
+
keyServed = new Map();
|
|
14
|
+
servedTick = 0;
|
|
15
|
+
constructor(config, clock) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.clock = clock;
|
|
18
|
+
}
|
|
19
|
+
/** Whether this controller does any priority/fairness/arrival ordering (else a plain FIFO gate). */
|
|
20
|
+
get ordered() {
|
|
21
|
+
return this.config.fairness === 'key' || this.config.order === 'lifo';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
25
|
+
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
26
|
+
*
|
|
27
|
+
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
28
|
+
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
29
|
+
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
30
|
+
*/
|
|
31
|
+
tryAdmit(item) {
|
|
32
|
+
const now = this.clock();
|
|
33
|
+
const rl = this.config.rateLimit;
|
|
34
|
+
// Rate limit is a hard global window — checked first and unaffected by priority/fairness.
|
|
35
|
+
if (rl) {
|
|
36
|
+
if (now - this.windowStart >= rl.periodMs) {
|
|
37
|
+
this.windowStart = now;
|
|
38
|
+
this.windowCount = 0;
|
|
39
|
+
}
|
|
40
|
+
if (this.windowCount >= rl.limit)
|
|
41
|
+
return { ok: false, retryAt: this.windowStart + rl.periodMs };
|
|
42
|
+
}
|
|
43
|
+
const usingOrder = this.ordered || item?.priority != null || item?.waiterId != null;
|
|
44
|
+
const waiterId = item?.waiterId;
|
|
45
|
+
// Concurrency gate.
|
|
46
|
+
const full = this.config.concurrency != null && this.inFlight >= this.config.concurrency;
|
|
47
|
+
if (full) {
|
|
48
|
+
// No slot at all → block (and register as a waiter so a later retry can be ordered).
|
|
49
|
+
if (usingOrder && waiterId != null)
|
|
50
|
+
this.register(waiterId, item);
|
|
51
|
+
return { ok: false, retryAt: now + (this.config.retryMs ?? 1000) };
|
|
52
|
+
}
|
|
53
|
+
// A slot is free. Without ordering, admit immediately (legacy FIFO). With ordering, only admit if
|
|
54
|
+
// THIS call is the rightful next among all registered waiters — otherwise keep it waiting so the
|
|
55
|
+
// freed slot can go to a higher-priority / under-served key on its retry.
|
|
56
|
+
if (usingOrder && this.waiters.size > 0) {
|
|
57
|
+
if (waiterId != null)
|
|
58
|
+
this.register(waiterId, item);
|
|
59
|
+
if (!this.isNext(waiterId, item)) {
|
|
60
|
+
return { ok: false, retryAt: now + (this.config.retryMs ?? 1000) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Admit: consume the slot, the rate-window tick, and clear this waiter.
|
|
64
|
+
if (rl)
|
|
65
|
+
this.windowCount += 1;
|
|
66
|
+
this.inFlight += 1;
|
|
67
|
+
if (waiterId != null)
|
|
68
|
+
this.waiters.delete(waiterId);
|
|
69
|
+
if (item?.key != null)
|
|
70
|
+
this.keyServed.set(item.key, ++this.servedTick);
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
74
|
+
release() {
|
|
75
|
+
if (this.inFlight > 0)
|
|
76
|
+
this.inFlight -= 1;
|
|
77
|
+
}
|
|
78
|
+
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
79
|
+
register(waiterId, item) {
|
|
80
|
+
const existing = this.waiters.get(waiterId);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.priority = item?.priority ?? 0;
|
|
83
|
+
existing.key = item?.key;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.waiters.set(waiterId, {
|
|
87
|
+
priority: item?.priority ?? 0,
|
|
88
|
+
key: item?.key,
|
|
89
|
+
seq: this.waiterSeq++,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
94
|
+
* 1. higher `priority`;
|
|
95
|
+
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
96
|
+
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
97
|
+
* A bare item (no waiterId) is compared on its hints directly.
|
|
98
|
+
*/
|
|
99
|
+
isNext(waiterId, item) {
|
|
100
|
+
const self = (waiterId != null ? this.waiters.get(waiterId) : undefined) ??
|
|
101
|
+
{ priority: item?.priority ?? 0, key: item?.key, seq: Number.POSITIVE_INFINITY };
|
|
102
|
+
let best = self;
|
|
103
|
+
for (const w of this.waiters.values()) {
|
|
104
|
+
if (this.better(w, best))
|
|
105
|
+
best = w;
|
|
106
|
+
}
|
|
107
|
+
return best === self;
|
|
108
|
+
}
|
|
109
|
+
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
110
|
+
better(a, b) {
|
|
111
|
+
if (a.priority !== b.priority)
|
|
112
|
+
return a.priority > b.priority;
|
|
113
|
+
if (this.config.fairness === 'key') {
|
|
114
|
+
const sa = this.servedOf(a.key);
|
|
115
|
+
const sb = this.servedOf(b.key);
|
|
116
|
+
if (sa !== sb)
|
|
117
|
+
return sa < sb; // least-recently-served key first
|
|
118
|
+
}
|
|
119
|
+
return this.config.order === 'lifo' ? a.seq > b.seq : a.seq < b.seq; // arrival tiebreak
|
|
120
|
+
}
|
|
121
|
+
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
122
|
+
servedOf(key) {
|
|
123
|
+
if (key == null)
|
|
124
|
+
return 0;
|
|
125
|
+
return this.keyServed.get(key) ?? 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/queue.ts"],"names":[],"mappings":"AAwEA,mEAAmE;AACnE,MAAM,OAAO,eAAe;IAaf;IACQ;IAbX,QAAQ,GAAG,CAAC,CAAC;IACb,WAAW,GAAG,CAAC,CAAC;IAChB,WAAW,GAAG,CAAC,CAAC;IACxB,uFAAuF;IACtE,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IACrD,oFAAoF;IAC5E,SAAS,GAAG,CAAC,CAAC;IACtB,4FAA4F;IAC3E,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,UAAU,GAAG,CAAC,CAAC;IAEvB,YACW,MAAmB,EACX,KAAmB;QAD3B,WAAM,GAAN,MAAM,CAAa;QACX,UAAK,GAAL,KAAK,CAAc;IACnC,CAAC;IAEJ,oGAAoG;IACpG,IAAY,OAAO;QACjB,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC;IACxE,CAAC;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAoB;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC;QACjC,0FAA0F;QAC1F,IAAI,EAAE,EAAE,CAAC;YACP,IAAI,GAAG,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;gBAC1C,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;gBACvB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;YACvB,CAAC;YACD,IAAI,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,KAAK;gBAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;QAClE,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,EAAE,QAAQ,IAAI,IAAI,IAAI,IAAI,EAAE,QAAQ,IAAI,IAAI,CAAC;QACpF,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,CAAC;QAEhC,oBAAoB;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC;QACzF,IAAI,IAAI,EAAE,CAAC;YACT,qFAAqF;YACrF,IAAI,UAAU,IAAI,QAAQ,IAAI,IAAI;gBAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAClE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;QACrE,CAAC;QAED,kGAAkG;QAClG,iGAAiG;QACjG,0EAA0E;QAC1E,IAAI,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACxC,IAAI,QAAQ,IAAI,IAAI;gBAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;gBACjC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;YACrE,CAAC;QACH,CAAC;QAED,wEAAwE;QACxE,IAAI,EAAE;YAAE,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnB,IAAI,QAAQ,IAAI,IAAI;YAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpD,IAAI,IAAI,EAAE,GAAG,IAAI,IAAI;YAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACvE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IAED,yEAAyE;IACzE,OAAO;QACL,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC;YAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,kGAAkG;IAC1F,QAAQ,CAAC,QAAgB,EAAE,IAAoB;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,QAAQ,GAAG,IAAI,EAAE,QAAQ,IAAI,CAAC,CAAC;YACxC,QAAQ,CAAC,GAAG,GAAG,IAAI,EAAE,GAAG,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE;YACzB,QAAQ,EAAE,IAAI,EAAE,QAAQ,IAAI,CAAC;YAC7B,GAAG,EAAE,IAAI,EAAE,GAAG;YACd,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,QAA4B,EAAE,IAAoB;QAC/D,MAAM,IAAI,GACR,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1D,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,iBAAiB,EAAa,CAAC;QAC/F,IAAI,IAAI,GAAG,IAAI,CAAC;QAChB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC;gBAAE,IAAI,GAAG,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,IAAI,KAAK,IAAI,CAAC;IACvB,CAAC;IAED,iFAAiF;IACzE,MAAM,CAAC,CAAS,EAAE,CAAS;QACjC,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;YAAE,OAAO,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC;QAC9D,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YACnC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAChC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,EAAE,KAAK,EAAE;gBAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,kCAAkC;QACnE,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,mBAAmB;IAC1F,CAAC;IAED,qGAAqG;IAC7F,QAAQ,CAAC,GAAY;QAC3B,IAAI,GAAG,IAAI,IAAI;YAAE,OAAO,CAAC,CAAC;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { RemoteStepDef, StepOptions } from './interfaces.js';
|
|
3
|
+
export interface RemoteStepConfig<TInput, TOutput> extends StepOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
/** Worker group expected to handle this step. Defaults to the `name` prefix before the first dot. */
|
|
6
|
+
group?: string;
|
|
7
|
+
input: z.ZodType<TInput>;
|
|
8
|
+
output: z.ZodType<TOutput>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Defines a typed handle to a step that runs on a remote worker. Call it from a workflow with
|
|
12
|
+
* `ctx.call(step, input)`; a worker registers a handler under the same `name`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function remoteStep<TInput, TOutput>(config: RemoteStepConfig<TInput, TOutput>): RemoteStepDef<TInput, TOutput>;
|
|
15
|
+
//# sourceMappingURL=remote-step-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-step-factory.d.ts","sourceRoot":"","sources":["../../src/remote-step-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAElE,MAAM,WAAW,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAE,SAAQ,WAAW;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qGAAqG;IACrG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,EACxC,MAAM,EAAE,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,GACxC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAMhC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines a typed handle to a step that runs on a remote worker. Call it from a workflow with
|
|
3
|
+
* `ctx.call(step, input)`; a worker registers a handler under the same `name`.
|
|
4
|
+
*/
|
|
5
|
+
export function remoteStep(config) {
|
|
6
|
+
return {
|
|
7
|
+
...config,
|
|
8
|
+
group: config.group ?? config.name.split('.')[0] ?? config.name,
|
|
9
|
+
__remote: true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=remote-step-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-step-factory.js","sourceRoot":"","sources":["../../src/remote-step-factory.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,MAAyC;IAEzC,OAAO;QACL,GAAG,MAAM;QACT,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI;QAC/D,QAAQ,EAAE,IAAI;KACf,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HistoryEvent, Transport, WorkflowDecision, WorkflowExecutor, WorkflowRun } from './interfaces.js';
|
|
2
|
+
/**
|
|
3
|
+
* A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
|
|
4
|
+
* dispatching a {@link WorkflowTask} over the broker and awaiting the matching {@link WorkflowDecision}
|
|
5
|
+
* (correlated by `taskId`). Pass one to `engine.registerRemote(name, version, { group, executor })` so
|
|
6
|
+
* a workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
|
|
7
|
+
*
|
|
8
|
+
* Recovery-safe: if the engine crashes awaiting a decision, the re-drive dispatches a fresh task with
|
|
9
|
+
* the same history — the worker's replay is deterministic, so it returns the same decision; a late
|
|
10
|
+
* decision for the old `taskId` simply finds no waiter and is dropped.
|
|
11
|
+
*/
|
|
12
|
+
export declare class RemoteWorkflowExecutor implements WorkflowExecutor {
|
|
13
|
+
private readonly transport;
|
|
14
|
+
private readonly group;
|
|
15
|
+
private readonly opts;
|
|
16
|
+
private readonly pending;
|
|
17
|
+
private subscribed;
|
|
18
|
+
constructor(transport: Transport, group: string, opts?: {
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
});
|
|
21
|
+
private ensureSubscribed;
|
|
22
|
+
advance(run: WorkflowRun, history: HistoryEvent[]): Promise<WorkflowDecision>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=remote-workflow-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-workflow-executor.d.ts","sourceRoot":"","sources":["../../src/remote-workflow-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AAIzB;;;;;;;;;GASG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAK3D,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2D;IACnF,OAAO,CAAC,UAAU,CAAS;gBAGR,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO;IAGpD,OAAO,CAAC,gBAAgB;IAelB,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA+BpF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
let taskCounter = 0;
|
|
2
|
+
/**
|
|
3
|
+
* A {@link WorkflowExecutor} backed by a {@link Transport}: it advances a remote workflow by
|
|
4
|
+
* dispatching a {@link WorkflowTask} over the broker and awaiting the matching {@link WorkflowDecision}
|
|
5
|
+
* (correlated by `taskId`). Pass one to `engine.registerRemote(name, version, { group, executor })` so
|
|
6
|
+
* a workflow authored in another SDK (e.g. the Python `durable-worker`) is driven over Redis/BullMQ.
|
|
7
|
+
*
|
|
8
|
+
* Recovery-safe: if the engine crashes awaiting a decision, the re-drive dispatches a fresh task with
|
|
9
|
+
* the same history — the worker's replay is deterministic, so it returns the same decision; a late
|
|
10
|
+
* decision for the old `taskId` simply finds no waiter and is dropped.
|
|
11
|
+
*/
|
|
12
|
+
export class RemoteWorkflowExecutor {
|
|
13
|
+
transport;
|
|
14
|
+
group;
|
|
15
|
+
opts;
|
|
16
|
+
pending = new Map();
|
|
17
|
+
subscribed = false;
|
|
18
|
+
constructor(transport, group, opts = {}) {
|
|
19
|
+
this.transport = transport;
|
|
20
|
+
this.group = group;
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
}
|
|
23
|
+
ensureSubscribed() {
|
|
24
|
+
if (this.subscribed)
|
|
25
|
+
return;
|
|
26
|
+
if (!this.transport.onDecision) {
|
|
27
|
+
throw new Error('transport does not support workflow decisions (onDecision)');
|
|
28
|
+
}
|
|
29
|
+
this.subscribed = true;
|
|
30
|
+
this.transport.onDecision(async (decision) => {
|
|
31
|
+
const resolve = this.pending.get(decision.taskId);
|
|
32
|
+
if (resolve) {
|
|
33
|
+
this.pending.delete(decision.taskId);
|
|
34
|
+
resolve(decision);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async advance(run, history) {
|
|
39
|
+
if (!this.transport.dispatchWorkflowTask) {
|
|
40
|
+
throw new Error('transport does not support workflow tasks (dispatchWorkflowTask)');
|
|
41
|
+
}
|
|
42
|
+
this.ensureSubscribed();
|
|
43
|
+
taskCounter += 1;
|
|
44
|
+
const taskId = `${run.id}:wf:${taskCounter}`;
|
|
45
|
+
const task = {
|
|
46
|
+
taskId,
|
|
47
|
+
runId: run.id,
|
|
48
|
+
workflow: run.workflow,
|
|
49
|
+
workflowVersion: run.workflowVersion,
|
|
50
|
+
input: run.input,
|
|
51
|
+
history,
|
|
52
|
+
group: this.group,
|
|
53
|
+
priority: run.priority,
|
|
54
|
+
attempt: 1,
|
|
55
|
+
};
|
|
56
|
+
const decision = new Promise((resolve, reject) => {
|
|
57
|
+
this.pending.set(taskId, resolve);
|
|
58
|
+
if (this.opts.timeoutMs) {
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
this.pending.delete(taskId);
|
|
61
|
+
reject(new Error(`workflow task ${taskId} timed out after ${this.opts.timeoutMs}ms`));
|
|
62
|
+
}, this.opts.timeoutMs);
|
|
63
|
+
timer.unref?.();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
await this.transport.dispatchWorkflowTask(task);
|
|
67
|
+
return decision;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=remote-workflow-executor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-workflow-executor.js","sourceRoot":"","sources":["../../src/remote-workflow-executor.ts"],"names":[],"mappings":"AASA,IAAI,WAAW,GAAG,CAAC,CAAC;AAEpB;;;;;;;;;GASG;AACH,MAAM,OAAO,sBAAsB;IAKd;IACA;IACA;IANF,OAAO,GAAG,IAAI,GAAG,EAAgD,CAAC;IAC3E,UAAU,GAAG,KAAK,CAAC;IAE3B,YACmB,SAAoB,EACpB,KAAa,EACb,OAA+B,EAAE;QAFjC,cAAS,GAAT,SAAS,CAAW;QACpB,UAAK,GAAL,KAAK,CAAQ;QACb,SAAI,GAAJ,IAAI,CAA6B;IACjD,CAAC;IAEI,gBAAgB;QACtB,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAChF,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAClD,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACrC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAgB,EAAE,OAAuB;QACrD,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,oBAAoB,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,WAAW,IAAI,CAAC,CAAC;QACjB,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,OAAO,WAAW,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAiB;YACzB,MAAM;YACN,KAAK,EAAE,GAAG,CAAC,EAAE;YACb,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,eAAe,EAAE,GAAG,CAAC,eAAe;YACpC,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,OAAO;YACP,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,OAAO,EAAE,CAAC;SACX,CAAC;QACF,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAClC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBAC5B,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,MAAM,oBAAoB,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;gBACxF,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACvB,KAAgC,CAAC,KAAK,EAAE,EAAE,CAAC;YAC9C,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QAChD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { WorkflowEngine } from './engine.js';
|
|
2
|
+
export interface ScheduledWorkflow {
|
|
3
|
+
/** Stable key identifying this schedule — part of the deterministic run id. */
|
|
4
|
+
key: string;
|
|
5
|
+
workflow: string;
|
|
6
|
+
input?: unknown;
|
|
7
|
+
/** Start one run every `everyMs`. Mutually exclusive with `cron`. */
|
|
8
|
+
everyMs?: number;
|
|
9
|
+
/**
|
|
10
|
+
* A cron expression (5 fields `m h dom mon dow`, or 6 with leading seconds) evaluated in
|
|
11
|
+
* {@link ScheduledWorkflow.timezone}. Mutually exclusive with `everyMs`. Needs the optional peer
|
|
12
|
+
* dependency `cron-parser`.
|
|
13
|
+
*/
|
|
14
|
+
cron?: string;
|
|
15
|
+
/** IANA timezone the `cron` fires in (e.g. `America/Sao_Paulo`). Defaults to UTC. */
|
|
16
|
+
timezone?: string;
|
|
17
|
+
/** Temporarily stop firing this schedule (kept registered). Defaults to false. */
|
|
18
|
+
paused?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* What to do when the previous window's run hasn't finished yet (fixed-interval schedules only):
|
|
21
|
+
* `'allow'` (default) starts the new window anyway; `'skip'` skips it while the prior run is still
|
|
22
|
+
* `running`/`suspended`, so a slow run can't pile up overlapping executions.
|
|
23
|
+
*/
|
|
24
|
+
overlap?: 'allow' | 'skip';
|
|
25
|
+
/**
|
|
26
|
+
* Randomly delay the dispatch by up to this many ms BEFORE firing, to spread load when many
|
|
27
|
+
* instances tick on the same boundary (avoids a thundering herd). Opt-in; absent = fire immediately.
|
|
28
|
+
*
|
|
29
|
+
* This is dispatch-path jitter only — it does NOT affect the run id (still the time bucket), so
|
|
30
|
+
* idempotency is unchanged: two instances jittering the same window still start it exactly once.
|
|
31
|
+
* (The engine forbids `Math.random` inside workflow code; the scheduler dispatch path is not
|
|
32
|
+
* workflow code, so jitter here is fine.)
|
|
33
|
+
*/
|
|
34
|
+
jitter?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Enqueue windows that were missed while the scheduler was down, instead of silently skipping
|
|
37
|
+
* them. Opt-in; absent = only the current window fires. `maxCatchup` bounds how many PRIOR windows
|
|
38
|
+
* are backfilled (a long outage can't flood the system). Backfilled runs use each missed window's
|
|
39
|
+
* deterministic bucket run id, so `engine.start` idempotency skips any that already ran.
|
|
40
|
+
*/
|
|
41
|
+
backfill?: {
|
|
42
|
+
maxCatchup: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Injectable dispatch-path effects (kept out of the deterministic run-id logic). Defaults to real
|
|
46
|
+
* `Math.random` + `setTimeout`; overridden in tests so jitter is deterministic and instant. */
|
|
47
|
+
export interface RunSchedulesOptions {
|
|
48
|
+
random?: () => number;
|
|
49
|
+
sleep?: (ms: number) => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** Deterministic run id for a fixed-interval schedule's current time window. */
|
|
52
|
+
export declare function scheduledRunId(key: string, everyMs: number, nowMs: number): string;
|
|
53
|
+
/**
|
|
54
|
+
* Epoch ms of the most recent cron fire at or before `nowMs`, evaluated in `timezone` (default UTC).
|
|
55
|
+
* This is the deterministic "bucket" a cron run is keyed on, so polling repeatedly within an
|
|
56
|
+
* interval resolves to the same fire time — and thus the same idempotent run id.
|
|
57
|
+
*/
|
|
58
|
+
export declare function prevCronFireMs(expr: string, nowMs: number, timezone?: string): number;
|
|
59
|
+
/**
|
|
60
|
+
* Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
|
|
61
|
+
* or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
|
|
62
|
+
* racing two instances on the same tick — starts **each window exactly once**. Wire it to a
|
|
63
|
+
* `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids for the
|
|
64
|
+
* current windows.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runSchedules(engine: Pick<WorkflowEngine, 'start' | 'getRun'>, schedules: readonly ScheduledWorkflow[], nowMs: number, opts?: RunSchedulesOptions): Promise<string[]>;
|
|
67
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/scheduler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC;AAED;gGACgG;AAChG,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAID,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElF;AAiBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,MAAM,CAKpF;AAwCD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,QAAQ,CAAC,EAChD,SAAS,EAAE,SAAS,iBAAiB,EAAE,EACvC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBnB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// Resolve the optional `cron-parser` peer dependency in a way that works in BOTH the ESM and CJS
|
|
3
|
+
// builds of this package (dual publish). `createRequire(import.meta.url)` gives a real `require` in
|
|
4
|
+
// ESM output; in the CJS build, tsup's banner shims `import.meta.url` to the file URL, so the same
|
|
5
|
+
// call resolves there too. This keeps the load synchronous (no async leak into the scheduler API).
|
|
6
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
7
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8
|
+
/** Deterministic run id for a fixed-interval schedule's current time window. */
|
|
9
|
+
export function scheduledRunId(key, everyMs, nowMs) {
|
|
10
|
+
return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
|
|
11
|
+
}
|
|
12
|
+
let cronParser;
|
|
13
|
+
function loadCronParser() {
|
|
14
|
+
if (cronParser)
|
|
15
|
+
return cronParser;
|
|
16
|
+
try {
|
|
17
|
+
// Lazy + optional: core stays dependency-free; only users who schedule by cron need this.
|
|
18
|
+
cronParser = nodeRequire('cron-parser');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error('cron schedules need the optional peer dependency "cron-parser" — install it (e.g. `npm i cron-parser`).');
|
|
22
|
+
}
|
|
23
|
+
return cronParser;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Epoch ms of the most recent cron fire at or before `nowMs`, evaluated in `timezone` (default UTC).
|
|
27
|
+
* This is the deterministic "bucket" a cron run is keyed on, so polling repeatedly within an
|
|
28
|
+
* interval resolves to the same fire time — and thus the same idempotent run id.
|
|
29
|
+
*/
|
|
30
|
+
export function prevCronFireMs(expr, nowMs, timezone = 'UTC') {
|
|
31
|
+
const parser = loadCronParser();
|
|
32
|
+
// `+1` makes a fire landing exactly on `nowMs` count as "at or before now", not the prior one.
|
|
33
|
+
const it = parser.parseExpression(expr, { currentDate: new Date(nowMs + 1), tz: timezone });
|
|
34
|
+
return it.prev().toDate().getTime();
|
|
35
|
+
}
|
|
36
|
+
/** The deterministic, idempotent run id for a schedule at `nowMs` — its current fire window. */
|
|
37
|
+
function scheduleRunIdAt(s, nowMs) {
|
|
38
|
+
if (s.cron != null)
|
|
39
|
+
return `sched:${s.key}:${prevCronFireMs(s.cron, nowMs, s.timezone)}`;
|
|
40
|
+
if (s.everyMs != null)
|
|
41
|
+
return scheduledRunId(s.key, s.everyMs, nowMs);
|
|
42
|
+
throw new Error(`schedule "${s.key}" needs either "everyMs" or "cron"`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The run ids this schedule should fire at `nowMs`: the current window, plus — when `backfill` is set
|
|
46
|
+
* — up to `maxCatchup` prior windows that may have been missed. Oldest-first, so a backfill catches
|
|
47
|
+
* up in chronological order. Ids are the same deterministic buckets `scheduleRunIdAt` produces, so
|
|
48
|
+
* `engine.start` idempotency drops any window that already ran.
|
|
49
|
+
*/
|
|
50
|
+
function scheduleRunIdsAt(s, nowMs) {
|
|
51
|
+
const current = scheduleRunIdAt(s, nowMs);
|
|
52
|
+
if (!s.backfill || s.backfill.maxCatchup <= 0)
|
|
53
|
+
return [current];
|
|
54
|
+
const ids = [];
|
|
55
|
+
if (s.everyMs != null) {
|
|
56
|
+
const bucket = Math.floor(nowMs / s.everyMs);
|
|
57
|
+
const oldest = Math.max(0, bucket - s.backfill.maxCatchup);
|
|
58
|
+
for (let b = oldest; b < bucket; b += 1)
|
|
59
|
+
ids.push(`sched:${s.key}:${b}`);
|
|
60
|
+
}
|
|
61
|
+
else if (s.cron != null) {
|
|
62
|
+
// Walk back through prior cron fire times (most-recent missed first), then reverse to oldest-first.
|
|
63
|
+
const prior = [];
|
|
64
|
+
let cursor = prevCronFireMs(s.cron, nowMs, s.timezone);
|
|
65
|
+
for (let i = 0; i < s.backfill.maxCatchup; i += 1) {
|
|
66
|
+
// `cursor` (exclusive) → the fire strictly before it.
|
|
67
|
+
const before = prevCronFireMs(s.cron, cursor - 1, s.timezone);
|
|
68
|
+
prior.push(`sched:${s.key}:${before}`);
|
|
69
|
+
cursor = before;
|
|
70
|
+
}
|
|
71
|
+
ids.push(...prior.reverse());
|
|
72
|
+
}
|
|
73
|
+
ids.push(current);
|
|
74
|
+
return ids;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start each schedule's current-window run. The run id is the time bucket (a fixed-interval index,
|
|
78
|
+
* or the cron fire time) and `engine.start` is idempotent, so firing this on an interval — or
|
|
79
|
+
* racing two instances on the same tick — starts **each window exactly once**. Wire it to a
|
|
80
|
+
* `setInterval`, the durable timer poller, or `@nestjs/schedule`. Returns the run ids for the
|
|
81
|
+
* current windows.
|
|
82
|
+
*/
|
|
83
|
+
export async function runSchedules(engine, schedules, nowMs, opts) {
|
|
84
|
+
const random = opts?.random ?? Math.random;
|
|
85
|
+
const sleep = opts?.sleep ?? defaultSleep;
|
|
86
|
+
const ids = [];
|
|
87
|
+
for (const s of schedules) {
|
|
88
|
+
if (s.paused)
|
|
89
|
+
continue;
|
|
90
|
+
// overlap:'skip' (fixed-interval): don't start this window while the previous window's run is
|
|
91
|
+
// still in-flight, so a slow run can't pile up. Interval-only — cron windows aren't adjacent ids.
|
|
92
|
+
if (s.overlap === 'skip' && s.everyMs) {
|
|
93
|
+
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
94
|
+
if (prevBucket >= 0) {
|
|
95
|
+
const prev = await engine.getRun(`sched:${s.key}:${prevBucket}`);
|
|
96
|
+
if (prev && (prev.status === 'running' || prev.status === 'suspended'))
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Dispatch-path jitter (not workflow code): spread same-boundary ticks across instances. Applied
|
|
101
|
+
// once per schedule, before firing — the run ids below are unaffected, so idempotency holds.
|
|
102
|
+
if (s.jitter && s.jitter > 0)
|
|
103
|
+
await sleep(Math.floor(random() * s.jitter));
|
|
104
|
+
for (const runId of scheduleRunIdsAt(s, nowMs)) {
|
|
105
|
+
await engine.start(s.workflow, s.input, runId);
|
|
106
|
+
ids.push(runId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=scheduler.js.map
|