@shware/workflow 2.0.1 → 3.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 +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -169,15 +169,18 @@ export default defineConfig({
|
|
|
169
169
|
// src/index.ts
|
|
170
170
|
import { type JourneyEnv, journeyWorker } from '@shware/workflow/cloudflare';
|
|
171
171
|
import { postgresJsStore } from '@shware/workflow/postgres-js';
|
|
172
|
+
import { cloudflareEmailTransport } from '@shware/workflow/transports/cloudflare-email';
|
|
172
173
|
import config from '../workflow.config';
|
|
173
174
|
|
|
174
175
|
interface Env extends JourneyEnv {
|
|
175
176
|
HYPERDRIVE: { connectionString: string };
|
|
177
|
+
EMAIL: NonNullable<JourneyEnv['EMAIL']>;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
const journeys = journeyWorker<Env>({
|
|
179
181
|
config,
|
|
180
182
|
store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),
|
|
183
|
+
email: (env) => cloudflareEmailTransport({ binding: env.EMAIL }),
|
|
181
184
|
});
|
|
182
185
|
|
|
183
186
|
/** The wrangler `workflows` binding's class_name. */
|
|
@@ -464,7 +467,7 @@ the compile fails.
|
|
|
464
467
|
`emailTemplate({ id, from, replyTo?, unsubscribeUrl?, title, description?, props, subject, preview, component })`
|
|
465
468
|
declares one. The Worker renders it with `@shware/workflow/react-email` (React's
|
|
466
469
|
streaming renderer, which waits for react-email's `<Tailwind>`) and sends it
|
|
467
|
-
through the
|
|
470
|
+
through the host's email transport, from the template's own `from`:
|
|
468
471
|
|
|
469
472
|
```ts
|
|
470
473
|
// src/email/welcome.tsx
|
|
@@ -503,6 +506,23 @@ email: {
|
|
|
503
506
|
},
|
|
504
507
|
```
|
|
505
508
|
|
|
509
|
+
The transport is the host's `email` option, chosen independently of where the
|
|
510
|
+
workflow runs. Rendering, tracking and the tracked-link commit happen before it,
|
|
511
|
+
the same for every provider (`EmailSender`):
|
|
512
|
+
|
|
513
|
+
| Transport | Import |
|
|
514
|
+
| -------------------------------------------------------------------------- | ---------------------------------------------- |
|
|
515
|
+
| `cloudflareEmailTransport({ binding })` — a Worker's `send_email` | `@shware/workflow/transports/cloudflare-email` |
|
|
516
|
+
| `cloudflareEmailTransport({ accountId, apiToken })` — REST, anywhere | `@shware/workflow/transports/cloudflare-email` |
|
|
517
|
+
| `sesEmailTransport({ region, credentials? })` — SES v2, SigV4 over `fetch` | `@shware/workflow/transports/ses` |
|
|
518
|
+
|
|
519
|
+
`sesEmailTransport` runs in a Worker and in Node alike. Without `credentials` it
|
|
520
|
+
reads `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` at send
|
|
521
|
+
time (a Lambda's execution role); a Worker passes them, or a provider, from its
|
|
522
|
+
env. Delivery is at least once: SES has no de-duplication key, so an email the
|
|
523
|
+
provider accepted just before the step's checkpoint failed can go out twice.
|
|
524
|
+
Without an `email` option the email channel fails its sends, saying so.
|
|
525
|
+
|
|
506
526
|
An email step refuses to send when the profile has no `email`.
|
|
507
527
|
`GET /preview/<id>?props=<JSON>` renders a template, with the subject in the
|
|
508
528
|
`x-subject` header.
|
|
@@ -705,7 +725,7 @@ The default store needs three bindings, named as in `JourneyEnv`:
|
|
|
705
725
|
| `JOURNEY` | Workflow | The `JourneyRunner` entrypoint |
|
|
706
726
|
|
|
707
727
|
A Worker on Postgres needs only `JOURNEY`. Add the `EMAIL` send_email binding
|
|
708
|
-
when
|
|
728
|
+
when it sends through Cloudflare Email (`cloudflareEmailTransport({ binding: env.EMAIL })`), and a `triggers.crons` entry with `scheduled`
|
|
709
729
|
exported to run the segment sweep.
|
|
710
730
|
|
|
711
731
|
Set `API_TOKEN` in production. Without it every mutating endpoint is open, and
|
|
@@ -726,6 +746,80 @@ There are no migrations, and re-applying the file is not one: every statement is
|
|
|
726
746
|
the first insert needing a new column fails. Recreate it instead. The demo has
|
|
727
747
|
`pnpm db:reset`.
|
|
728
748
|
|
|
749
|
+
## AWS Lambda
|
|
750
|
+
|
|
751
|
+
The same config runs as one Lambda durable function behind a Function URL, on
|
|
752
|
+
Postgres. `@aws/durable-execution-sdk-js` and `@aws-sdk/client-lambda` are optional
|
|
753
|
+
peer dependencies, needed only by `@shware/workflow/aws`. The durable SDK's own type
|
|
754
|
+
declarations reference `aws-lambda`, so a TypeScript project that type-checks
|
|
755
|
+
dependencies (no `skipLibCheck`) also needs `@types/aws-lambda`.
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
// src/aws.ts
|
|
759
|
+
import { journeyLambda } from '@shware/workflow/aws';
|
|
760
|
+
import { postgresJsStore } from '@shware/workflow/postgres-js';
|
|
761
|
+
import { sesEmailTransport } from '@shware/workflow/transports/ses';
|
|
762
|
+
import config from '../workflow.config';
|
|
763
|
+
import { env } from './env.aws'; // process.env, parsed with zod
|
|
764
|
+
|
|
765
|
+
const journeys = journeyLambda({
|
|
766
|
+
config,
|
|
767
|
+
env,
|
|
768
|
+
store: (env, database) => postgresJsStore(env.DATABASE_URL, database),
|
|
769
|
+
email: (env) => sesEmailTransport({ region: env.AWS_REGION }),
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
export const handler = journeys.handler;
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
One handler serves three inputs, told apart by the event's top-level shape, so
|
|
776
|
+
no HTTP body can pass for the others:
|
|
777
|
+
|
|
778
|
+
- **A Function URL request**: the HTTP API, as on Cloudflare. A journey it
|
|
779
|
+
starts is its own durable execution, submitted with an asynchronous `Invoke`
|
|
780
|
+
of this function's numbered version under a stable execution name, and the
|
|
781
|
+
request returns.
|
|
782
|
+
- **A run** (`$workflow.run`): one instance. Steps, delays and waits map onto
|
|
783
|
+
the durable SDK's `step`, `wait` and callbacks; a wait parks on a fresh
|
|
784
|
+
callback, recorded in Postgres, which the next relevant event completes.
|
|
785
|
+
- **A sweep** (`{ "type": "$workflow.sweep" }`, from EventBridge Scheduler, or
|
|
786
|
+
`POST /segments/sweep` from an existing cron): the segment sweep, plus
|
|
787
|
+
retrying starts whose submit was never confirmed and reconciling executions
|
|
788
|
+
the platform ended (timeout, stop) into the ledger. Without one, clock-only
|
|
789
|
+
segments and this upkeep do not happen.
|
|
790
|
+
|
|
791
|
+
A start is recorded with its entry in one transaction (the `execution` table),
|
|
792
|
+
so a lost `Invoke` response is retried with the same name and payload and never
|
|
793
|
+
re-entered as a second execution. Retries stop after 12 hours (`startRetryMs`,
|
|
794
|
+
keep it under the execution retention) and the start is left unresolved.
|
|
795
|
+
|
|
796
|
+
Deploy one artifact per version: publish a numbered version and point the
|
|
797
|
+
`live` alias, which the Function URL is bound to, at it. Each version carries its
|
|
798
|
+
compiled bundle and makes it live by itself, only over an older one; executions
|
|
799
|
+
stay on the version that started them. `POST /deploy` of an arbitrary bundle is
|
|
800
|
+
refused.
|
|
801
|
+
|
|
802
|
+
```yaml
|
|
803
|
+
Workflow:
|
|
804
|
+
Type: AWS::Serverless::Function
|
|
805
|
+
Properties:
|
|
806
|
+
Runtime: nodejs24.x
|
|
807
|
+
Handler: aws.handler
|
|
808
|
+
Timeout: 60
|
|
809
|
+
AutoPublishAlias: live
|
|
810
|
+
DurableConfig: { ExecutionTimeout: 2592000, RetentionPeriodInDays: 30 }
|
|
811
|
+
FunctionUrlConfig: { AuthType: NONE, InvokeMode: BUFFERED }
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
The execution role needs the durable checkpoint permissions,
|
|
815
|
+
`lambda:InvokeFunction` on its own versions, `lambda:SendDurableExecutionCallbackSuccess`,
|
|
816
|
+
`lambda:GetDurableExecution`, `lambda:ListDurableExecutionsByFunction` and
|
|
817
|
+
`lambda:StopDurableExecution`, plus `ses:SendEmail` when it sends through SES.
|
|
818
|
+
With `AuthType: NONE`, scope the public `lambda:InvokeFunction` grant to
|
|
819
|
+
`lambda:InvokedViaFunctionUrl = true` so the URL cannot be used to invoke the
|
|
820
|
+
function directly. APNs goes over Node's HTTP/2 client; a custom domain needs
|
|
821
|
+
CloudFront (or another HTTPS proxy) in front of the Function URL.
|
|
822
|
+
|
|
729
823
|
## Postgres
|
|
730
824
|
|
|
731
825
|
A host on Postgres (through Hyperdrive, say) keeps Cloudflare Workflows for
|
package/dist/apns-relay.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_http2_client = require("./http2-client.cjs");
|
|
2
3
|
let node_http = require("node:http");
|
|
3
|
-
let node_http2 = require("node:http2");
|
|
4
4
|
//#region src/apns-relay.ts
|
|
5
5
|
const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
|
|
6
6
|
/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
|
|
@@ -16,19 +16,7 @@ const FORWARDED = [
|
|
|
16
16
|
];
|
|
17
17
|
function startApnsRelay(options) {
|
|
18
18
|
const allow = new Set(options.allow ?? DEFAULT_ALLOW);
|
|
19
|
-
const sessions =
|
|
20
|
-
function session(upstream) {
|
|
21
|
-
const existing = sessions.get(upstream);
|
|
22
|
-
if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
|
|
23
|
-
const created = (0, node_http2.connect)(upstream);
|
|
24
|
-
const forget = () => {
|
|
25
|
-
if (sessions.get(upstream) === created) sessions.delete(upstream);
|
|
26
|
-
};
|
|
27
|
-
created.on("error", forget);
|
|
28
|
-
created.on("close", forget);
|
|
29
|
-
sessions.set(upstream, created);
|
|
30
|
-
return created;
|
|
31
|
-
}
|
|
19
|
+
const sessions = require_http2_client.http2Sessions();
|
|
32
20
|
function relay(req, res) {
|
|
33
21
|
const upstreamHeader = req.headers["apns-relay-upstream"];
|
|
34
22
|
const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
|
|
@@ -53,7 +41,7 @@ function startApnsRelay(options) {
|
|
|
53
41
|
}
|
|
54
42
|
let stream;
|
|
55
43
|
try {
|
|
56
|
-
stream = session(upstream).request(headers);
|
|
44
|
+
stream = sessions.session(upstream).request(headers);
|
|
57
45
|
} catch (error) {
|
|
58
46
|
res.writeHead(502, { "content-type": "application/json" });
|
|
59
47
|
res.end(JSON.stringify({
|
|
@@ -91,8 +79,7 @@ function startApnsRelay(options) {
|
|
|
91
79
|
resolve({
|
|
92
80
|
port: typeof address === "object" && address !== null ? address.port : options.port,
|
|
93
81
|
close: () => new Promise((done) => {
|
|
94
|
-
|
|
95
|
-
sessions.clear();
|
|
82
|
+
sessions.close();
|
|
96
83
|
server.close(() => done());
|
|
97
84
|
})
|
|
98
85
|
});
|
package/dist/apns-relay.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apns-relay.cjs","names":["
|
|
1
|
+
{"version":3,"file":"apns-relay.cjs","names":["http2Sessions","createServer"],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { http2Sessions } from './http2-client';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = http2Sessions();\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = sessions.session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n sessions.close();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":";;;;AA8BA,MAAM,gBAAgB,CAAC,8BAA8B,oCAAoC;;AAGzF,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAAe,SAA+C;CAC5E,MAAM,QAAQ,IAAI,IAAI,QAAQ,SAAS,aAAa;CACpD,MAAM,WAAWA,qBAAAA,cAAc;CAE/B,SAAS,MAAM,KAAsB,KAA2B;EAC9D,MAAM,iBAAiB,IAAI,QAAQ;EACnC,MAAM,WAAW,MAAM,QAAQ,cAAc,IAAI,eAAe,KAAK;EACrE,IAAI,aAAa,KAAA,KAAa,CAAC,MAAM,IAAI,QAAQ,GAAG;GAClD,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,QAAQ;IAA2B,UAAU,YAAY;GAAK,CAAC,CAAC;GACzF;EACF;EAEA,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,MAAM,UAAkC;IACtC,WAAW,IAAI,UAAU;IACzB,SAAS,IAAI,OAAO;GACtB;GACA,KAAK,MAAM,QAAQ,WAAW;IAC5B,MAAM,QAAQ,IAAI,QAAQ;IAC1B,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;GACjD;GACA,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,QAAQ,QAAQ,CAAC,CAAC,QAAQ,OAAO;GACrD,SAAS,OAAO;IACd,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IACzD,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;IAC9E;GACF;GACA,MAAM,OAAiB,CAAC;GACxB,OAAO,GAAG,aAAa,oBAAoB;IACzC,MAAM,SAAS,gBAAgB,cAAc;IAC7C,MAAM,SAAS,gBAAgB;IAC/B,IAAI,UAAU,QAAQ;KACpB,gBAAgB;KAChB,GAAI,OAAO,WAAW,WAAW,EAAE,WAAW,OAAO,IAAI,CAAC;IAC5D,CAAC;GACH,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,KAAK,KAAK,KAAK,CAAC;GACrD,OAAO,GAAG,aAAa,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,CAAC;GACnD,OAAO,GAAG,UAAU,UAAU;IAC5B,IAAI,CAAC,IAAI,aAAa,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IAC/E,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;GAChF,CAAC;GACD,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;EAClC,CAAC;CACH;CAEA,MAAM,UAAA,GAASC,UAAAA,aAAAA,CAAa,KAAK;CACjC,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,QAAQ,MAAM,QAAQ,YAAY,mBAAmB;GACjE,MAAM,UAAU,OAAO,QAAQ;GAE/B,QAAQ;IACN,MAFW,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,QAAQ;IAGpF,aACE,IAAI,SAAS,SAAS;KACpB,SAAS,MAAM;KACf,OAAO,YAAY,KAAK,CAAC;IAC3B,CAAC;GACL,CAAC;EACH,CAAC;CACH,CAAC;AACH"}
|
package/dist/apns-relay.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { http2Sessions } from "./http2-client.mjs";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
|
-
import { connect } from "node:http2";
|
|
3
3
|
//#region src/apns-relay.ts
|
|
4
4
|
const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
|
|
5
5
|
/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
|
|
@@ -15,19 +15,7 @@ const FORWARDED = [
|
|
|
15
15
|
];
|
|
16
16
|
function startApnsRelay(options) {
|
|
17
17
|
const allow = new Set(options.allow ?? DEFAULT_ALLOW);
|
|
18
|
-
const sessions =
|
|
19
|
-
function session(upstream) {
|
|
20
|
-
const existing = sessions.get(upstream);
|
|
21
|
-
if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
|
|
22
|
-
const created = connect(upstream);
|
|
23
|
-
const forget = () => {
|
|
24
|
-
if (sessions.get(upstream) === created) sessions.delete(upstream);
|
|
25
|
-
};
|
|
26
|
-
created.on("error", forget);
|
|
27
|
-
created.on("close", forget);
|
|
28
|
-
sessions.set(upstream, created);
|
|
29
|
-
return created;
|
|
30
|
-
}
|
|
18
|
+
const sessions = http2Sessions();
|
|
31
19
|
function relay(req, res) {
|
|
32
20
|
const upstreamHeader = req.headers["apns-relay-upstream"];
|
|
33
21
|
const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
|
|
@@ -52,7 +40,7 @@ function startApnsRelay(options) {
|
|
|
52
40
|
}
|
|
53
41
|
let stream;
|
|
54
42
|
try {
|
|
55
|
-
stream = session(upstream).request(headers);
|
|
43
|
+
stream = sessions.session(upstream).request(headers);
|
|
56
44
|
} catch (error) {
|
|
57
45
|
res.writeHead(502, { "content-type": "application/json" });
|
|
58
46
|
res.end(JSON.stringify({
|
|
@@ -90,8 +78,7 @@ function startApnsRelay(options) {
|
|
|
90
78
|
resolve({
|
|
91
79
|
port: typeof address === "object" && address !== null ? address.port : options.port,
|
|
92
80
|
close: () => new Promise((done) => {
|
|
93
|
-
|
|
94
|
-
sessions.clear();
|
|
81
|
+
sessions.close();
|
|
95
82
|
server.close(() => done());
|
|
96
83
|
})
|
|
97
84
|
});
|
package/dist/apns-relay.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apns-relay.mjs","names":[],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport {
|
|
1
|
+
{"version":3,"file":"apns-relay.mjs","names":[],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { http2Sessions } from './http2-client';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = http2Sessions();\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = sessions.session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n sessions.close();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":";;;AA8BA,MAAM,gBAAgB,CAAC,8BAA8B,oCAAoC;;AAGzF,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAAe,SAA+C;CAC5E,MAAM,QAAQ,IAAI,IAAI,QAAQ,SAAS,aAAa;CACpD,MAAM,WAAW,cAAc;CAE/B,SAAS,MAAM,KAAsB,KAA2B;EAC9D,MAAM,iBAAiB,IAAI,QAAQ;EACnC,MAAM,WAAW,MAAM,QAAQ,cAAc,IAAI,eAAe,KAAK;EACrE,IAAI,aAAa,KAAA,KAAa,CAAC,MAAM,IAAI,QAAQ,GAAG;GAClD,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,QAAQ;IAA2B,UAAU,YAAY;GAAK,CAAC,CAAC;GACzF;EACF;EAEA,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,MAAM,UAAkC;IACtC,WAAW,IAAI,UAAU;IACzB,SAAS,IAAI,OAAO;GACtB;GACA,KAAK,MAAM,QAAQ,WAAW;IAC5B,MAAM,QAAQ,IAAI,QAAQ;IAC1B,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;GACjD;GACA,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,QAAQ,QAAQ,CAAC,CAAC,QAAQ,OAAO;GACrD,SAAS,OAAO;IACd,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IACzD,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;IAC9E;GACF;GACA,MAAM,OAAiB,CAAC;GACxB,OAAO,GAAG,aAAa,oBAAoB;IACzC,MAAM,SAAS,gBAAgB,cAAc;IAC7C,MAAM,SAAS,gBAAgB;IAC/B,IAAI,UAAU,QAAQ;KACpB,gBAAgB;KAChB,GAAI,OAAO,WAAW,WAAW,EAAE,WAAW,OAAO,IAAI,CAAC;IAC5D,CAAC;GACH,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,KAAK,KAAK,KAAK,CAAC;GACrD,OAAO,GAAG,aAAa,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,CAAC;GACnD,OAAO,GAAG,UAAU,UAAU;IAC5B,IAAI,CAAC,IAAI,aAAa,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IAC/E,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;GAChF,CAAC;GACD,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;EAClC,CAAC;CACH;CAEA,MAAM,SAAS,aAAa,KAAK;CACjC,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,QAAQ,MAAM,QAAQ,YAAY,mBAAmB;GACjE,MAAM,UAAU,OAAO,QAAQ;GAE/B,QAAQ;IACN,MAFW,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,QAAQ;IAGpF,aACE,IAAI,SAAS,SAAS;KACpB,SAAS,MAAM;KACf,OAAO,YAAY,KAAK,CAAC;IAC3B,CAAC;GACL,CAAC;EACH,CAAC;CACH,CAAC;AACH"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _aws_durable_execution_sdk_js = require("@aws/durable-execution-sdk-js");
|
|
3
|
+
//#region src/aws/engine-step.ts
|
|
4
|
+
/**
|
|
5
|
+
* EngineStep over a Lambda durable execution.
|
|
6
|
+
*
|
|
7
|
+
* Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
|
|
8
|
+
* callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
|
|
9
|
+
* every attempt): created where the durable context allows it — outside any step, so its id
|
|
10
|
+
* replays — then recorded in Postgres together with the subscription rows, numbered by a
|
|
11
|
+
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
12
|
+
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
13
|
+
*/
|
|
14
|
+
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
15
|
+
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
16
|
+
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
17
|
+
function stepRetry(error, attempt) {
|
|
18
|
+
if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };
|
|
19
|
+
return {
|
|
20
|
+
shouldRetry: true,
|
|
21
|
+
delay: { seconds: Math.min(60, 2 ** attempt) }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var AwsEngineStep = class {
|
|
25
|
+
context;
|
|
26
|
+
store;
|
|
27
|
+
executions;
|
|
28
|
+
lambda;
|
|
29
|
+
userId;
|
|
30
|
+
instanceId;
|
|
31
|
+
generation = 0;
|
|
32
|
+
armed;
|
|
33
|
+
constructor(context, store, executions, lambda, userId, instanceId) {
|
|
34
|
+
this.context = context;
|
|
35
|
+
this.store = store;
|
|
36
|
+
this.executions = executions;
|
|
37
|
+
this.lambda = lambda;
|
|
38
|
+
this.userId = userId;
|
|
39
|
+
this.instanceId = instanceId;
|
|
40
|
+
}
|
|
41
|
+
do(name, fn) {
|
|
42
|
+
return this.context.step(name, () => fn(), { retryStrategy: stepRetry });
|
|
43
|
+
}
|
|
44
|
+
async sleep(name, ms) {
|
|
45
|
+
if (!(ms > 0)) return;
|
|
46
|
+
await this.context.wait(name, { seconds: Math.ceil(ms / 1e3) });
|
|
47
|
+
}
|
|
48
|
+
async sleepUntil(name, timestampMs) {
|
|
49
|
+
const remaining = await this.context.step(`${name}:remaining`, async () => timestampMs - Date.now());
|
|
50
|
+
await this.sleep(name, remaining);
|
|
51
|
+
}
|
|
52
|
+
async subscribe(name, events, options = {}) {
|
|
53
|
+
const generation = ++this.generation;
|
|
54
|
+
const { deadlineMs } = options;
|
|
55
|
+
const seconds = deadlineMs === void 0 ? void 0 : await this.context.step(`${name}:timeout`, async () => Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1e3)));
|
|
56
|
+
const [promise, callbackId] = await this.context.createCallback(`${name}:callback`, seconds === void 0 ? {} : { timeout: { seconds } });
|
|
57
|
+
const previous = this.armed;
|
|
58
|
+
this.armed = {
|
|
59
|
+
promise,
|
|
60
|
+
callbackId,
|
|
61
|
+
generation,
|
|
62
|
+
settled: false
|
|
63
|
+
};
|
|
64
|
+
await this.context.step(name, async () => {
|
|
65
|
+
await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
|
|
66
|
+
await this.executions.armCallback(this.instanceId, {
|
|
67
|
+
id: callbackId,
|
|
68
|
+
generation,
|
|
69
|
+
deadlineAt: deadlineMs
|
|
70
|
+
});
|
|
71
|
+
if (previous !== void 0 && !previous.settled) await this.lambda.completeCallback(previous.callbackId, "{}");
|
|
72
|
+
}, { retryStrategy: stepRetry });
|
|
73
|
+
if (previous !== void 0) previous.settled = true;
|
|
74
|
+
}
|
|
75
|
+
async unsubscribe(name) {
|
|
76
|
+
const armed = this.armed;
|
|
77
|
+
await this.context.step(name, async () => {
|
|
78
|
+
await this.store.unsubscribe(this.instanceId);
|
|
79
|
+
if (armed === void 0) return;
|
|
80
|
+
await this.executions.clearCallback(this.instanceId, armed.generation);
|
|
81
|
+
if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, "{}");
|
|
82
|
+
}, { retryStrategy: stepRetry });
|
|
83
|
+
if (armed !== void 0) armed.settled = true;
|
|
84
|
+
this.armed = void 0;
|
|
85
|
+
}
|
|
86
|
+
async waitForWake(name, _timeoutMs) {
|
|
87
|
+
const armed = this.armed;
|
|
88
|
+
if (armed === void 0) throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);
|
|
89
|
+
try {
|
|
90
|
+
await armed.promise;
|
|
91
|
+
return "event";
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof _aws_durable_execution_sdk_js.CallbackTimeoutError) return "timeout";
|
|
94
|
+
throw error;
|
|
95
|
+
} finally {
|
|
96
|
+
armed.settled = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
exports.AwsEngineStep = AwsEngineStep;
|
|
102
|
+
exports.stepRetry = stepRetry;
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=engine-step.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-step.cjs","names":["CallbackTimeoutError"],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EngineStep } from "../engine/ports.cjs";
|
|
2
|
+
import { ExecutionRegistry, JourneyStore } from "../store/index.cjs";
|
|
3
|
+
import { LambdaApi } from "./lambda-api.cjs";
|
|
4
|
+
import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
|
|
5
|
+
//#region src/aws/engine-step.d.ts
|
|
6
|
+
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
7
|
+
export declare function stepRetry(error: Error, attempt: number): RetryDecision;
|
|
8
|
+
export declare class AwsEngineStep implements EngineStep {
|
|
9
|
+
private readonly context;
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly executions;
|
|
12
|
+
private readonly lambda;
|
|
13
|
+
private readonly userId;
|
|
14
|
+
private readonly instanceId;
|
|
15
|
+
private generation;
|
|
16
|
+
private armed;
|
|
17
|
+
constructor(context: DurableContext, store: JourneyStore, executions: ExecutionRegistry, lambda: LambdaApi, userId: string, instanceId: string);
|
|
18
|
+
do<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
19
|
+
sleep(name: string, ms: number): Promise<void>;
|
|
20
|
+
sleepUntil(name: string, timestampMs: number): Promise<void>;
|
|
21
|
+
subscribe(name: string, events: readonly string[], options?: {
|
|
22
|
+
deadlineMs?: number;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
unsubscribe(name: string): Promise<void>;
|
|
25
|
+
waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'>;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//# sourceMappingURL=engine-step.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;wBAyBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EngineStep } from "../engine/ports.mjs";
|
|
2
|
+
import { ExecutionRegistry, JourneyStore } from "../store/index.mjs";
|
|
3
|
+
import { LambdaApi } from "./lambda-api.mjs";
|
|
4
|
+
import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
|
|
5
|
+
//#region src/aws/engine-step.d.ts
|
|
6
|
+
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
7
|
+
export declare function stepRetry(error: Error, attempt: number): RetryDecision;
|
|
8
|
+
export declare class AwsEngineStep implements EngineStep {
|
|
9
|
+
private readonly context;
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly executions;
|
|
12
|
+
private readonly lambda;
|
|
13
|
+
private readonly userId;
|
|
14
|
+
private readonly instanceId;
|
|
15
|
+
private generation;
|
|
16
|
+
private armed;
|
|
17
|
+
constructor(context: DurableContext, store: JourneyStore, executions: ExecutionRegistry, lambda: LambdaApi, userId: string, instanceId: string);
|
|
18
|
+
do<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
19
|
+
sleep(name: string, ms: number): Promise<void>;
|
|
20
|
+
sleepUntil(name: string, timestampMs: number): Promise<void>;
|
|
21
|
+
subscribe(name: string, events: readonly string[], options?: {
|
|
22
|
+
deadlineMs?: number;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
unsubscribe(name: string): Promise<void>;
|
|
25
|
+
waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'>;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//# sourceMappingURL=engine-step.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;wBAyBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CallbackTimeoutError } from "@aws/durable-execution-sdk-js";
|
|
2
|
+
//#region src/aws/engine-step.ts
|
|
3
|
+
/**
|
|
4
|
+
* EngineStep over a Lambda durable execution.
|
|
5
|
+
*
|
|
6
|
+
* Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
|
|
7
|
+
* callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
|
|
8
|
+
* every attempt): created where the durable context allows it — outside any step, so its id
|
|
9
|
+
* replays — then recorded in Postgres together with the subscription rows, numbered by a
|
|
10
|
+
* generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
|
|
11
|
+
* is only a hint; the facts the wait re-checks are Postgres'.
|
|
12
|
+
*/
|
|
13
|
+
/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
|
|
14
|
+
const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
15
|
+
/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
|
|
16
|
+
function stepRetry(error, attempt) {
|
|
17
|
+
if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };
|
|
18
|
+
return {
|
|
19
|
+
shouldRetry: true,
|
|
20
|
+
delay: { seconds: Math.min(60, 2 ** attempt) }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
var AwsEngineStep = class {
|
|
24
|
+
context;
|
|
25
|
+
store;
|
|
26
|
+
executions;
|
|
27
|
+
lambda;
|
|
28
|
+
userId;
|
|
29
|
+
instanceId;
|
|
30
|
+
generation = 0;
|
|
31
|
+
armed;
|
|
32
|
+
constructor(context, store, executions, lambda, userId, instanceId) {
|
|
33
|
+
this.context = context;
|
|
34
|
+
this.store = store;
|
|
35
|
+
this.executions = executions;
|
|
36
|
+
this.lambda = lambda;
|
|
37
|
+
this.userId = userId;
|
|
38
|
+
this.instanceId = instanceId;
|
|
39
|
+
}
|
|
40
|
+
do(name, fn) {
|
|
41
|
+
return this.context.step(name, () => fn(), { retryStrategy: stepRetry });
|
|
42
|
+
}
|
|
43
|
+
async sleep(name, ms) {
|
|
44
|
+
if (!(ms > 0)) return;
|
|
45
|
+
await this.context.wait(name, { seconds: Math.ceil(ms / 1e3) });
|
|
46
|
+
}
|
|
47
|
+
async sleepUntil(name, timestampMs) {
|
|
48
|
+
const remaining = await this.context.step(`${name}:remaining`, async () => timestampMs - Date.now());
|
|
49
|
+
await this.sleep(name, remaining);
|
|
50
|
+
}
|
|
51
|
+
async subscribe(name, events, options = {}) {
|
|
52
|
+
const generation = ++this.generation;
|
|
53
|
+
const { deadlineMs } = options;
|
|
54
|
+
const seconds = deadlineMs === void 0 ? void 0 : await this.context.step(`${name}:timeout`, async () => Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1e3)));
|
|
55
|
+
const [promise, callbackId] = await this.context.createCallback(`${name}:callback`, seconds === void 0 ? {} : { timeout: { seconds } });
|
|
56
|
+
const previous = this.armed;
|
|
57
|
+
this.armed = {
|
|
58
|
+
promise,
|
|
59
|
+
callbackId,
|
|
60
|
+
generation,
|
|
61
|
+
settled: false
|
|
62
|
+
};
|
|
63
|
+
await this.context.step(name, async () => {
|
|
64
|
+
await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
|
|
65
|
+
await this.executions.armCallback(this.instanceId, {
|
|
66
|
+
id: callbackId,
|
|
67
|
+
generation,
|
|
68
|
+
deadlineAt: deadlineMs
|
|
69
|
+
});
|
|
70
|
+
if (previous !== void 0 && !previous.settled) await this.lambda.completeCallback(previous.callbackId, "{}");
|
|
71
|
+
}, { retryStrategy: stepRetry });
|
|
72
|
+
if (previous !== void 0) previous.settled = true;
|
|
73
|
+
}
|
|
74
|
+
async unsubscribe(name) {
|
|
75
|
+
const armed = this.armed;
|
|
76
|
+
await this.context.step(name, async () => {
|
|
77
|
+
await this.store.unsubscribe(this.instanceId);
|
|
78
|
+
if (armed === void 0) return;
|
|
79
|
+
await this.executions.clearCallback(this.instanceId, armed.generation);
|
|
80
|
+
if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, "{}");
|
|
81
|
+
}, { retryStrategy: stepRetry });
|
|
82
|
+
if (armed !== void 0) armed.settled = true;
|
|
83
|
+
this.armed = void 0;
|
|
84
|
+
}
|
|
85
|
+
async waitForWake(name, _timeoutMs) {
|
|
86
|
+
const armed = this.armed;
|
|
87
|
+
if (armed === void 0) throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);
|
|
88
|
+
try {
|
|
89
|
+
await armed.promise;
|
|
90
|
+
return "event";
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof CallbackTimeoutError) return "timeout";
|
|
93
|
+
throw error;
|
|
94
|
+
} finally {
|
|
95
|
+
armed.settled = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
export { AwsEngineStep, stepRetry };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=engine-step.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-step.mjs","names":[],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}
|