@shware/workflow 3.1.0 → 3.3.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 +13 -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/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +2 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +2 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.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/email-sender.cjs +24 -0
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +18 -0
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +18 -0
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +24 -1
- package/dist/email-sender.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/ports.d.cts +6 -0
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -0
- package/dist/engine/ports.d.mts.map +1 -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/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +7 -2
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +3 -1
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +3 -1
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +7 -2
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +6 -1
- 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 +6 -1
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +9 -1
- 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 +9 -1
- 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,17 @@ 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.
|
|
526
|
+
|
|
527
|
+
An email with an unsubscribe link also carries `List-Unsubscribe` (RFC 2369), so
|
|
528
|
+
a mail client's own unsubscribe button works. With reporting on (a Postgres store and
|
|
529
|
+
`WORKFLOW_ORIGIN`), it names the copy's one-click endpoint `/t/u/<message>`, together with
|
|
530
|
+
`List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058). Gmail and Yahoo require
|
|
531
|
+
one-click unsubscribe for bulk senders. The provider's POST there unsubscribes at once, and the
|
|
532
|
+
same URL opened in a browser asks first. Without reporting, the header names the template's own
|
|
533
|
+
unsubscribe link, with no one-click claim. Only https URLs are named, so a local `http://`
|
|
534
|
+
origin sends no header.
|
|
525
535
|
|
|
526
536
|
An email step refuses to send when the profile has no `email`.
|
|
527
537
|
`GET /preview/<id>?props=<JSON>` renders a template, with the subject in the
|
|
@@ -816,7 +826,8 @@ Workflow:
|
|
|
816
826
|
Handler: aws.handler
|
|
817
827
|
Timeout: 60
|
|
818
828
|
AutoPublishAlias: live
|
|
819
|
-
# ExecutionTimeout: the longest journey (waits, delays and the goal window), up to 366 days
|
|
829
|
+
# ExecutionTimeout: the longest journey (waits, delays and the goal window), up to 366 days;
|
|
830
|
+
# pass the same value to journeyLambda({ executionTimeoutMs }) to check it at startup
|
|
820
831
|
DurableConfig: { ExecutionTimeout: 2592000, RetentionPeriodInDays: 30 }
|
|
821
832
|
FunctionUrlConfig: { AuthType: NONE, InvokeMode: BUFFERED }
|
|
822
833
|
```
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n /** The code version whose routes named `params.contentHash` (absent after a forced deploy). */\n version?: string | undefined;\n }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n /** The code version whose routes named `params.contentHash` (absent after a forced deploy). */\n version?: string | undefined;\n }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;;AAgJA,IAAa,wBAAb,cAA2C,MAAM;CAE1B;CADrB,OAAyB;CACzB,YAAY,YAA6B;EACvC,MAAM,qBAAqB,YAAY;EADpB,KAAA,aAAA;CAErB;AACF;;;;;AAMA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,iBAAiB,uBAAuB,OAAO;CACnD,OAAO,iCAAiC,KAAK,OAAO,KAAK,CAAC;AAC5D;;AAGA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
|
|
@@ -96,6 +96,8 @@ export interface EmailBindingLike {
|
|
|
96
96
|
replyTo?: EmailAddress;
|
|
97
97
|
subject: string;
|
|
98
98
|
html: string;
|
|
99
|
+
/** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */
|
|
100
|
+
headers?: Readonly<Record<string, string>>;
|
|
99
101
|
/** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */
|
|
100
102
|
idempotencyKey: string;
|
|
101
103
|
}): Promise<unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.d.cts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;iBAYiB;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,gBAAgB;;;iBAIlB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;iBAG/B;EACf;EACA,UAAU;IAAS;IAAc;MAAsB;;EAEvD,cAAc;;EAEd,WAAW;IAAU;;;;;;;iBAON;EACf,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAC1D,IAAI,aAAa,QAAQ;;;;;;;EAOzB,MAAM;IACJ;IACA,QAAQ;IACR;;IAEA;MACE,QAAQ;;EAEZ,QAAQ;IAAS;IAAkB;MAAmB;;;;;EAKtD,aAAa;;;;;;;;;;;;iBAaE;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA;MACE;;;iBAIW;;;;;EAKf,cAAc;;EAEd,KAAK;;EAEL,SAAS;;EAET;;;;;;EAMA;;;;;EAKA;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;EACA;;;;;;;EAOA;IAAwB;IAAY;IAAc;;;EAElD;;;qBAIW,8BAA8B;WAEpB;WADH;EAClB,YAAqB;;;;;;wBASP,mBAAmB;;qBAMtB;;iBAGI;EACf;;EAEA;EACA;EACA;IAAW;IAAe,SAAS;;GAClC;;;;;;;qBAQU;;;;;;;;;;wBAWG,YAAY,mBAAmB,mBAAmB"}
|
|
1
|
+
{"version":3,"file":"bindings.d.cts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;iBAYiB;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,gBAAgB;;;iBAIlB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;iBAG/B;EACf;EACA,UAAU;IAAS;IAAc;MAAsB;;EAEvD,cAAc;;EAEd,WAAW;IAAU;;;;;;;iBAON;EACf,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAC1D,IAAI,aAAa,QAAQ;;;;;;;EAOzB,MAAM;IACJ;IACA,QAAQ;IACR;;IAEA;MACE,QAAQ;;EAEZ,QAAQ;IAAS;IAAkB;MAAmB;;;;;EAKtD,aAAa;;;;;;;;;;;;iBAaE;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;iBAIW;;;;;EAKf,cAAc;;EAEd,KAAK;;EAEL,SAAS;;EAET;;;;;;EAMA;;;;;EAKA;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;EACA;;;;;;;EAOA;IAAwB;IAAY;IAAc;;;EAElD;;;qBAIW,8BAA8B;WAEpB;WADH;EAClB,YAAqB;;;;;;wBASP,mBAAmB;;qBAMtB;;iBAGI;EACf;;EAEA;EACA;EACA;IAAW;IAAe,SAAS;;GAClC;;;;;;;qBAQU;;;;;;;;;;wBAWG,YAAY,mBAAmB,mBAAmB"}
|
|
@@ -96,6 +96,8 @@ export interface EmailBindingLike {
|
|
|
96
96
|
replyTo?: EmailAddress;
|
|
97
97
|
subject: string;
|
|
98
98
|
html: string;
|
|
99
|
+
/** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */
|
|
100
|
+
headers?: Readonly<Record<string, string>>;
|
|
99
101
|
/** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */
|
|
100
102
|
idempotencyKey: string;
|
|
101
103
|
}): Promise<unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.d.mts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;iBAYiB;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,gBAAgB;;;iBAIlB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;iBAG/B;EACf;EACA,UAAU;IAAS;IAAc;MAAsB;;EAEvD,cAAc;;EAEd,WAAW;IAAU;;;;;;;iBAON;EACf,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAC1D,IAAI,aAAa,QAAQ;;;;;;;EAOzB,MAAM;IACJ;IACA,QAAQ;IACR;;IAEA;MACE,QAAQ;;EAEZ,QAAQ;IAAS;IAAkB;MAAmB;;;;;EAKtD,aAAa;;;;;;;;;;;;iBAaE;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA;MACE;;;iBAIW;;;;;EAKf,cAAc;;EAEd,KAAK;;EAEL,SAAS;;EAET;;;;;;EAMA;;;;;EAKA;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;EACA;;;;;;;EAOA;IAAwB;IAAY;IAAc;;;EAElD;;;qBAIW,8BAA8B;WAEpB;WADH;EAClB,YAAqB;;;;;;wBASP,mBAAmB;;qBAMtB;;iBAGI;EACf;;EAEA;EACA;EACA;IAAW;IAAe,SAAS;;GAClC;;;;;;;qBAQU;;;;;;;;;;wBAWG,YAAY,mBAAmB,mBAAmB"}
|
|
1
|
+
{"version":3,"file":"bindings.d.mts","names":[],"sources":["../../src/cloudflare/bindings.ts"],"mappings":";;;;;;;;;;;iBAYiB;EACf,IAAI,cAAc;EAClB,IAAI,aAAa,gBAAgB;;;iBAIlB;EACf;IAAS;;;iBAGM;EACf,QAAQ,oBAAoB;EAC5B,MAAM,IAAI,4BAA4B,QAAQ;EAC9C,IAAI,IAAI,4BAA4B;IAAU,SAAS;;EACvD,OAAO,QAAQ;;iBAGA;EACf,QAAQ,cAAc;;EAEtB,MAAM,YAAY,4BAA4B;;iBAG/B;EACf;EACA,UAAU;IAAS;IAAc;MAAsB;;EAEvD,cAAc;;EAEd,WAAW;IAAU;;;;;;;iBAON;EACf,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAC1D,IAAI,aAAa,QAAQ;;;;;;;EAOzB,MAAM;IACJ;IACA,QAAQ;IACR;;IAEA;MACE,QAAQ;;EAEZ,QAAQ;IAAS;IAAkB;MAAmB;;;;;EAKtD,aAAa;;;;;;;;;;;;iBAaE;EACf,KAAK;IACH,MAAM;IACN,IAAI;IACJ,UAAU;IACV;IACA;;IAEA,UAAU,SAAS;;IAEnB;MACE;;;iBAIW;;;;;EAKf,cAAc;;EAEd,KAAK;;EAEL,SAAS;;EAET;;;;;;EAMA;;;;;EAKA;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;EACA;;;;;;;EAOA;IAAwB;IAAY;IAAc;;;EAElD;;;qBAIW,8BAA8B;WAEpB;WADH;EAClB,YAAqB;;;;;;wBASP,mBAAmB;;qBAMtB;;iBAGI;EACf;;EAEA;EACA;EACA;IAAW;IAAe,SAAS;;GAClC;;;;;;;qBAQU;;;;;;;;;;wBAWG,YAAY,mBAAmB,mBAAmB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n /** The code version whose routes named `params.contentHash` (absent after a forced deploy). */\n version?: string | undefined;\n }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n /** The code version whose routes named `params.contentHash` (absent after a forced deploy). */\n version?: string | undefined;\n }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** List-Unsubscribe and List-Unsubscribe-Post, when the email has an unsubscribe link. */\n headers?: Readonly<Record<string, string>>;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;AAgJA,IAAa,wBAAb,cAA2C,MAAM;CAE1B;CADrB,OAAyB;CACzB,YAAY,YAA6B;EACvC,MAAM,qBAAqB,YAAY;EADpB,KAAA,aAAA;CAErB;AACF;;;;;AAMA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,iBAAiB,uBAAuB,OAAO;CACnD,OAAO,iCAAiC,KAAK,OAAO,KAAK,CAAC;AAC5D;;AAGA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
|