@shware/workflow 3.2.0 → 4.0.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 +100 -29
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +47 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +47 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +17 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +17 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +28 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.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/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +21 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +21 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +47 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +321 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +54 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +54 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +317 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +65 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +19 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +19 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +65 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.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 +19 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +13 -11
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +13 -11
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +19 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +3 -2
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.mjs +3 -2
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +52 -7
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +52 -8
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
package/dist/aws/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["createJourneyHost","http2Fetch","longestJourneyMs","sdkLambdaApi","AsyncLocalStorage","AwsInstances","readWithRetry","PermanentError","executeJourney","AwsEngineStep","toRequest","fromResponse","sweepMaintenance","sweepSegmentsPaged","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 { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\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' | '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 // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\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 /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return (await store.reporting?.snapshotSegments(Date.now())) ?? [];\n },\n once\n );\n return { users, started, sizes };\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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JourneyEnv } from "../
|
|
1
|
+
import { JourneyEnv } from "../host/platform.cjs";
|
|
2
2
|
import { JourneyStore, StoreLease } from "../store/index.cjs";
|
|
3
3
|
import { ExecutionState, LambdaApi, sdkLambdaApi } from "./lambda-api.cjs";
|
|
4
4
|
import { AwsEngineStep, stepRetry } from "./engine-step.cjs";
|
|
@@ -28,7 +28,7 @@ import { DurableExecutionInvocationInput, DurableExecutionInvocationOutput } fro
|
|
|
28
28
|
/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */
|
|
29
29
|
export declare const SWEEP_EVENT_TYPE = "$workflow.sweep";
|
|
30
30
|
/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */
|
|
31
|
-
export type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | '
|
|
31
|
+
export type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;
|
|
32
32
|
export interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<JourneyHostOptions<Env & JourneyEnv>, 'env' | 'store'> {
|
|
33
33
|
/** The validated environment (parsed from `process.env` with zod by the app). */
|
|
34
34
|
env: Env;
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAqDa;;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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JourneyEnv } from "../
|
|
1
|
+
import { JourneyEnv } from "../host/platform.mjs";
|
|
2
2
|
import { JourneyStore, StoreLease } from "../store/index.mjs";
|
|
3
3
|
import { ExecutionState, LambdaApi, sdkLambdaApi } from "./lambda-api.mjs";
|
|
4
4
|
import { AwsEngineStep, stepRetry } from "./engine-step.mjs";
|
|
@@ -28,7 +28,7 @@ import { DurableExecutionInvocationInput, DurableExecutionInvocationOutput } fro
|
|
|
28
28
|
/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */
|
|
29
29
|
export declare const SWEEP_EVENT_TYPE = "$workflow.sweep";
|
|
30
30
|
/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */
|
|
31
|
-
export type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | '
|
|
31
|
+
export type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;
|
|
32
32
|
export interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<JourneyHostOptions<Env & JourneyEnv>, 'env' | 'store'> {
|
|
33
33
|
/** The validated environment (parsed from `process.env` with zod by the app). */
|
|
34
34
|
env: Env;
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAqDa;;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,8 +1,10 @@
|
|
|
1
1
|
import { http2Fetch } from "../http2-client.mjs";
|
|
2
|
+
import { PermanentError } from "../engine/retry.mjs";
|
|
2
3
|
import { AwsEngineStep, stepRetry } from "./engine-step.mjs";
|
|
3
4
|
import { fromResponse, isFunctionUrlEvent, toRequest } from "./function-url.mjs";
|
|
4
5
|
import { longestJourneyMs } from "../engine/duration.mjs";
|
|
5
|
-
import {
|
|
6
|
+
import { readWithRetry } from "../host/fresh-read.mjs";
|
|
7
|
+
import { sweepMaintenance, sweepSegmentsPaged } from "../host/ingest.mjs";
|
|
6
8
|
import { executeJourney } from "../host/execute.mjs";
|
|
7
9
|
import { createJourneyHost } from "../host/journey-host.mjs";
|
|
8
10
|
import { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from "./instances.mjs";
|
|
@@ -99,6 +101,11 @@ function journeyLambda(options) {
|
|
|
99
101
|
const run = async (input, context) => {
|
|
100
102
|
const { store, env, executions } = open(context);
|
|
101
103
|
const { instanceId, params } = input;
|
|
104
|
+
const record = await readWithRetry(() => executions.get(instanceId), {
|
|
105
|
+
what: "run entry",
|
|
106
|
+
instanceId
|
|
107
|
+
});
|
|
108
|
+
if (record === void 0 || record.stoppedAt !== void 0 || record.input !== JSON.stringify(input)) throw new PermanentError("journeyLambda: run input is not a recorded start");
|
|
102
109
|
await context.step("$workflow:started", async () => {
|
|
103
110
|
await executions.markSubmitted(instanceId, context.executionContext.durableExecutionArn, Date.now());
|
|
104
111
|
});
|
|
@@ -127,11 +134,45 @@ function journeyLambda(options) {
|
|
|
127
134
|
if (method === "GET" || method === "HEAD") return serve();
|
|
128
135
|
return context.step("http", serve, { retryStrategy: () => ({ shouldRetry: false }) });
|
|
129
136
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The sweep as durable steps, one per page of users: an invocation cut short (the function's
|
|
139
|
+
* timeout) is re-invoked, and the re-invocation replays the pages already checkpointed
|
|
140
|
+
* instead of evaluating everyone again. No step retries on its own — a page that failed is
|
|
141
|
+
* evaluated again by the next tick.
|
|
142
|
+
*/
|
|
143
|
+
const sweep = async (context) => {
|
|
144
|
+
const once = { retryStrategy: () => ({ shouldRetry: false }) };
|
|
145
|
+
await context.step("sweep:maintain", async () => {
|
|
146
|
+
const { store, env } = open(context);
|
|
147
|
+
await host.applyOwnBundle(env, bundleVersion());
|
|
148
|
+
await sweepMaintenance(env, store);
|
|
149
|
+
}, once);
|
|
150
|
+
let users = 0;
|
|
151
|
+
const started = [];
|
|
152
|
+
let after;
|
|
153
|
+
for (let n = 0;; n++) {
|
|
154
|
+
const page = await context.step(`sweep:page:${n}`, async () => {
|
|
155
|
+
const { store, env } = open(context);
|
|
156
|
+
return sweepSegmentsPaged(env, store, {
|
|
157
|
+
after,
|
|
158
|
+
limit: 500
|
|
159
|
+
});
|
|
160
|
+
}, once);
|
|
161
|
+
users += page.users;
|
|
162
|
+
started.push(...page.started);
|
|
163
|
+
if (!page.more || page.lastUserId === void 0) break;
|
|
164
|
+
after = page.lastUserId;
|
|
165
|
+
}
|
|
166
|
+
const sizes = await context.step("sweep:snapshot", async () => {
|
|
167
|
+
const { store } = open(context);
|
|
168
|
+
return await store.reporting?.snapshotSegments(Date.now()) ?? [];
|
|
169
|
+
}, once);
|
|
170
|
+
return {
|
|
171
|
+
users,
|
|
172
|
+
started,
|
|
173
|
+
sizes
|
|
174
|
+
};
|
|
175
|
+
};
|
|
135
176
|
const durable = withDurableExecution(async (input, context) => {
|
|
136
177
|
if (isFunctionUrlEvent(input)) return http(input, context);
|
|
137
178
|
if (isRunInput(input)) return run(input, context);
|
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 { 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
|
+
{"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 { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\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' | '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 // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\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 /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return (await store.reporting?.snapshotSegments(Date.now())) ?? [];\n },\n once\n );\n return { users, started, sizes };\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/instances.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_engine_bucket = require("../engine/bucket.cjs");
|
|
3
|
-
const
|
|
3
|
+
const require_host_platform = require("../host/platform.cjs");
|
|
4
4
|
//#region src/aws/instances.ts
|
|
5
5
|
/**
|
|
6
6
|
* The router's instance port over Lambda durable executions: an instance is one execution of
|
|
@@ -65,7 +65,7 @@ var AwsInstances = class {
|
|
|
65
65
|
return this.instance(options.id);
|
|
66
66
|
}
|
|
67
67
|
async get(id) {
|
|
68
|
-
if (await this.options.executions.get(id) === void 0) throw new
|
|
68
|
+
if (await this.options.executions.get(id) === void 0) throw new require_host_platform.InstanceNotFoundError(id);
|
|
69
69
|
return this.instance(id);
|
|
70
70
|
}
|
|
71
71
|
async resume(entry) {
|
|
@@ -124,10 +124,29 @@ var AwsInstances = class {
|
|
|
124
124
|
draining(record) {
|
|
125
125
|
return record.stoppedAt !== void 0 && this.now() - record.stoppedAt < this.stopDrainMs;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The one judgment every path makes of an execution the platform reports stopped — by
|
|
129
|
+
* `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first
|
|
130
|
+
* sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the
|
|
131
|
+
* drain), and the record is read again so the verdict rests on what is committed now, not on
|
|
132
|
+
* the snapshot a caller read before its API calls. A write or read that fails throws: an
|
|
133
|
+
* unknown stop time is never taken for "safely stopped". Undefined means the record is gone —
|
|
134
|
+
* the erasure finished with it meanwhile.
|
|
135
|
+
*/
|
|
136
|
+
async observeStop(instanceId) {
|
|
137
|
+
await this.options.executions.markStopped(instanceId, this.now());
|
|
138
|
+
return this.options.executions.get(instanceId);
|
|
139
|
+
}
|
|
127
140
|
async reconcile(record, now) {
|
|
128
141
|
const arn = await this.arnOf(record);
|
|
129
142
|
const state = arn === void 0 ? void 0 : await this.options.lambda.executionState(arn);
|
|
130
143
|
if (state === "failed" || state === "stopped" || state === "timed_out" || state === void 0 && this.vanished(record)) {
|
|
144
|
+
const fresh = state === "stopped" ? await this.observeStop(record.instanceId) : await this.options.executions.get(record.instanceId);
|
|
145
|
+
if (fresh === void 0) return;
|
|
146
|
+
if (this.draining(fresh)) {
|
|
147
|
+
await this.options.executions.markChecked(record.instanceId, now);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
131
150
|
const { store } = this.options;
|
|
132
151
|
await store.setEntryStatus(record.instanceId, "failed");
|
|
133
152
|
await store.reporting?.instanceFinished({
|
|
@@ -145,7 +164,7 @@ var AwsInstances = class {
|
|
|
145
164
|
const { executions, lambda } = this.options;
|
|
146
165
|
const current = async () => {
|
|
147
166
|
const record = await executions.get(id);
|
|
148
|
-
if (record === void 0) throw new
|
|
167
|
+
if (record === void 0) throw new require_host_platform.InstanceNotFoundError(id);
|
|
149
168
|
return record;
|
|
150
169
|
};
|
|
151
170
|
return {
|
|
@@ -169,9 +188,9 @@ var AwsInstances = class {
|
|
|
169
188
|
}
|
|
170
189
|
const state = await lambda.executionState(arn);
|
|
171
190
|
if (state === "succeeded" || state === "failed" || state === "timed_out") return;
|
|
172
|
-
if (!await lambda.stopExecution(arn)) throw new Error(`stopping ${id} failed`);
|
|
173
|
-
await
|
|
174
|
-
throw new Error(`${id} is stopped; its running invocation may still be finishing`);
|
|
191
|
+
if (state !== "stopped" && !await lambda.stopExecution(arn)) throw new Error(`stopping ${id} failed`);
|
|
192
|
+
const fresh = await this.observeStop(id);
|
|
193
|
+
if (fresh !== void 0 && this.draining(fresh)) throw new Error(`${id} is stopped; its running invocation may still be finishing`);
|
|
175
194
|
},
|
|
176
195
|
status: async () => {
|
|
177
196
|
const record = await current();
|
|
@@ -182,6 +201,10 @@ var AwsInstances = class {
|
|
|
182
201
|
if (state === "succeeded") return { status: "complete" };
|
|
183
202
|
if (state === "failed") return { status: "errored" };
|
|
184
203
|
if (state === "running") return { status: "running" };
|
|
204
|
+
if (state === "stopped") {
|
|
205
|
+
const fresh = await this.observeStop(id);
|
|
206
|
+
if (fresh !== void 0 && this.draining(fresh)) return { status: "running" };
|
|
207
|
+
}
|
|
185
208
|
return { status: "terminated" };
|
|
186
209
|
}
|
|
187
210
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instances.cjs","names":["murmur3","InstanceNotFoundError"],"sources":["../../src/aws/instances.ts"],"sourcesContent":["import {\n InstanceNotFoundError,\n type JourneyParams,\n type WorkflowBindingLike,\n type WorkflowInstanceLike,\n} from '../cloudflare/bindings';\nimport { murmur3 } from '../engine/bucket';\nimport type {\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyStore,\n} from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * The router's instance port over Lambda durable executions: an instance is one execution of\n * this same function, started by an asynchronous self-invoke under a stable execution name;\n * a wake completes the callback its current wait parks on; stopping stops the execution.\n */\n\n/** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */\nexport const RUN_EVENT_TYPE = '$workflow.run';\n\nexport interface RunInput {\n type: typeof RUN_EVENT_TYPE;\n instanceId: string;\n params: JourneyParams;\n /** When the instance was created (as Cloudflare's event timestamp): its entry anchor, fixed with the start. */\n enteredAtMs: number;\n}\n\nexport function isRunInput(event: unknown): event is RunInput {\n if (typeof event !== 'object' || event === null) return false;\n const candidate = event as {\n type?: unknown;\n instanceId?: unknown;\n enteredAtMs?: unknown;\n params?: unknown;\n requestContext?: unknown;\n };\n return (\n candidate.type === RUN_EVENT_TYPE &&\n candidate.requestContext === undefined &&\n typeof candidate.instanceId === 'string' &&\n typeof candidate.enteredAtMs === 'number' &&\n typeof candidate.params === 'object' &&\n candidate.params !== null\n );\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * The durable execution name for one start of an instance: the instance id, cut to fit, with a\n * fingerprint of the whole id (so a cut never makes two instances collide) and the attempt's\n * token. Every retry of that start reuses the recorded name; a new attempt gets a new token.\n */\nexport function executionName(instanceId: string, attempt: string): string {\n const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, '-');\n const token = attempt.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);\n const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 0x9747b28c).toString(36)}`;\n const suffix = `-${fingerprint}-${token}`;\n return `${safe.slice(0, 64 - suffix.length)}${suffix}`;\n}\n\nexport interface AwsInstancesOptions {\n store: JourneyStore;\n executions: ExecutionRegistry;\n lambda: LambdaApi;\n /**\n * The qualified function ARN an execution pinned to `contentHash` must run on; `version` is\n * the code version whose routes named it.\n */\n target(contentHash: string, version?: string): Promise<string>;\n /**\n * How long an unconfirmed start keeps being retried. It must stay shorter than the durable\n * execution retention: past that the platform forgets the name, and a retry would start a\n * second execution. Twelve hours by default.\n */\n startRetryMs?: number;\n /** How stale a running instance's last check must be before maintenance looks again. */\n reconcileEveryMs?: number;\n /**\n * How long after a stop an invocation already running may still act: Lambda cannot interrupt\n * one, and it runs on to its next checkpoint. An erasure waits this long before it erases.\n * The function's timeout is enough; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n now?: () => number;\n}\n\nconst MINUTE = 60_000;\n\nexport class AwsInstances implements WorkflowBindingLike {\n private readonly now: () => number;\n private readonly startRetryMs: number;\n private readonly reconcileEveryMs: number;\n private readonly stopDrainMs: number;\n\n constructor(private readonly options: AwsInstancesOptions) {\n this.now = options.now ?? Date.now;\n this.startRetryMs = options.startRetryMs ?? 12 * 60 * MINUTE;\n this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;\n this.stopDrainMs = options.stopDrainMs ?? 15 * MINUTE;\n }\n\n /**\n * A start, planned for one entry attempt. Its name is new every time: a failed entry reclaimed\n * under the same instance id (an event replayed with its ts) must start a new execution — the\n * old name would return the closed one. Retries of this attempt reuse the recorded name.\n */\n async plan(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n version?: string | undefined;\n }): Promise<ExecutionStart> {\n const input: RunInput = {\n type: RUN_EVENT_TYPE,\n instanceId: options.id,\n params: options.params,\n // The moment the instance is created, like Cloudflare's: an event's own ts may be long past\n enteredAtMs: this.now(),\n };\n return {\n name: executionName(options.id, crypto.randomUUID().slice(0, 8)),\n target: await this.options.target(options.params.contentHash, options.version),\n input: JSON.stringify(input),\n };\n }\n\n async create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike> {\n const record = await this.options.executions.get(options.id);\n if (record === undefined) {\n throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);\n }\n await this.submit(record);\n return this.instance(options.id);\n }\n\n async get(id: string): Promise<WorkflowInstanceLike> {\n // A read of the registry alone: a Lambda failure must never read as \"no such instance\"\n if ((await this.options.executions.get(id)) === undefined) {\n throw new InstanceNotFoundError(id);\n }\n return this.instance(id);\n }\n\n async resume(entry: { workflow: string; userId: string }): Promise<void> {\n for (const record of await this.options.executions.pendingStarts({ ...entry, limit: 1 })) {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n }\n }\n\n async maintain(): Promise<void> {\n const now = this.now();\n const failures: unknown[] = [];\n // Starts whose submit never confirmed: retried with the recorded name and payload\n for (const record of await this.options.executions.pendingStarts({\n createdBefore: now - MINUTE,\n limit: 25,\n })) {\n try {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n } catch (error) {\n failures.push(error);\n }\n }\n // Executions the platform ended without the run finalizing (a timeout, a stop, a crash)\n for (const record of await this.options.executions.toReconcile(\n 25,\n now - this.reconcileEveryMs\n )) {\n try {\n await this.reconcile(record, now);\n } catch (error) {\n failures.push(error);\n }\n }\n if (failures.length > 0) throw failures[0];\n }\n\n private async submit(record: ExecutionRecord): Promise<void> {\n const { arn } = await this.options.lambda.startExecution({\n target: record.target,\n name: record.name,\n payload: record.input,\n });\n await this.options.executions.markSubmitted(record.instanceId, arn, this.now());\n }\n\n /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */\n private async giveUpIfStale(record: ExecutionRecord): Promise<boolean> {\n if (this.now() - record.createdAt <= this.startRetryMs) return false;\n await this.options.executions.markUnresolved(record.instanceId, this.now());\n console.error(\n `[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`\n );\n return true;\n }\n\n private async arnOf(record: ExecutionRecord): Promise<string | undefined> {\n if (record.arn !== undefined) return record.arn;\n const arn = await this.options.lambda.findExecution(record.target, record.name);\n if (arn !== undefined) await this.options.executions.setArn(record.instanceId, arn);\n return arn;\n }\n\n /**\n * Past the start retry budget, an execution still nowhere to be seen never started: any queued\n * invoke has expired by then. Before that, not finding it says nothing.\n */\n private vanished(record: ExecutionRecord): boolean {\n return this.now() - record.createdAt > this.startRetryMs;\n }\n\n /** Stopped so recently that an invocation running at the time may still be acting. */\n private draining(record: ExecutionRecord): boolean {\n return record.stoppedAt !== undefined && this.now() - record.stoppedAt < this.stopDrainMs;\n }\n\n private async reconcile(record: ExecutionRecord, now: number): Promise<void> {\n const arn = await this.arnOf(record);\n const state = arn === undefined ? undefined : await this.options.lambda.executionState(arn);\n // Only a state that will never run another step — or one missing for good — ends the entry;\n // a run still queued, or not visible yet, is left alone\n const ended =\n state === 'failed' ||\n state === 'stopped' ||\n state === 'timed_out' ||\n (state === undefined && this.vanished(record));\n if (ended) {\n // Ended without its finalize step: the ledger must not show it running forever\n const { store } = this.options;\n await store.setEntryStatus(record.instanceId, 'failed');\n await store.reporting?.instanceFinished({\n workflow: record.workflow,\n instanceId: record.instanceId,\n userId: record.userId,\n status: 'failed',\n at: now,\n });\n console.warn(\n `[workflow] ${record.instanceId} ended on the platform (${state ?? 'unknown'}) without finishing: recorded as failed`\n );\n }\n await this.options.executions.markChecked(record.instanceId, now);\n }\n\n private instance(id: string): WorkflowInstanceLike {\n const { executions, lambda } = this.options;\n const current = async () => {\n const record = await executions.get(id);\n if (record === undefined) throw new InstanceNotFoundError(id);\n return record;\n };\n return {\n id,\n sendEvent: async (event) => {\n // The wait parked now, read at the moment of the wake: a newer wait's callback replaces\n // an older one, and a wake for none (the instance is between waits) is nothing to do —\n // its next wait re-reads the facts before parking\n const { callbackId } = await current();\n if (callbackId === undefined) return;\n await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));\n },\n terminate: async () => {\n const record = await current();\n // Stopped before: done once an invocation that was running then has had time to end\n if (record.stoppedAt !== undefined) {\n if (this.draining(record)) {\n throw new Error(`${id} was stopped; its running invocation may still be finishing`);\n }\n return;\n }\n if (record.submittedAt === undefined && record.unresolvedAt === undefined) {\n // No retry may start it after this\n await executions.markUnresolved(id, this.now());\n }\n const arn = await this.arnOf(record);\n if (arn === undefined) {\n // Not visible yet is not stopped: the erasure stays pending, and the sweep retries\n if (this.vanished(record)) return;\n throw new Error(`cannot stop ${id} yet: its execution is not visible`);\n }\n // One that finished on its own has no invocation left to drain\n const state = await lambda.executionState(arn);\n if (state === 'succeeded' || state === 'failed' || state === 'timed_out') return;\n if (!(await lambda.stopExecution(arn))) throw new Error(`stopping ${id} failed`);\n // A stop does not interrupt an invocation already running: it goes on to its next\n // checkpoint, side effects included. The erasure waits (see stopDrainMs)\n await executions.markStopped(id, this.now());\n throw new Error(`${id} is stopped; its running invocation may still be finishing`);\n },\n status: async () => {\n const record = await current();\n if (this.draining(record)) return { status: 'running' };\n const arn = await this.arnOf(record);\n // Not visible yet may still run: only one missing past the budget is gone\n if (arn === undefined) return { status: this.vanished(record) ? 'terminated' : 'running' };\n const state = await lambda.executionState(arn);\n // In the router's vocabulary (Cloudflare's): what will never run another step\n if (state === 'succeeded') return { status: 'complete' };\n if (state === 'failed') return { status: 'errored' };\n if (state === 'running') return { status: 'running' };\n return { status: 'terminated' };\n },\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"instances.cjs","names":["murmur3","InstanceNotFoundError"],"sources":["../../src/aws/instances.ts"],"sourcesContent":["import { murmur3 } from '../engine/bucket';\nimport {\n InstanceNotFoundError,\n type JourneyParams,\n type WorkflowBindingLike,\n type WorkflowInstanceLike,\n} from '../host/platform';\nimport type {\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyStore,\n} from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * The router's instance port over Lambda durable executions: an instance is one execution of\n * this same function, started by an asynchronous self-invoke under a stable execution name;\n * a wake completes the callback its current wait parks on; stopping stops the execution.\n */\n\n/** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */\nexport const RUN_EVENT_TYPE = '$workflow.run';\n\nexport interface RunInput {\n type: typeof RUN_EVENT_TYPE;\n instanceId: string;\n params: JourneyParams;\n /** When the instance was created (as Cloudflare's event timestamp): its entry anchor, fixed with the start. */\n enteredAtMs: number;\n}\n\nexport function isRunInput(event: unknown): event is RunInput {\n if (typeof event !== 'object' || event === null) return false;\n const candidate = event as {\n type?: unknown;\n instanceId?: unknown;\n enteredAtMs?: unknown;\n params?: unknown;\n requestContext?: unknown;\n };\n return (\n candidate.type === RUN_EVENT_TYPE &&\n candidate.requestContext === undefined &&\n typeof candidate.instanceId === 'string' &&\n typeof candidate.enteredAtMs === 'number' &&\n typeof candidate.params === 'object' &&\n candidate.params !== null\n );\n}\n\nconst utf8 = new TextEncoder();\n\n/**\n * The durable execution name for one start of an instance: the instance id, cut to fit, with a\n * fingerprint of the whole id (so a cut never makes two instances collide) and the attempt's\n * token. Every retry of that start reuses the recorded name; a new attempt gets a new token.\n */\nexport function executionName(instanceId: string, attempt: string): string {\n const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, '-');\n const token = attempt.replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);\n const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 0x9747b28c).toString(36)}`;\n const suffix = `-${fingerprint}-${token}`;\n return `${safe.slice(0, 64 - suffix.length)}${suffix}`;\n}\n\nexport interface AwsInstancesOptions {\n store: JourneyStore;\n executions: ExecutionRegistry;\n lambda: LambdaApi;\n /**\n * The qualified function ARN an execution pinned to `contentHash` must run on; `version` is\n * the code version whose routes named it.\n */\n target(contentHash: string, version?: string): Promise<string>;\n /**\n * How long an unconfirmed start keeps being retried. It must stay shorter than the durable\n * execution retention: past that the platform forgets the name, and a retry would start a\n * second execution. Twelve hours by default.\n */\n startRetryMs?: number;\n /** How stale a running instance's last check must be before maintenance looks again. */\n reconcileEveryMs?: number;\n /**\n * How long after a stop an invocation already running may still act: Lambda cannot interrupt\n * one, and it runs on to its next checkpoint. An erasure waits this long before it erases.\n * The function's timeout is enough; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n now?: () => number;\n}\n\nconst MINUTE = 60_000;\n\nexport class AwsInstances implements WorkflowBindingLike {\n private readonly now: () => number;\n private readonly startRetryMs: number;\n private readonly reconcileEveryMs: number;\n private readonly stopDrainMs: number;\n\n constructor(private readonly options: AwsInstancesOptions) {\n this.now = options.now ?? Date.now;\n this.startRetryMs = options.startRetryMs ?? 12 * 60 * MINUTE;\n this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;\n this.stopDrainMs = options.stopDrainMs ?? 15 * MINUTE;\n }\n\n /**\n * A start, planned for one entry attempt. Its name is new every time: a failed entry reclaimed\n * under the same instance id (an event replayed with its ts) must start a new execution — the\n * old name would return the closed one. Retries of this attempt reuse the recorded name.\n */\n async plan(options: {\n id: string;\n params: JourneyParams;\n ts: number;\n version?: string | undefined;\n }): Promise<ExecutionStart> {\n const input: RunInput = {\n type: RUN_EVENT_TYPE,\n instanceId: options.id,\n params: options.params,\n // The moment the instance is created, like Cloudflare's: an event's own ts may be long past\n enteredAtMs: this.now(),\n };\n return {\n name: executionName(options.id, crypto.randomUUID().slice(0, 8)),\n target: await this.options.target(options.params.contentHash, options.version),\n input: JSON.stringify(input),\n };\n }\n\n async create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike> {\n const record = await this.options.executions.get(options.id);\n if (record === undefined) {\n throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);\n }\n await this.submit(record);\n return this.instance(options.id);\n }\n\n async get(id: string): Promise<WorkflowInstanceLike> {\n // A read of the registry alone: a Lambda failure must never read as \"no such instance\"\n if ((await this.options.executions.get(id)) === undefined) {\n throw new InstanceNotFoundError(id);\n }\n return this.instance(id);\n }\n\n async resume(entry: { workflow: string; userId: string }): Promise<void> {\n for (const record of await this.options.executions.pendingStarts({ ...entry, limit: 1 })) {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n }\n }\n\n async maintain(): Promise<void> {\n const now = this.now();\n const failures: unknown[] = [];\n // Starts whose submit never confirmed: retried with the recorded name and payload\n for (const record of await this.options.executions.pendingStarts({\n createdBefore: now - MINUTE,\n limit: 25,\n })) {\n try {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n } catch (error) {\n failures.push(error);\n }\n }\n // Executions the platform ended without the run finalizing (a timeout, a stop, a crash)\n for (const record of await this.options.executions.toReconcile(\n 25,\n now - this.reconcileEveryMs\n )) {\n try {\n await this.reconcile(record, now);\n } catch (error) {\n failures.push(error);\n }\n }\n if (failures.length > 0) throw failures[0];\n }\n\n private async submit(record: ExecutionRecord): Promise<void> {\n const { arn } = await this.options.lambda.startExecution({\n target: record.target,\n name: record.name,\n payload: record.input,\n });\n await this.options.executions.markSubmitted(record.instanceId, arn, this.now());\n }\n\n /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */\n private async giveUpIfStale(record: ExecutionRecord): Promise<boolean> {\n if (this.now() - record.createdAt <= this.startRetryMs) return false;\n await this.options.executions.markUnresolved(record.instanceId, this.now());\n console.error(\n `[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`\n );\n return true;\n }\n\n private async arnOf(record: ExecutionRecord): Promise<string | undefined> {\n if (record.arn !== undefined) return record.arn;\n const arn = await this.options.lambda.findExecution(record.target, record.name);\n if (arn !== undefined) await this.options.executions.setArn(record.instanceId, arn);\n return arn;\n }\n\n /**\n * Past the start retry budget, an execution still nowhere to be seen never started: any queued\n * invoke has expired by then. Before that, not finding it says nothing.\n */\n private vanished(record: ExecutionRecord): boolean {\n return this.now() - record.createdAt > this.startRetryMs;\n }\n\n /** Stopped so recently that an invocation running at the time may still be acting. */\n private draining(record: ExecutionRecord): boolean {\n return record.stoppedAt !== undefined && this.now() - record.stoppedAt < this.stopDrainMs;\n }\n\n /**\n * The one judgment every path makes of an execution the platform reports stopped — by\n * `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first\n * sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the\n * drain), and the record is read again so the verdict rests on what is committed now, not on\n * the snapshot a caller read before its API calls. A write or read that fails throws: an\n * unknown stop time is never taken for \"safely stopped\". Undefined means the record is gone —\n * the erasure finished with it meanwhile.\n */\n private async observeStop(instanceId: string): Promise<ExecutionRecord | undefined> {\n await this.options.executions.markStopped(instanceId, this.now());\n return this.options.executions.get(instanceId);\n }\n\n private async reconcile(record: ExecutionRecord, now: number): Promise<void> {\n const arn = await this.arnOf(record);\n const state = arn === undefined ? undefined : await this.options.lambda.executionState(arn);\n // Only a state that will never run another step — or one missing for good — ends the entry;\n // a run still queued, or not visible yet, is left alone\n const ended =\n state === 'failed' ||\n state === 'stopped' ||\n state === 'timed_out' ||\n (state === undefined && this.vanished(record));\n if (ended) {\n // The verdict is taken on the record as it is now: `toReconcile` handed over a snapshot,\n // and a stop may have been recorded since — by `terminate`, or here, at its first sighting\n const fresh =\n state === 'stopped'\n ? await this.observeStop(record.instanceId)\n : await this.options.executions.get(record.instanceId);\n if (fresh === undefined) return;\n if (this.draining(fresh)) {\n // Stopped, but an invocation running at the stop may still be acting: the entry stays\n // running until the drain has passed, so an erasure keeps waiting for it (`status`\n // says running too), and the next look finalizes it\n await this.options.executions.markChecked(record.instanceId, now);\n return;\n }\n // Ended without its finalize step: the ledger must not show it running forever\n const { store } = this.options;\n await store.setEntryStatus(record.instanceId, 'failed');\n await store.reporting?.instanceFinished({\n workflow: record.workflow,\n instanceId: record.instanceId,\n userId: record.userId,\n status: 'failed',\n at: now,\n });\n console.warn(\n `[workflow] ${record.instanceId} ended on the platform (${state ?? 'unknown'}) without finishing: recorded as failed`\n );\n }\n await this.options.executions.markChecked(record.instanceId, now);\n }\n\n private instance(id: string): WorkflowInstanceLike {\n const { executions, lambda } = this.options;\n const current = async () => {\n const record = await executions.get(id);\n if (record === undefined) throw new InstanceNotFoundError(id);\n return record;\n };\n return {\n id,\n sendEvent: async (event) => {\n // The wait parked now, read at the moment of the wake: a newer wait's callback replaces\n // an older one, and a wake for none (the instance is between waits) is nothing to do —\n // its next wait re-reads the facts before parking\n const { callbackId } = await current();\n if (callbackId === undefined) return;\n await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));\n },\n terminate: async () => {\n const record = await current();\n // Stopped before: done once an invocation that was running then has had time to end\n if (record.stoppedAt !== undefined) {\n if (this.draining(record)) {\n throw new Error(`${id} was stopped; its running invocation may still be finishing`);\n }\n return;\n }\n if (record.submittedAt === undefined && record.unresolvedAt === undefined) {\n // No retry may start it after this\n await executions.markUnresolved(id, this.now());\n }\n const arn = await this.arnOf(record);\n if (arn === undefined) {\n // Not visible yet is not stopped: the erasure stays pending, and the sweep retries\n if (this.vanished(record)) return;\n throw new Error(`cannot stop ${id} yet: its execution is not visible`);\n }\n // One that finished on its own has no invocation left to drain\n const state = await lambda.executionState(arn);\n if (state === 'succeeded' || state === 'failed' || state === 'timed_out') return;\n // A stop does not interrupt an invocation already running: it goes on to its next\n // checkpoint, side effects included. The erasure waits (see stopDrainMs). One already\n // stopped (by an operator, or a stop whose record did not land) is not stopped again:\n // its first sighting is recorded and the same drain judged\n if (state !== 'stopped' && !(await lambda.stopExecution(arn))) {\n throw new Error(`stopping ${id} failed`);\n }\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) {\n throw new Error(`${id} is stopped; its running invocation may still be finishing`);\n }\n },\n status: async () => {\n const record = await current();\n if (this.draining(record)) return { status: 'running' };\n const arn = await this.arnOf(record);\n // Not visible yet may still run: only one missing past the budget is gone\n if (arn === undefined) return { status: this.vanished(record) ? 'terminated' : 'running' };\n const state = await lambda.executionState(arn);\n // In the router's vocabulary (Cloudflare's): what will never run another step\n if (state === 'succeeded') return { status: 'complete' };\n if (state === 'failed') return { status: 'errored' };\n if (state === 'running') return { status: 'running' };\n if (state === 'stopped') {\n // Stopped, seen here first or recorded since this read: the drain is the judgment\n const fresh = await this.observeStop(id);\n if (fresh !== undefined && this.draining(fresh)) return { status: 'running' };\n }\n return { status: 'terminated' };\n },\n };\n }\n}\n"],"mappings":"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"}
|
package/dist/aws/instances.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JourneyParams, WorkflowBindingLike, WorkflowInstanceLike } from "../
|
|
1
|
+
import { JourneyParams, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.cjs";
|
|
2
2
|
import { ExecutionRegistry, ExecutionStart, JourneyStore } from "../store/index.cjs";
|
|
3
3
|
import { LambdaApi } from "./lambda-api.cjs";
|
|
4
4
|
//#region src/aws/instances.d.ts
|
|
@@ -87,6 +87,16 @@ export declare class AwsInstances implements WorkflowBindingLike {
|
|
|
87
87
|
private vanished;
|
|
88
88
|
/** Stopped so recently that an invocation running at the time may still be acting. */
|
|
89
89
|
private draining;
|
|
90
|
+
/**
|
|
91
|
+
* The one judgment every path makes of an execution the platform reports stopped — by
|
|
92
|
+
* `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first
|
|
93
|
+
* sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the
|
|
94
|
+
* drain), and the record is read again so the verdict rests on what is committed now, not on
|
|
95
|
+
* the snapshot a caller read before its API calls. A write or read that fails throws: an
|
|
96
|
+
* unknown stop time is never taken for "safely stopped". Undefined means the record is gone —
|
|
97
|
+
* the erasure finished with it meanwhile.
|
|
98
|
+
*/
|
|
99
|
+
private observeStop;
|
|
90
100
|
private reconcile;
|
|
91
101
|
private instance;
|
|
92
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instances.d.cts","names":[],"sources":["../../src/aws/instances.ts"],"mappings":";;;;;;;;;;qBAsBa;iBAEI;EACf,aAAa;EACb;EACA,QAAQ;;EAER;;wBAGc,WAAW,iBAAiB,SAAS;;;;;;wBA0BrC,cAAc,oBAAoB;iBAQjC;EACf,OAAO;EACP,YAAY;EACZ,QAAQ;;;;;EAKR,OAAO,qBAAqB,mBAAmB;;;;;;EAM/C;;EAEA;;;;;;EAMA;EACA;;qBAKW,wBAAwB;mBAMN;mBALZ;mBACA;mBACA;mBACA;EAEjB,YAA6B,SAAS;;;;;;EAYhC,KAAK;IACT;IACA,QAAQ;IACR;IACA;MACE,QAAQ;EAeN,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAS1D,IAAI,aAAa,QAAQ;EAQzB,OAAO;IAAS;IAAkB;MAAmB;EAMrD,YAAY;UA4BJ;;UAUA;UASA;;;;;UAWN;;UAKA;
|
|
1
|
+
{"version":3,"file":"instances.d.cts","names":[],"sources":["../../src/aws/instances.ts"],"mappings":";;;;;;;;;;qBAsBa;iBAEI;EACf,aAAa;EACb;EACA,QAAQ;;EAER;;wBAGc,WAAW,iBAAiB,SAAS;;;;;;wBA0BrC,cAAc,oBAAoB;iBAQjC;EACf,OAAO;EACP,YAAY;EACZ,QAAQ;;;;;EAKR,OAAO,qBAAqB,mBAAmB;;;;;;EAM/C;;EAEA;;;;;;EAMA;EACA;;qBAKW,wBAAwB;mBAMN;mBALZ;mBACA;mBACA;mBACA;EAEjB,YAA6B,SAAS;;;;;;EAYhC,KAAK;IACT;IACA,QAAQ;IACR;IACA;MACE,QAAQ;EAeN,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAS1D,IAAI,aAAa,QAAQ;EAQzB,OAAO;IAAS;IAAkB;MAAmB;EAMrD,YAAY;UA4BJ;;UAUA;UASA;;;;;UAWN;;UAKA;;;;;;;;;;UAaM;UAKA;UA0CN"}
|
package/dist/aws/instances.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JourneyParams, WorkflowBindingLike, WorkflowInstanceLike } from "../
|
|
1
|
+
import { JourneyParams, WorkflowBindingLike, WorkflowInstanceLike } from "../host/platform.mjs";
|
|
2
2
|
import { ExecutionRegistry, ExecutionStart, JourneyStore } from "../store/index.mjs";
|
|
3
3
|
import { LambdaApi } from "./lambda-api.mjs";
|
|
4
4
|
//#region src/aws/instances.d.ts
|
|
@@ -87,6 +87,16 @@ export declare class AwsInstances implements WorkflowBindingLike {
|
|
|
87
87
|
private vanished;
|
|
88
88
|
/** Stopped so recently that an invocation running at the time may still be acting. */
|
|
89
89
|
private draining;
|
|
90
|
+
/**
|
|
91
|
+
* The one judgment every path makes of an execution the platform reports stopped — by
|
|
92
|
+
* `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first
|
|
93
|
+
* sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the
|
|
94
|
+
* drain), and the record is read again so the verdict rests on what is committed now, not on
|
|
95
|
+
* the snapshot a caller read before its API calls. A write or read that fails throws: an
|
|
96
|
+
* unknown stop time is never taken for "safely stopped". Undefined means the record is gone —
|
|
97
|
+
* the erasure finished with it meanwhile.
|
|
98
|
+
*/
|
|
99
|
+
private observeStop;
|
|
90
100
|
private reconcile;
|
|
91
101
|
private instance;
|
|
92
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instances.d.mts","names":[],"sources":["../../src/aws/instances.ts"],"mappings":";;;;;;;;;;qBAsBa;iBAEI;EACf,aAAa;EACb;EACA,QAAQ;;EAER;;wBAGc,WAAW,iBAAiB,SAAS;;;;;;wBA0BrC,cAAc,oBAAoB;iBAQjC;EACf,OAAO;EACP,YAAY;EACZ,QAAQ;;;;;EAKR,OAAO,qBAAqB,mBAAmB;;;;;;EAM/C;;EAEA;;;;;;EAMA;EACA;;qBAKW,wBAAwB;mBAMN;mBALZ;mBACA;mBACA;mBACA;EAEjB,YAA6B,SAAS;;;;;;EAYhC,KAAK;IACT;IACA,QAAQ;IACR;IACA;MACE,QAAQ;EAeN,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAS1D,IAAI,aAAa,QAAQ;EAQzB,OAAO;IAAS;IAAkB;MAAmB;EAMrD,YAAY;UA4BJ;;UAUA;UASA;;;;;UAWN;;UAKA;
|
|
1
|
+
{"version":3,"file":"instances.d.mts","names":[],"sources":["../../src/aws/instances.ts"],"mappings":";;;;;;;;;;qBAsBa;iBAEI;EACf,aAAa;EACb;EACA,QAAQ;;EAER;;wBAGc,WAAW,iBAAiB,SAAS;;;;;;wBA0BrC,cAAc,oBAAoB;iBAQjC;EACf,OAAO;EACP,YAAY;EACZ,QAAQ;;;;;EAKR,OAAO,qBAAqB,mBAAmB;;;;;;EAM/C;;EAEA;;;;;;EAMA;EACA;;qBAKW,wBAAwB;mBAMN;mBALZ;mBACA;mBACA;mBACA;EAEjB,YAA6B,SAAS;;;;;;EAYhC,KAAK;IACT;IACA,QAAQ;IACR;IACA;MACE,QAAQ;EAeN,OAAO;IAAW;IAAY;MAAoB,QAAQ;EAS1D,IAAI,aAAa,QAAQ;EAQzB,OAAO;IAAS;IAAkB;MAAmB;EAMrD,YAAY;UA4BJ;;UAUA;UASA;;;;;UAWN;;UAKA;;;;;;;;;;UAaM;UAKA;UA0CN"}
|
package/dist/aws/instances.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { murmur3 } from "../engine/bucket.mjs";
|
|
2
|
-
import { InstanceNotFoundError } from "../
|
|
2
|
+
import { InstanceNotFoundError } from "../host/platform.mjs";
|
|
3
3
|
//#region src/aws/instances.ts
|
|
4
4
|
/**
|
|
5
5
|
* The router's instance port over Lambda durable executions: an instance is one execution of
|
|
@@ -123,10 +123,29 @@ var AwsInstances = class {
|
|
|
123
123
|
draining(record) {
|
|
124
124
|
return record.stoppedAt !== void 0 && this.now() - record.stoppedAt < this.stopDrainMs;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The one judgment every path makes of an execution the platform reports stopped — by
|
|
128
|
+
* `terminate`, by an operator, or seen by `reconcile` before the stop was recorded: the first
|
|
129
|
+
* sighting is recorded (`markStopped` keeps an earlier time, so no poll ever extends the
|
|
130
|
+
* drain), and the record is read again so the verdict rests on what is committed now, not on
|
|
131
|
+
* the snapshot a caller read before its API calls. A write or read that fails throws: an
|
|
132
|
+
* unknown stop time is never taken for "safely stopped". Undefined means the record is gone —
|
|
133
|
+
* the erasure finished with it meanwhile.
|
|
134
|
+
*/
|
|
135
|
+
async observeStop(instanceId) {
|
|
136
|
+
await this.options.executions.markStopped(instanceId, this.now());
|
|
137
|
+
return this.options.executions.get(instanceId);
|
|
138
|
+
}
|
|
126
139
|
async reconcile(record, now) {
|
|
127
140
|
const arn = await this.arnOf(record);
|
|
128
141
|
const state = arn === void 0 ? void 0 : await this.options.lambda.executionState(arn);
|
|
129
142
|
if (state === "failed" || state === "stopped" || state === "timed_out" || state === void 0 && this.vanished(record)) {
|
|
143
|
+
const fresh = state === "stopped" ? await this.observeStop(record.instanceId) : await this.options.executions.get(record.instanceId);
|
|
144
|
+
if (fresh === void 0) return;
|
|
145
|
+
if (this.draining(fresh)) {
|
|
146
|
+
await this.options.executions.markChecked(record.instanceId, now);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
130
149
|
const { store } = this.options;
|
|
131
150
|
await store.setEntryStatus(record.instanceId, "failed");
|
|
132
151
|
await store.reporting?.instanceFinished({
|
|
@@ -168,9 +187,9 @@ var AwsInstances = class {
|
|
|
168
187
|
}
|
|
169
188
|
const state = await lambda.executionState(arn);
|
|
170
189
|
if (state === "succeeded" || state === "failed" || state === "timed_out") return;
|
|
171
|
-
if (!await lambda.stopExecution(arn)) throw new Error(`stopping ${id} failed`);
|
|
172
|
-
await
|
|
173
|
-
throw new Error(`${id} is stopped; its running invocation may still be finishing`);
|
|
190
|
+
if (state !== "stopped" && !await lambda.stopExecution(arn)) throw new Error(`stopping ${id} failed`);
|
|
191
|
+
const fresh = await this.observeStop(id);
|
|
192
|
+
if (fresh !== void 0 && this.draining(fresh)) throw new Error(`${id} is stopped; its running invocation may still be finishing`);
|
|
174
193
|
},
|
|
175
194
|
status: async () => {
|
|
176
195
|
const record = await current();
|
|
@@ -181,6 +200,10 @@ var AwsInstances = class {
|
|
|
181
200
|
if (state === "succeeded") return { status: "complete" };
|
|
182
201
|
if (state === "failed") return { status: "errored" };
|
|
183
202
|
if (state === "running") return { status: "running" };
|
|
203
|
+
if (state === "stopped") {
|
|
204
|
+
const fresh = await this.observeStop(id);
|
|
205
|
+
if (fresh !== void 0 && this.draining(fresh)) return { status: "running" };
|
|
206
|
+
}
|
|
184
207
|
return { status: "terminated" };
|
|
185
208
|
}
|
|
186
209
|
};
|