@shware/workflow 3.1.0 → 3.2.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/README.md +4 -2
- package/dist/aws/engine-step.cjs +2 -3
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +10 -0
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +10 -0
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +2 -3
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +9 -1
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +6 -0
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +6 -0
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +9 -1
- package/dist/aws/index.mjs.map +1 -1
- package/dist/cloudflare/runner.cjs +4 -5
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +4 -5
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/engine/duration.cjs +34 -0
- package/dist/engine/duration.cjs.map +1 -0
- package/dist/engine/duration.d.cts +5 -0
- package/dist/engine/duration.d.cts.map +1 -0
- package/dist/engine/duration.d.mts +5 -0
- package/dist/engine/duration.d.mts.map +1 -0
- package/dist/engine/duration.mjs +33 -0
- package/dist/engine/duration.mjs.map +1 -0
- package/dist/engine/index.cjs +5 -0
- package/dist/engine/index.d.cts +3 -1
- package/dist/engine/index.d.mts +3 -1
- package/dist/engine/index.mjs +3 -1
- package/dist/engine/retry.cjs +29 -0
- package/dist/engine/retry.cjs.map +1 -0
- package/dist/engine/retry.d.cts +17 -0
- package/dist/engine/retry.d.cts.map +1 -0
- package/dist/engine/retry.d.mts +17 -0
- package/dist/engine/retry.d.mts.map +1 -0
- package/dist/engine/retry.mjs +27 -0
- package/dist/engine/retry.mjs.map +1 -0
- package/dist/host/journey-host.cjs +15 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +15 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +4 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.d.cts +2 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -1
- package/dist/transports/cloudflare-email.d.mts +2 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -1
- package/dist/transports/cloudflare-email.mjs +4 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +4 -0
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +2 -0
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +2 -0
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +4 -0
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/transports/sigv4.cjs +6 -1
- package/dist/transports/sigv4.cjs.map +1 -1
- package/dist/transports/sigv4.d.cts.map +1 -1
- package/dist/transports/sigv4.d.mts.map +1 -1
- package/dist/transports/sigv4.mjs +6 -1
- package/dist/transports/sigv4.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -521,7 +521,8 @@ reads `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` at sen
|
|
|
521
521
|
time (a Lambda's execution role); a Worker passes them, or a provider, from its
|
|
522
522
|
env. Delivery is at least once: SES has no de-duplication key, so an email the
|
|
523
523
|
provider accepted just before the step's checkpoint failed can go out twice.
|
|
524
|
-
|
|
524
|
+
A host whose workflows send email but that has no `email` option (nor a
|
|
525
|
+
`messages` override) fails when it is created, naming those workflows.
|
|
525
526
|
|
|
526
527
|
An email step refuses to send when the profile has no `email`.
|
|
527
528
|
`GET /preview/<id>?props=<JSON>` renders a template, with the subject in the
|
|
@@ -816,7 +817,8 @@ Workflow:
|
|
|
816
817
|
Handler: aws.handler
|
|
817
818
|
Timeout: 60
|
|
818
819
|
AutoPublishAlias: live
|
|
819
|
-
# ExecutionTimeout: the longest journey (waits, delays and the goal window), up to 366 days
|
|
820
|
+
# ExecutionTimeout: the longest journey (waits, delays and the goal window), up to 366 days;
|
|
821
|
+
# pass the same value to journeyLambda({ executionTimeoutMs }) to check it at startup
|
|
820
822
|
DurableConfig: { ExecutionTimeout: 2592000, RetentionPeriodInDays: 30 }
|
|
821
823
|
FunctionUrlConfig: { AuthType: NONE, InvokeMode: BUFFERED }
|
|
822
824
|
```
|
package/dist/aws/engine-step.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_engine_retry = require("../engine/retry.cjs");
|
|
2
3
|
let _aws_durable_execution_sdk_js = require("@aws/durable-execution-sdk-js");
|
|
3
4
|
//#region src/aws/engine-step.ts
|
|
4
5
|
/**
|
|
@@ -11,11 +12,9 @@ let _aws_durable_execution_sdk_js = require("@aws/durable-execution-sdk-js");
|
|
|
11
12
|
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
12
13
|
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
13
14
|
*/
|
|
14
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
15
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
16
15
|
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
17
16
|
function stepRetry(error, attempt) {
|
|
18
|
-
if (
|
|
17
|
+
if (!require_engine_retry.isRetryable(error) || attempt >= 6) return { shouldRetry: false };
|
|
19
18
|
return {
|
|
20
19
|
shouldRetry: true,
|
|
21
20
|
delay: { seconds: Math.min(60, 2 ** attempt) }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-step.cjs","names":["CallbackTimeoutError"],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/**
|
|
1
|
+
{"version":3,"file":"engine-step.cjs","names":["isRetryable","CallbackTimeoutError"],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,6 +3,16 @@ import { ExecutionRegistry, JourneyStore } from "../store/index.cjs";
|
|
|
3
3
|
import { LambdaApi } from "./lambda-api.cjs";
|
|
4
4
|
import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
|
|
5
5
|
//#region src/aws/engine-step.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* EngineStep over a Lambda durable execution.
|
|
8
|
+
*
|
|
9
|
+
* Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
|
|
10
|
+
* callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
|
|
11
|
+
* every attempt): created where the durable context allows it — outside any step, so its id
|
|
12
|
+
* replays — then recorded in Postgres together with the subscription rows, numbered by a
|
|
13
|
+
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
14
|
+
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
15
|
+
*/
|
|
6
16
|
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
7
17
|
export declare function stepRetry(error: Error, attempt: number): RetryDecision;
|
|
8
18
|
export declare class AwsEngineStep implements EngineStep {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;;wBAuBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
|
|
@@ -3,6 +3,16 @@ import { ExecutionRegistry, JourneyStore } from "../store/index.mjs";
|
|
|
3
3
|
import { LambdaApi } from "./lambda-api.mjs";
|
|
4
4
|
import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
|
|
5
5
|
//#region src/aws/engine-step.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* EngineStep over a Lambda durable execution.
|
|
8
|
+
*
|
|
9
|
+
* Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
|
|
10
|
+
* callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
|
|
11
|
+
* every attempt): created where the durable context allows it — outside any step, so its id
|
|
12
|
+
* replays — then recorded in Postgres together with the subscription rows, numbered by a
|
|
13
|
+
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
14
|
+
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
15
|
+
*/
|
|
6
16
|
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
7
17
|
export declare function stepRetry(error: Error, attempt: number): RetryDecision;
|
|
8
18
|
export declare class AwsEngineStep implements EngineStep {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;;wBAuBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
|
package/dist/aws/engine-step.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRetryable } from "../engine/retry.mjs";
|
|
1
2
|
import { CallbackTimeoutError } from "@aws/durable-execution-sdk-js";
|
|
2
3
|
//#region src/aws/engine-step.ts
|
|
3
4
|
/**
|
|
@@ -10,11 +11,9 @@ import { CallbackTimeoutError } from "@aws/durable-execution-sdk-js";
|
|
|
10
11
|
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
11
12
|
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
12
13
|
*/
|
|
13
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
14
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
15
14
|
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
16
15
|
function stepRetry(error, attempt) {
|
|
17
|
-
if (
|
|
16
|
+
if (!isRetryable(error) || attempt >= 6) return { shouldRetry: false };
|
|
18
17
|
return {
|
|
19
18
|
shouldRetry: true,
|
|
20
19
|
delay: { seconds: Math.min(60, 2 ** attempt) }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-step.mjs","names":[],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/**
|
|
1
|
+
{"version":3,"file":"engine-step.mjs","names":[],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/aws/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_http2_client = require("../http2-client.cjs");
|
|
3
3
|
const require_aws_engine_step = require("./engine-step.cjs");
|
|
4
4
|
const require_aws_function_url = require("./function-url.cjs");
|
|
5
|
+
const require_engine_duration = require("../engine/duration.cjs");
|
|
5
6
|
const require_host_router = require("../host/router.cjs");
|
|
6
7
|
const require_host_execute = require("../host/execute.cjs");
|
|
7
8
|
const require_host_journey_host = require("../host/journey-host.cjs");
|
|
@@ -39,11 +40,18 @@ const json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
|
39
40
|
headers: { "content-type": "application/json" }
|
|
40
41
|
});
|
|
41
42
|
function journeyLambda(options) {
|
|
42
|
-
const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, ...hostOptions } = options;
|
|
43
|
+
const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, executionTimeoutMs, ...hostOptions } = options;
|
|
43
44
|
const host = require_host_journey_host.createJourneyHost({
|
|
44
45
|
...hostOptions,
|
|
45
46
|
store: (env, database) => options.store(env, database)
|
|
46
47
|
}, { apnsFetch: require_http2_client.http2Fetch() });
|
|
48
|
+
if (executionTimeoutMs !== void 0) {
|
|
49
|
+
const tooLong = host.bundle().workflows.filter((workflow) => require_engine_duration.longestJourneyMs(workflow) > executionTimeoutMs);
|
|
50
|
+
if (tooLong.length > 0) {
|
|
51
|
+
const days = (ms) => `${Math.ceil(ms / 864e5)} days`;
|
|
52
|
+
throw new RangeError(`${tooLong.map((w) => `'${w.id}' can run ${days(require_engine_duration.longestJourneyMs(w))}`).join(", ")}, longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
47
55
|
const lambda = lambdaOption ?? require_aws_lambda_api.sdkLambdaApi();
|
|
48
56
|
const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));
|
|
49
57
|
const scope = new node_async_hooks.AsyncLocalStorage();
|
package/dist/aws/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["createJourneyHost","http2Fetch","sdkLambdaApi","AsyncLocalStorage","AwsInstances","executeJourney","AwsEngineStep","toRequest","fromResponse","sweepSegments","withDurableExecution","isFunctionUrlEvent","isRunInput"],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport type { JourneyEnv } from '../cloudflare/bindings';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { executeJourney } from '../host/execute';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport { type SweepResult, sweepSegments } from '../host/router';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'WORKFLOW_KV' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, ...hostOptions } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const executions = store.executions;\n if (executions === undefined) {\n throw new Error(\n 'journeyLambda needs a store with an execution registry (PostgresJourneyStore)'\n );\n }\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n const sweep = (context: DurableContext): Promise<SweepResult> =>\n context.step(\n 'sweep',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n return sweepSegments(env, store);\n },\n { retryStrategy: () => ({ shouldRetry: false }) }\n );\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["createJourneyHost","http2Fetch","longestJourneyMs","sdkLambdaApi","AsyncLocalStorage","AwsInstances","executeJourney","AwsEngineStep","toRequest","fromResponse","sweepSegments","withDurableExecution","isFunctionUrlEvent","isRunInput"],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport type { JourneyEnv } from '../cloudflare/bindings';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { executeJourney } from '../host/execute';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport { type SweepResult, sweepSegments } from '../host/router';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'WORKFLOW_KV' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const executions = store.executions;\n if (executions === undefined) {\n throw new Error(\n 'journeyLambda needs a store with an execution registry (PostgresJourneyStore)'\n );\n }\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n const sweep = (context: DurableContext): Promise<SweepResult> =>\n context.step(\n 'sweep',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n return sweepSegments(env, store);\n },\n { retryStrategy: () => ({ shouldRetry: false }) }\n );\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
|
package/dist/aws/index.d.cts
CHANGED
|
@@ -46,6 +46,12 @@ export interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit
|
|
|
46
46
|
* timeout; 15 minutes (Lambda's maximum) by default.
|
|
47
47
|
*/
|
|
48
48
|
stopDrainMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that
|
|
51
|
+
* can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —
|
|
52
|
+
* instead of being stopped by the platform midway and recorded as failed.
|
|
53
|
+
*/
|
|
54
|
+
executionTimeoutMs?: number;
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
57
|
* The Lambda invocation context, as far as the handler's signature needs it — declared here so
|
package/dist/aws/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/aws/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/aws/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8Ca;;YAGD,mBAAmB,KAAK;iBAEnB,qBAAqB,YAAY,0BAA0B,KAC1E,mBAAmB,MAAM;;EAIzB,KAAK;;EAEL,QAAQ,KAAK,KAAK;IAAY;QAAqB,eAAe;;EAElE,SAAS;;EAET;;;;;;EAMA;;;;;;EAMA;;;;;;iBAOe;EACf;EACA;EACA;EACA;EACA;;iBAGe;;EAEf,UACE,OAAO,iCACP,SAAS,kBACN,QAAQ;;wBAeC,cAAc,YAAY,kBACxC,SAAS,qBAAqB,OAC7B"}
|
package/dist/aws/index.d.mts
CHANGED
|
@@ -46,6 +46,12 @@ export interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit
|
|
|
46
46
|
* timeout; 15 minutes (Lambda's maximum) by default.
|
|
47
47
|
*/
|
|
48
48
|
stopDrainMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that
|
|
51
|
+
* can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —
|
|
52
|
+
* instead of being stopped by the platform midway and recorded as failed.
|
|
53
|
+
*/
|
|
54
|
+
executionTimeoutMs?: number;
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
57
|
* The Lambda invocation context, as far as the handler's signature needs it — declared here so
|
package/dist/aws/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/aws/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/aws/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8Ca;;YAGD,mBAAmB,KAAK;iBAEnB,qBAAqB,YAAY,0BAA0B,KAC1E,mBAAmB,MAAM;;EAIzB,KAAK;;EAEL,QAAQ,KAAK,KAAK;IAAY;QAAqB,eAAe;;EAElE,SAAS;;EAET;;;;;;EAMA;;;;;;EAMA;;;;;;iBAOe;EACf;EACA;EACA;EACA;EACA;;iBAGe;;EAEf,UACE,OAAO,iCACP,SAAS,kBACN,QAAQ;;wBAeC,cAAc,YAAY,kBACxC,SAAS,qBAAqB,OAC7B"}
|
package/dist/aws/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { http2Fetch } from "../http2-client.mjs";
|
|
2
2
|
import { AwsEngineStep, stepRetry } from "./engine-step.mjs";
|
|
3
3
|
import { fromResponse, isFunctionUrlEvent, toRequest } from "./function-url.mjs";
|
|
4
|
+
import { longestJourneyMs } from "../engine/duration.mjs";
|
|
4
5
|
import { sweepSegments } from "../host/router.mjs";
|
|
5
6
|
import { executeJourney } from "../host/execute.mjs";
|
|
6
7
|
import { createJourneyHost } from "../host/journey-host.mjs";
|
|
@@ -38,11 +39,18 @@ const json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
|
38
39
|
headers: { "content-type": "application/json" }
|
|
39
40
|
});
|
|
40
41
|
function journeyLambda(options) {
|
|
41
|
-
const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, ...hostOptions } = options;
|
|
42
|
+
const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, executionTimeoutMs, ...hostOptions } = options;
|
|
42
43
|
const host = createJourneyHost({
|
|
43
44
|
...hostOptions,
|
|
44
45
|
store: (env, database) => options.store(env, database)
|
|
45
46
|
}, { apnsFetch: http2Fetch() });
|
|
47
|
+
if (executionTimeoutMs !== void 0) {
|
|
48
|
+
const tooLong = host.bundle().workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);
|
|
49
|
+
if (tooLong.length > 0) {
|
|
50
|
+
const days = (ms) => `${Math.ceil(ms / 864e5)} days`;
|
|
51
|
+
throw new RangeError(`${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(", ")}, longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
const lambda = lambdaOption ?? sdkLambdaApi();
|
|
47
55
|
const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));
|
|
48
56
|
const scope = new AsyncLocalStorage();
|
package/dist/aws/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport type { JourneyEnv } from '../cloudflare/bindings';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { executeJourney } from '../host/execute';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport { type SweepResult, sweepSegments } from '../host/router';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'WORKFLOW_KV' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const { env: baseEnv, lambda: lambdaOption, startRetryMs, stopDrainMs, ...hostOptions } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const executions = store.executions;\n if (executions === undefined) {\n throw new Error(\n 'journeyLambda needs a store with an execution registry (PostgresJourneyStore)'\n );\n }\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n const sweep = (context: DurableContext): Promise<SweepResult> =>\n context.step(\n 'sweep',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n return sweepSegments(env, store);\n },\n { retryStrategy: () => ({ shouldRetry: false }) }\n );\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport type { JourneyEnv } from '../cloudflare/bindings';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { executeJourney } from '../host/execute';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport { type SweepResult, sweepSegments } from '../host/router';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'WORKFLOW_KV' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const executions = store.executions;\n if (executions === undefined) {\n throw new Error(\n 'journeyLambda needs a store with an execution registry (PostgresJourneyStore)'\n );\n }\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n const sweep = (context: DurableContext): Promise<SweepResult> =>\n context.step(\n 'sweep',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n return sweepSegments(env, store);\n },\n { retryStrategy: () => ({ shouldRetry: false }) }\n );\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_runtime = require("../runtime.cjs");
|
|
3
|
+
const require_engine_retry = require("../engine/retry.cjs");
|
|
3
4
|
const require_cloudflare_bindings = require("./bindings.cjs");
|
|
4
5
|
const require_cloudflare_d1_env = require("./d1-env.cjs");
|
|
5
6
|
const require_host_execute = require("../host/execute.cjs");
|
|
@@ -15,8 +16,6 @@ let cloudflare_workflows = require("cloudflare:workflows");
|
|
|
15
16
|
* waitForEvent, which signals a timeout by throwing — and so does every other
|
|
16
17
|
* failure in there, so only the clock can tell them apart (see wakeExpired).
|
|
17
18
|
*/
|
|
18
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
19
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
20
19
|
var CfEngineStep = class {
|
|
21
20
|
step;
|
|
22
21
|
store;
|
|
@@ -29,8 +28,8 @@ var CfEngineStep = class {
|
|
|
29
28
|
this.instanceId = instanceId;
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
|
-
* A
|
|
33
|
-
*
|
|
31
|
+
* A failure a retry would only repeat (`isRetryable`: a template refusing its props, the
|
|
32
|
+
* contract refusing an emitted event, a provider rejecting the request) ends the step at
|
|
34
33
|
* once instead of burning the retry budget.
|
|
35
34
|
*/
|
|
36
35
|
do(name, fn) {
|
|
@@ -38,7 +37,7 @@ var CfEngineStep = class {
|
|
|
38
37
|
try {
|
|
39
38
|
return await fn();
|
|
40
39
|
} catch (error) {
|
|
41
|
-
if (error instanceof Error &&
|
|
40
|
+
if (error instanceof Error && !require_engine_retry.isRetryable(error)) throw new cloudflare_workflows.NonRetryableError(error.message, error.name);
|
|
42
41
|
throw error;
|
|
43
42
|
}
|
|
44
43
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.cjs","names":["NonRetryableError","WAKE_EVENT_TYPE","wakeExpired","WorkflowEntrypoint","d1Store","resolveRuntime","WebhookMessageSender","LogMessageSender","RegistryActionInvoker","executeJourney"],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock can tell them apart (see wakeExpired).\n */\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A template refusing its props, or the contract refusing an emitted event,\n * is deterministic — a retry hits the same refusal — so it ends the step at\n * once instead of burning the retry budget.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && NON_RETRYABLE.has(error.name)) {\n throw new NonRetryableError(error.message, error.name);\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 + KV from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.env,\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runner.cjs","names":["isRetryable","NonRetryableError","WAKE_EVENT_TYPE","wakeExpired","WorkflowEntrypoint","d1Store","resolveRuntime","WebhookMessageSender","LogMessageSender","RegistryActionInvoker","executeJourney"],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock can tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n throw new NonRetryableError(error.message, error.name);\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 + KV from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.env,\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runner.d.cts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAqGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;;;;;;;;;;;;YAaxC,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runner.d.mts","names":[],"sources":["../../src/cloudflare/runner.ts"],"mappings":";;;;;;;;;;;;;;;qBAqGa,sBAAsB,mBAAmB,YAAY;;;;;;;YAOtD,eAAe;;YAKT,WAAW,QAAQ,eAAe;;;;;;;;;;;;YAaxC,QAAQ,SAAS,gBAAgB;;;;;;YASjC,uBAAuB;;;;;;;;YAavB,oBAAoB;;;;;YAQpB,YAAY;;;;;;YASZ,0BAA0B;;;;;YAQ1B,eAAe,QAAQ,eAAe;;YAKtC,uBAAuB;;;;;YAQvB,eAAe,OAAO,cAAc,QAAQ,gBAAgB;EAahE,IAAI,OAAO,cAAc,gBAAgB,MAAM,eAAe,QAAQ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveRuntime } from "../runtime.mjs";
|
|
2
|
+
import { isRetryable } from "../engine/retry.mjs";
|
|
2
3
|
import { WAKE_EVENT_TYPE, wakeExpired } from "./bindings.mjs";
|
|
3
4
|
import { d1Store } from "./d1-env.mjs";
|
|
4
5
|
import { executeJourney } from "../host/execute.mjs";
|
|
@@ -14,8 +15,6 @@ import { NonRetryableError } from "cloudflare:workflows";
|
|
|
14
15
|
* waitForEvent, which signals a timeout by throwing — and so does every other
|
|
15
16
|
* failure in there, so only the clock can tell them apart (see wakeExpired).
|
|
16
17
|
*/
|
|
17
|
-
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
18
|
-
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
19
18
|
var CfEngineStep = class {
|
|
20
19
|
step;
|
|
21
20
|
store;
|
|
@@ -28,8 +27,8 @@ var CfEngineStep = class {
|
|
|
28
27
|
this.instanceId = instanceId;
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
|
-
* A
|
|
32
|
-
*
|
|
30
|
+
* A failure a retry would only repeat (`isRetryable`: a template refusing its props, the
|
|
31
|
+
* contract refusing an emitted event, a provider rejecting the request) ends the step at
|
|
33
32
|
* once instead of burning the retry budget.
|
|
34
33
|
*/
|
|
35
34
|
do(name, fn) {
|
|
@@ -37,7 +36,7 @@ var CfEngineStep = class {
|
|
|
37
36
|
try {
|
|
38
37
|
return await fn();
|
|
39
38
|
} catch (error) {
|
|
40
|
-
if (error instanceof Error &&
|
|
39
|
+
if (error instanceof Error && !isRetryable(error)) throw new NonRetryableError(error.message, error.name);
|
|
41
40
|
throw error;
|
|
42
41
|
}
|
|
43
42
|
});
|