unfazed 0.4.0 → 0.6.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 +323 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -1
- package/dist/generated/gql.d.ts +5 -1
- package/dist/generated/gql.js +2 -1
- package/dist/generated/gql.js.map +1 -1
- package/dist/generated/graphql.d.ts +107 -0
- package/dist/generated/graphql.js +101 -0
- package/dist/generated/graphql.js.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +3 -0
- package/dist/operations.js +23 -0
- package/dist/operations.js.map +1 -1
- package/dist/remote/app.d.ts +18 -0
- package/dist/remote/app.js +76 -0
- package/dist/remote/app.js.map +1 -0
- package/dist/remote/documents.d.ts +175 -0
- package/dist/remote/documents.js +122 -0
- package/dist/remote/documents.js.map +1 -0
- package/dist/remote/values.d.ts +67 -0
- package/dist/remote/values.js +156 -0
- package/dist/remote/values.js.map +1 -0
- package/dist/remote/world.d.ts +50 -0
- package/dist/remote/world.js +612 -0
- package/dist/remote/world.js.map +1 -0
- package/dist/unfazed.d.ts +37 -1
- package/dist/unfazed.js +13 -1
- package/dist/unfazed.js.map +1 -1
- package/dist/workflow/config.d.ts +23 -0
- package/dist/workflow/config.js +48 -0
- package/dist/workflow/config.js.map +1 -0
- package/dist/workflow/errors.d.ts +33 -0
- package/dist/workflow/errors.js +16 -0
- package/dist/workflow/errors.js.map +1 -0
- package/dist/workflow/index.d.ts +30 -0
- package/dist/workflow/index.js +23 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/inspect-render.d.ts +93 -0
- package/dist/workflow/inspect-render.js +302 -0
- package/dist/workflow/inspect-render.js.map +1 -0
- package/dist/workflow/inspect.d.ts +52 -0
- package/dist/workflow/inspect.js +879 -0
- package/dist/workflow/inspect.js.map +1 -0
- package/dist/workflow/migration-files.d.ts +13 -0
- package/dist/workflow/migration-files.js +52 -0
- package/dist/workflow/migration-files.js.map +1 -0
- package/dist/workflow/migration-types.d.ts +19 -0
- package/dist/workflow/migration-types.js +8 -0
- package/dist/workflow/migration-types.js.map +1 -0
- package/dist/workflow/migrations.d.ts +12 -0
- package/dist/workflow/migrations.generated.d.ts +2 -0
- package/dist/workflow/migrations.generated.js +34 -0
- package/dist/workflow/migrations.generated.js.map +1 -0
- package/dist/workflow/migrations.js +94 -0
- package/dist/workflow/migrations.js.map +1 -0
- package/dist/workflow/nitro-flow-guard.d.ts +4 -0
- package/dist/workflow/nitro-flow-guard.js +19 -0
- package/dist/workflow/nitro-flow-guard.js.map +1 -0
- package/dist/workflow/nitro-inspect.d.ts +3 -0
- package/dist/workflow/nitro-inspect.js +19 -0
- package/dist/workflow/nitro-inspect.js.map +1 -0
- package/dist/workflow/nitro-plugin.d.ts +1 -0
- package/dist/workflow/nitro-plugin.js +13 -0
- package/dist/workflow/nitro-plugin.js.map +1 -0
- package/dist/workflow/nitro.d.ts +70 -0
- package/dist/workflow/nitro.js +213 -0
- package/dist/workflow/nitro.js.map +1 -0
- package/dist/workflow/public-url.d.ts +27 -0
- package/dist/workflow/public-url.js +45 -0
- package/dist/workflow/public-url.js.map +1 -0
- package/dist/workflow/queue.d.ts +128 -0
- package/dist/workflow/queue.js +375 -0
- package/dist/workflow/queue.js.map +1 -0
- package/dist/workflow/state.d.ts +25 -0
- package/dist/workflow/state.js +15 -0
- package/dist/workflow/state.js.map +1 -0
- package/dist/workflow/transport.d.ts +12 -0
- package/dist/workflow/transport.js +29 -0
- package/dist/workflow/transport.js.map +1 -0
- package/dist/workflow/world.d.ts +123 -0
- package/dist/workflow/world.js +375 -0
- package/dist/workflow/world.js.map +1 -0
- package/package.json +66 -5
- package/schema/SOURCE.json +4 -4
- package/schema/schema.graphqls +36 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message body codec: JSON, with `Uint8Array` values carried as
|
|
3
|
+
* `{ "__type": "Uint8Array", "data": "<base64>" }`, the same envelope the
|
|
4
|
+
* upstream Local and Postgres Worlds use. The runtime puts bytes in messages
|
|
5
|
+
* (the serialized run input of a resilient start), so plain JSON would lose
|
|
6
|
+
* them.
|
|
7
|
+
*/
|
|
8
|
+
export const MESSAGE_CONTENT_TYPE = "application/json";
|
|
9
|
+
export function encodeMessage(message) {
|
|
10
|
+
return new TextEncoder().encode(JSON.stringify(message, (_key, value) => value instanceof Uint8Array ? { __type: "Uint8Array", data: toBase64(value) } : value));
|
|
11
|
+
}
|
|
12
|
+
export function decodeMessage(bytes) {
|
|
13
|
+
return JSON.parse(new TextDecoder().decode(bytes), (_key, value) => {
|
|
14
|
+
if (value !== null &&
|
|
15
|
+
typeof value === "object" &&
|
|
16
|
+
value.__type === "Uint8Array" &&
|
|
17
|
+
typeof value.data === "string") {
|
|
18
|
+
return fromBase64(value.data);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function toBase64(bytes) {
|
|
24
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
25
|
+
}
|
|
26
|
+
export function fromBase64(text) {
|
|
27
|
+
return new Uint8Array(Buffer.from(text, "base64"));
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/workflow/transport.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;AAEvD,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAC7B,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAc,EAAE,EAAE,CAC/C,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CACtF,CACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,KAAc,EAAE,EAAE;QAC1E,IACE,KAAK,KAAK,IAAI;YACd,OAAO,KAAK,KAAK,QAAQ;YACxB,KAA8B,CAAC,MAAM,KAAK,YAAY;YACvD,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,EACtD,CAAC;YACD,OAAO,UAAU,CAAE,KAA0B,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAiB;IACxC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;AACrD,CAAC","sourcesContent":["/**\n * The message body codec: JSON, with `Uint8Array` values carried as\n * `{ \"__type\": \"Uint8Array\", \"data\": \"<base64>\" }`, the same envelope the\n * upstream Local and Postgres Worlds use. The runtime puts bytes in messages\n * (the serialized run input of a resilient start), so plain JSON would lose\n * them.\n */\nexport const MESSAGE_CONTENT_TYPE = \"application/json\";\n\nexport function encodeMessage(message: unknown): Uint8Array {\n return new TextEncoder().encode(\n JSON.stringify(message, (_key, value: unknown) =>\n value instanceof Uint8Array ? { __type: \"Uint8Array\", data: toBase64(value) } : value,\n ),\n );\n}\n\nexport function decodeMessage(bytes: Uint8Array): unknown {\n return JSON.parse(new TextDecoder().decode(bytes), (_key, value: unknown) => {\n if (\n value !== null &&\n typeof value === \"object\" &&\n (value as { __type?: unknown }).__type === \"Uint8Array\" &&\n typeof (value as { data?: unknown }).data === \"string\"\n ) {\n return fromBase64((value as { data: string }).data);\n }\n return value;\n });\n}\n\nexport function toBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(\"base64\");\n}\n\nexport function fromBase64(text: string): Uint8Array {\n return new Uint8Array(Buffer.from(text, \"base64\"));\n}\n"]}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type Queue, type World, type WorldCapabilities } from "@workflow/world";
|
|
2
|
+
import { createWorld as createPostgresWorld } from "@workflow/world-postgres";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
import { type WorkflowEnv } from "./config.js";
|
|
5
|
+
import type { WorkflowMigrations } from "./migration-types.js";
|
|
6
|
+
import { type WorkflowFetch } from "./queue.js";
|
|
7
|
+
/** `getDeploymentId()` outside an unfazed service (no `UNFZ_DEPLOYMENT_ID`). */
|
|
8
|
+
export declare const LOCAL_DEPLOYMENT_ID = "dpl_unfazed_local";
|
|
9
|
+
export interface UnfazedWorldOptions {
|
|
10
|
+
/** Where every default below is read from. Default: `process.env`. */
|
|
11
|
+
env?: WorkflowEnv;
|
|
12
|
+
/**
|
|
13
|
+
* Postgres connection string for run storage and streams. Default: the first
|
|
14
|
+
* of `WORKFLOW_POSTGRES_URL`, `DATABASE_URL_UNPOOLED`, `DATABASE_URL`.
|
|
15
|
+
* `sslmode=require` gets `uselibpqcompat=true` so node-postgres accepts the
|
|
16
|
+
* unfazed integration's self-signed certificate as libpq would.
|
|
17
|
+
*/
|
|
18
|
+
connectionString?: string;
|
|
19
|
+
/** Pool size. Default: `WORKFLOW_POSTGRES_MAX_POOL_SIZE`, else node-postgres' 10. */
|
|
20
|
+
maxPoolSize?: number;
|
|
21
|
+
/** The platform state plane. Default: `UNFZ_STATE_URL`. */
|
|
22
|
+
stateUrl?: string;
|
|
23
|
+
/** The app's state token, also the delivery token. Default: `UNFZ_STATE_TOKEN`. */
|
|
24
|
+
stateToken?: string;
|
|
25
|
+
/** Default: `UNFZ_DEPLOYMENT_ID`, else {@link LOCAL_DEPLOYMENT_ID}. */
|
|
26
|
+
deploymentId?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The upstream migrations to apply on start-up. Default: the copy embedded
|
|
29
|
+
* in this package. The Nitro module passes the copy from the app's own
|
|
30
|
+
* `@workflow/world-postgres`, so schema and storage code always match.
|
|
31
|
+
*/
|
|
32
|
+
migrations?: WorkflowMigrations;
|
|
33
|
+
/** How long one queue call keeps retrying 503s and network failures. Default 30 s. */
|
|
34
|
+
retryBudgetMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* How long the upstream storage (and its `LISTEN` connections) outlives the
|
|
37
|
+
* last open World call or stream reader. Default 2 s.
|
|
38
|
+
*/
|
|
39
|
+
idleReleaseMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* The app's public URL, which `createWebhook().url` and
|
|
42
|
+
* `getWorkflowMetadata().url` are built on. Default: `UNFZ_PUBLIC_URL`.
|
|
43
|
+
* A bare https host (custom domain, `https://<alias>.unfz.to`) is handed to
|
|
44
|
+
* Workflow SDK through `process.env.VERCEL_URL` when that is unset, the
|
|
45
|
+
* only base URL the SDK reads; a path-form URL cannot be. `false` leaves
|
|
46
|
+
* `VERCEL_URL` alone.
|
|
47
|
+
*/
|
|
48
|
+
publicUrl?: string | false;
|
|
49
|
+
/** Injection point for tests. Default: global `fetch`. */
|
|
50
|
+
fetch?: WorkflowFetch;
|
|
51
|
+
}
|
|
52
|
+
/** The World `createWorld()` returns. */
|
|
53
|
+
export type UnfazedWorld = World & Required<Pick<Queue, "queueBatch" | "registerRoute">> & {
|
|
54
|
+
getRuntimeDeadline(): Promise<Date | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolves once start-up succeeded (schema migrated, platform queue
|
|
57
|
+
* reachable), starting it if needed; rejects with the start-up error.
|
|
58
|
+
* Unlike {@link start} it does not wait for route writes or the re-enqueue.
|
|
59
|
+
*/
|
|
60
|
+
ready(): Promise<void>;
|
|
61
|
+
start(): Promise<void>;
|
|
62
|
+
close(): Promise<void>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The capabilities `@workflow/world-postgres` declares for its storage (all
|
|
66
|
+
* but `invoke`, a queue feature the platform queue does not offer). Declared
|
|
67
|
+
* here because the upstream World is only built on demand; a test checks the
|
|
68
|
+
* two stay equal.
|
|
69
|
+
*/
|
|
70
|
+
export declare const UPSTREAM_STORAGE_CAPABILITIES: WorldCapabilities;
|
|
71
|
+
/**
|
|
72
|
+
* The storage and stream methods of `@workflow/world-postgres` this World
|
|
73
|
+
* forwards. Listed rather than discovered because the upstream World is only
|
|
74
|
+
* built on demand; a test fails when upstream has a method that is missing
|
|
75
|
+
* here, so a new one gets a decision instead of silently going missing.
|
|
76
|
+
*/
|
|
77
|
+
export declare const FORWARDED_METHODS: {
|
|
78
|
+
readonly runs: readonly ["get", "getMany", "list", "waitForTerminalStatus", "experimentalSetAttributes"];
|
|
79
|
+
readonly steps: readonly ["get", "list"];
|
|
80
|
+
readonly events: readonly ["create", "get", "list", "listByCorrelationId"];
|
|
81
|
+
readonly hooks: readonly ["get", "getByToken", "list"];
|
|
82
|
+
readonly experimental_snapshots: readonly ["save", "load", "delete"];
|
|
83
|
+
readonly streams: readonly ["write", "writeMulti", "close", "getChunks", "getInfo", "get", "list"];
|
|
84
|
+
};
|
|
85
|
+
/** Top-level members of the upstream World, and what happens to each. */
|
|
86
|
+
export declare const UPSTREAM_MEMBERS: {
|
|
87
|
+
readonly forwarded: string[];
|
|
88
|
+
readonly declared: readonly ["specVersion", "capabilities", "streamFlushIntervalMs"];
|
|
89
|
+
readonly replacedByPlatformQueue: readonly ["queue", "createQueueHandler", "getDeploymentId", "registerRoute", "registerHandler", "invoke", "start", "close"];
|
|
90
|
+
};
|
|
91
|
+
type Upstream = ReturnType<typeof createPostgresWorld>;
|
|
92
|
+
/**
|
|
93
|
+
* Creates the unfazed World for Workflow SDK.
|
|
94
|
+
*
|
|
95
|
+
* Storage, streams and `runs.waitForTerminalStatus` are the upstream
|
|
96
|
+
* `@workflow/world-postgres` code over the app's own Postgres; its
|
|
97
|
+
* graphile-worker queue is never started. The queue is the unfazed
|
|
98
|
+
* platform's: `queue()` enqueues through the worker's state plane, and the
|
|
99
|
+
* platform delivers each message to the flow route, cold-starting an
|
|
100
|
+
* `on_demand` VM, authenticated by a header public ingress strips.
|
|
101
|
+
*
|
|
102
|
+
* Nothing here needs a warm process. `createWorld()` is synchronous and opens
|
|
103
|
+
* nothing: install it with `setWorld(createWorld())` before the server serves.
|
|
104
|
+
* It does point the SDK at the app's public URL (see `publicUrl`), so webhook
|
|
105
|
+
* URLs are right without app code.
|
|
106
|
+
* The first World call of a process applies the upstream migrations (a no-op
|
|
107
|
+
* when current) and probes the queue, and every call waits for that, so a
|
|
108
|
+
* request that arrives during a cold start waits instead of failing. Every
|
|
109
|
+
* wake-up is a platform delivery; Postgres connections, including the
|
|
110
|
+
* `LISTEN` ones behind stream readers and `waitForTerminalStatus`, are held
|
|
111
|
+
* only while a World call or a stream reader is open, plus a short grace.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createWorld(options?: UnfazedWorldOptions): UnfazedWorld;
|
|
114
|
+
/**
|
|
115
|
+
* Builds the upstream World with its internal Local World on the `node:http`
|
|
116
|
+
* transport. Its default, an undici `Agent`, has no `close()` under Bun's
|
|
117
|
+
* built-in undici, and the upstream `close()` closes that queue before the
|
|
118
|
+
* streamer: the throw would leave the streamer's `LISTEN` connection open.
|
|
119
|
+
* The flag is read synchronously during construction, so setting it around
|
|
120
|
+
* the call alone changes nothing else in the process.
|
|
121
|
+
*/
|
|
122
|
+
export declare function constructUpstream(pool: Pool): Upstream;
|
|
123
|
+
export {};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { mintedSpecVersion, parseQueueName, parseQueueRoute, QueuePrefix, reenqueueActiveRuns, } from "@workflow/world";
|
|
2
|
+
import { createWorld as createPostgresWorld } from "@workflow/world-postgres";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
import { DATABASE_URL_VARS, envPositiveInt, envValue, resolveDatabaseUrl, withLibpqCompat } from "./config.js";
|
|
5
|
+
import { UnfazedWorkflowError } from "./errors.js";
|
|
6
|
+
import { applyMigrations } from "./migrations.js";
|
|
7
|
+
import { embeddedMigrations } from "./migrations.generated.js";
|
|
8
|
+
import { applyPublicUrl } from "./public-url.js";
|
|
9
|
+
import { byteLength, chunkMessages, createDeliveryHandler, createQueueClient, currentDeliveryDeadline, describe, enqueueBatch, MAX_ROUTE_PATH_BYTES, toWireMessage, } from "./queue.js";
|
|
10
|
+
/** `getDeploymentId()` outside an unfazed service (no `UNFZ_DEPLOYMENT_ID`). */
|
|
11
|
+
export const LOCAL_DEPLOYMENT_ID = "dpl_unfazed_local";
|
|
12
|
+
/**
|
|
13
|
+
* The capabilities `@workflow/world-postgres` declares for its storage (all
|
|
14
|
+
* but `invoke`, a queue feature the platform queue does not offer). Declared
|
|
15
|
+
* here because the upstream World is only built on demand; a test checks the
|
|
16
|
+
* two stay equal.
|
|
17
|
+
*/
|
|
18
|
+
export const UPSTREAM_STORAGE_CAPABILITIES = {
|
|
19
|
+
hookRetention: { active: true },
|
|
20
|
+
dynamicWorkflowCode: true,
|
|
21
|
+
hookResumeDedup: true,
|
|
22
|
+
hookForceClaim: true,
|
|
23
|
+
inBandFence: true,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The storage and stream methods of `@workflow/world-postgres` this World
|
|
27
|
+
* forwards. Listed rather than discovered because the upstream World is only
|
|
28
|
+
* built on demand; a test fails when upstream has a method that is missing
|
|
29
|
+
* here, so a new one gets a decision instead of silently going missing.
|
|
30
|
+
*/
|
|
31
|
+
export const FORWARDED_METHODS = {
|
|
32
|
+
runs: ["get", "getMany", "list", "waitForTerminalStatus", "experimentalSetAttributes"],
|
|
33
|
+
steps: ["get", "list"],
|
|
34
|
+
events: ["create", "get", "list", "listByCorrelationId"],
|
|
35
|
+
hooks: ["get", "getByToken", "list"],
|
|
36
|
+
experimental_snapshots: ["save", "load", "delete"],
|
|
37
|
+
streams: ["write", "writeMulti", "close", "getChunks", "getInfo", "get", "list"],
|
|
38
|
+
};
|
|
39
|
+
/** Top-level members of the upstream World, and what happens to each. */
|
|
40
|
+
export const UPSTREAM_MEMBERS = {
|
|
41
|
+
forwarded: Object.keys(FORWARDED_METHODS),
|
|
42
|
+
declared: ["specVersion", "capabilities", "streamFlushIntervalMs"],
|
|
43
|
+
replacedByPlatformQueue: ["queue", "createQueueHandler", "getDeploymentId", "registerRoute", "registerHandler", "invoke", "start", "close"],
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Creates the unfazed World for Workflow SDK.
|
|
47
|
+
*
|
|
48
|
+
* Storage, streams and `runs.waitForTerminalStatus` are the upstream
|
|
49
|
+
* `@workflow/world-postgres` code over the app's own Postgres; its
|
|
50
|
+
* graphile-worker queue is never started. The queue is the unfazed
|
|
51
|
+
* platform's: `queue()` enqueues through the worker's state plane, and the
|
|
52
|
+
* platform delivers each message to the flow route, cold-starting an
|
|
53
|
+
* `on_demand` VM, authenticated by a header public ingress strips.
|
|
54
|
+
*
|
|
55
|
+
* Nothing here needs a warm process. `createWorld()` is synchronous and opens
|
|
56
|
+
* nothing: install it with `setWorld(createWorld())` before the server serves.
|
|
57
|
+
* It does point the SDK at the app's public URL (see `publicUrl`), so webhook
|
|
58
|
+
* URLs are right without app code.
|
|
59
|
+
* The first World call of a process applies the upstream migrations (a no-op
|
|
60
|
+
* when current) and probes the queue, and every call waits for that, so a
|
|
61
|
+
* request that arrives during a cold start waits instead of failing. Every
|
|
62
|
+
* wake-up is a platform delivery; Postgres connections, including the
|
|
63
|
+
* `LISTEN` ones behind stream readers and `waitForTerminalStatus`, are held
|
|
64
|
+
* only while a World call or a stream reader is open, plus a short grace.
|
|
65
|
+
*/
|
|
66
|
+
export function createWorld(options = {}) {
|
|
67
|
+
const env = options.env ?? process.env;
|
|
68
|
+
const rawUrl = options.connectionString ?? resolveDatabaseUrl(env);
|
|
69
|
+
const connectionString = rawUrl ? withLibpqCompat(rawUrl) : undefined;
|
|
70
|
+
const deploymentId = options.deploymentId ?? envValue(env, "UNFZ_DEPLOYMENT_ID") ?? LOCAL_DEPLOYMENT_ID;
|
|
71
|
+
const stateToken = options.stateToken ?? envValue(env, "UNFZ_STATE_TOKEN");
|
|
72
|
+
const migrations = options.migrations ?? embeddedMigrations;
|
|
73
|
+
const maxPoolSize = options.maxPoolSize ?? envPositiveInt(env, "WORKFLOW_POSTGRES_MAX_POOL_SIZE");
|
|
74
|
+
const idleReleaseMs = options.idleReleaseMs ?? 2_000;
|
|
75
|
+
// Before the server serves: the SDK reads the base URL per invocation.
|
|
76
|
+
if (options.publicUrl !== false)
|
|
77
|
+
applyPublicUrl(options.publicUrl ?? envValue(env, "UNFZ_PUBLIC_URL"));
|
|
78
|
+
const client = createQueueClient({
|
|
79
|
+
stateUrl: options.stateUrl ?? envValue(env, "UNFZ_STATE_URL"),
|
|
80
|
+
stateToken,
|
|
81
|
+
fetch: options.fetch ?? ((input, init) => fetch(input, init)),
|
|
82
|
+
retryBudgetMs: options.retryBudgetMs ?? 30_000,
|
|
83
|
+
requestTimeoutMs: 15_000,
|
|
84
|
+
});
|
|
85
|
+
// node-postgres connects lazily, so constructing the pool opens nothing. Idle
|
|
86
|
+
// connections close after 5 s, well inside an on_demand VM's idle window, so
|
|
87
|
+
// a reaped VM leaves no half-open sessions behind on the database.
|
|
88
|
+
let pool;
|
|
89
|
+
function getPool() {
|
|
90
|
+
if (!pool) {
|
|
91
|
+
pool = new Pool({
|
|
92
|
+
connectionString,
|
|
93
|
+
idleTimeoutMillis: 5_000,
|
|
94
|
+
...(maxPoolSize ? { max: maxPoolSize } : {}),
|
|
95
|
+
});
|
|
96
|
+
// An `error` with no listener ends the process. pg-pool re-emits an idle
|
|
97
|
+
// client's error on the pool, but a checked-out client only on itself.
|
|
98
|
+
pool.on("error", (error) => console.warn(`[unfazed/workflow] idle database connection closed: ${error.message}`));
|
|
99
|
+
pool.on("connect", (connection) => {
|
|
100
|
+
connection.on("error", (error) => console.warn(`[unfazed/workflow] database connection error: ${error.message}`));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return pool;
|
|
104
|
+
}
|
|
105
|
+
let closed = false;
|
|
106
|
+
let ready;
|
|
107
|
+
let repairing;
|
|
108
|
+
let closing;
|
|
109
|
+
// ---- Start-up: schema and queue both usable. Every World call waits. ----
|
|
110
|
+
function whenReady() {
|
|
111
|
+
if (closed)
|
|
112
|
+
return Promise.reject(new Error("unfazed/workflow: the World is closed"));
|
|
113
|
+
ready ??= (async () => {
|
|
114
|
+
if (!connectionString) {
|
|
115
|
+
throw new UnfazedWorkflowError("no_database", "unfazed/workflow: no Postgres database is bound to this app. Workflow runs are stored in " +
|
|
116
|
+
"the app's own Postgres: bind a Postgres or Neon integration to the app and redeploy so " +
|
|
117
|
+
`DATABASE_URL is set, or set WORKFLOW_POSTGRES_URL (checked ${DATABASE_URL_VARS.join(", ")}).`);
|
|
118
|
+
}
|
|
119
|
+
await Promise.all([applyMigrations(getPool(), migrations), client.stats()]);
|
|
120
|
+
})().then(() => {
|
|
121
|
+
// Once per process: put active runs back on the queue in case a
|
|
122
|
+
// message went missing. Deduplicated by key, never needed for
|
|
123
|
+
// correctness, so it runs beside the call that started up.
|
|
124
|
+
void repairOnce().catch((error) => console.warn(`[unfazed/workflow] re-enqueueing active runs failed: ${describe(error)}`));
|
|
125
|
+
}, (error) => {
|
|
126
|
+
ready = undefined; // the next call tries again
|
|
127
|
+
throw error;
|
|
128
|
+
});
|
|
129
|
+
return ready;
|
|
130
|
+
}
|
|
131
|
+
// ---- The upstream storage, built on demand and released when idle. ----
|
|
132
|
+
//
|
|
133
|
+
// `@workflow/world-postgres` opens its stream `LISTEN` connection as soon as
|
|
134
|
+
// it is constructed, and `waitForTerminalStatus` keeps another once used.
|
|
135
|
+
// Held for the life of the process, those would outlive every reaped VM as
|
|
136
|
+
// half-open sessions on the database. So an instance lives only while World
|
|
137
|
+
// calls or stream readers are open (plus `idleReleaseMs`), and the next call
|
|
138
|
+
// builds a fresh one. Built only after start-up, when the database answered.
|
|
139
|
+
let upstream;
|
|
140
|
+
let activity = 0;
|
|
141
|
+
let releaseTimer;
|
|
142
|
+
const retiring = new Set();
|
|
143
|
+
function acquire() {
|
|
144
|
+
activity++;
|
|
145
|
+
if (releaseTimer) {
|
|
146
|
+
clearTimeout(releaseTimer);
|
|
147
|
+
releaseTimer = undefined;
|
|
148
|
+
}
|
|
149
|
+
upstream ??= constructUpstream(getPool());
|
|
150
|
+
return upstream;
|
|
151
|
+
}
|
|
152
|
+
function release() {
|
|
153
|
+
activity--;
|
|
154
|
+
if (activity > 0 || releaseTimer || !upstream)
|
|
155
|
+
return;
|
|
156
|
+
releaseTimer = setTimeout(() => {
|
|
157
|
+
releaseTimer = undefined;
|
|
158
|
+
if (activity > 0 || !upstream)
|
|
159
|
+
return;
|
|
160
|
+
const idle = upstream;
|
|
161
|
+
upstream = undefined;
|
|
162
|
+
const done = Promise.resolve(idle.close?.())
|
|
163
|
+
.catch((error) => console.warn(`[unfazed/workflow] releasing idle storage failed: ${describe(error)}`))
|
|
164
|
+
.finally(() => retiring.delete(done));
|
|
165
|
+
retiring.add(done);
|
|
166
|
+
}, idleReleaseMs);
|
|
167
|
+
releaseTimer.unref?.();
|
|
168
|
+
}
|
|
169
|
+
function forward(namespace, method) {
|
|
170
|
+
const target = (instance) => {
|
|
171
|
+
const ns = instance[namespace];
|
|
172
|
+
const fn = ns?.[method];
|
|
173
|
+
if (typeof fn !== "function") {
|
|
174
|
+
throw new TypeError(`unfazed/workflow: the installed @workflow/world-postgres has no ${namespace}.${method}()`);
|
|
175
|
+
}
|
|
176
|
+
return fn.bind(ns);
|
|
177
|
+
};
|
|
178
|
+
if (namespace === "streams" && method === "get") {
|
|
179
|
+
// A reader outlives the call that opened it: hold the instance (and its
|
|
180
|
+
// LISTEN connection) until the stream ends or is cancelled.
|
|
181
|
+
return async (...args) => {
|
|
182
|
+
await whenReady();
|
|
183
|
+
const instance = acquire();
|
|
184
|
+
let stream;
|
|
185
|
+
try {
|
|
186
|
+
stream = (await target(instance)(...args));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
release();
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
return holdUntilDone(stream, release);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return async (...args) => {
|
|
196
|
+
await whenReady();
|
|
197
|
+
const instance = acquire();
|
|
198
|
+
try {
|
|
199
|
+
return await target(instance)(...args);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
release();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function namespace(name) {
|
|
207
|
+
return Object.fromEntries(FORWARDED_METHODS[name].map((method) => [method, forward(name, method)]));
|
|
208
|
+
}
|
|
209
|
+
// ---- Queue ----
|
|
210
|
+
//
|
|
211
|
+
// registerRoute() is synchronous in the World interface; the platform write
|
|
212
|
+
// runs in the background and enqueues wait for it, so a message is never
|
|
213
|
+
// delivered before its prefix's route exists. Each entry settles to the
|
|
214
|
+
// failure, if any.
|
|
215
|
+
const routeWrites = [];
|
|
216
|
+
async function routesSettled() {
|
|
217
|
+
const outcomes = await Promise.all(routeWrites);
|
|
218
|
+
return outcomes.find((outcome) => outcome !== undefined);
|
|
219
|
+
}
|
|
220
|
+
const queue = async (queueName, message, opts) => {
|
|
221
|
+
parseQueueName(queueName);
|
|
222
|
+
await whenReady();
|
|
223
|
+
await routesSettled();
|
|
224
|
+
const [stored] = await client.enqueue([toWireMessage(queueName, message, opts)]);
|
|
225
|
+
return { messageId: stored.message_id };
|
|
226
|
+
};
|
|
227
|
+
const queueBatch = async (queueName, messages) => {
|
|
228
|
+
parseQueueName(queueName);
|
|
229
|
+
await whenReady();
|
|
230
|
+
await routesSettled();
|
|
231
|
+
return enqueueBatch(client, queueName, messages);
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Re-enqueues every pending or running run (`reenqueueActiveRuns`, which
|
|
235
|
+
* sends each to the namespace it was created under) in batches of up to 100.
|
|
236
|
+
* The platform drops a repeated `reenqueue:<runId>:<deploymentId>` while
|
|
237
|
+
* that message is stored and for an hour after it settles: a run asleep for
|
|
238
|
+
* days keeps its one repair message (rescheduled until the wake-up) instead
|
|
239
|
+
* of gaining a copy on every on_demand cold start.
|
|
240
|
+
*/
|
|
241
|
+
async function repair() {
|
|
242
|
+
const entries = [];
|
|
243
|
+
await reenqueueActiveRuns(world.runs, async (queueName, message, opts) => {
|
|
244
|
+
const runId = message.runId;
|
|
245
|
+
entries.push({
|
|
246
|
+
wire: toWireMessage(queueName, message, { ...opts, idempotencyKey: `reenqueue:${runId}:${deploymentId}` }),
|
|
247
|
+
});
|
|
248
|
+
return { messageId: null };
|
|
249
|
+
}, "unfazed/workflow");
|
|
250
|
+
for (const chunk of chunkMessages(entries)) {
|
|
251
|
+
await client.enqueue(chunk.map((entry) => entry.wire));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function repairOnce() {
|
|
255
|
+
repairing ??= (async () => {
|
|
256
|
+
await routesSettled();
|
|
257
|
+
await repair();
|
|
258
|
+
})().catch((error) => {
|
|
259
|
+
repairing = undefined;
|
|
260
|
+
throw error;
|
|
261
|
+
});
|
|
262
|
+
return repairing;
|
|
263
|
+
}
|
|
264
|
+
const world = {
|
|
265
|
+
specVersion: mintedSpecVersion(),
|
|
266
|
+
capabilities: UPSTREAM_STORAGE_CAPABILITIES,
|
|
267
|
+
runs: namespace("runs"),
|
|
268
|
+
steps: namespace("steps"),
|
|
269
|
+
events: namespace("events"),
|
|
270
|
+
hooks: namespace("hooks"),
|
|
271
|
+
experimental_snapshots: namespace("experimental_snapshots"),
|
|
272
|
+
streams: namespace("streams"),
|
|
273
|
+
getDeploymentId: async () => deploymentId,
|
|
274
|
+
queue,
|
|
275
|
+
queueBatch,
|
|
276
|
+
createQueueHandler: (prefix, handler) => createDeliveryHandler(stateToken, QueuePrefix.parse(prefix), handler),
|
|
277
|
+
registerRoute(prefix, path) {
|
|
278
|
+
const validPrefix = QueuePrefix.parse(prefix);
|
|
279
|
+
const validPath = parseQueueRoute(path);
|
|
280
|
+
if (byteLength(validPath) > MAX_ROUTE_PATH_BYTES || !/^[\x21-\x7e]+$/.test(validPath)) {
|
|
281
|
+
throw new TypeError(`Invalid queue route "${path}": the platform takes visible ASCII paths of at most ${MAX_ROUTE_PATH_BYTES} bytes`);
|
|
282
|
+
}
|
|
283
|
+
routeWrites.push(client.putRoute(validPrefix, validPath).then(() => undefined, (error) => {
|
|
284
|
+
console.error(`[unfazed/workflow] registerRoute(${validPrefix}, ${validPath}) failed: ${describe(error)}`);
|
|
285
|
+
return error;
|
|
286
|
+
}));
|
|
287
|
+
},
|
|
288
|
+
getRuntimeDeadline: async () => currentDeliveryDeadline(),
|
|
289
|
+
ready: () => whenReady(),
|
|
290
|
+
/**
|
|
291
|
+
* Optional: everything here also happens lazily on the first World call.
|
|
292
|
+
* Awaiting it surfaces start-up, route and re-enqueue failures.
|
|
293
|
+
*/
|
|
294
|
+
async start() {
|
|
295
|
+
await whenReady();
|
|
296
|
+
const routeError = await routesSettled();
|
|
297
|
+
await repairOnce();
|
|
298
|
+
if (routeError)
|
|
299
|
+
throw routeError;
|
|
300
|
+
},
|
|
301
|
+
close() {
|
|
302
|
+
closing ??= (async () => {
|
|
303
|
+
closed = true;
|
|
304
|
+
await Promise.allSettled([ready, repairing, ...routeWrites]);
|
|
305
|
+
if (releaseTimer)
|
|
306
|
+
clearTimeout(releaseTimer);
|
|
307
|
+
releaseTimer = undefined;
|
|
308
|
+
const current = upstream;
|
|
309
|
+
upstream = undefined;
|
|
310
|
+
await Promise.allSettled([current?.close?.(), ...retiring]);
|
|
311
|
+
await pool?.end();
|
|
312
|
+
})();
|
|
313
|
+
return closing;
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
return world;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Builds the upstream World with its internal Local World on the `node:http`
|
|
320
|
+
* transport. Its default, an undici `Agent`, has no `close()` under Bun's
|
|
321
|
+
* built-in undici, and the upstream `close()` closes that queue before the
|
|
322
|
+
* streamer: the throw would leave the streamer's `LISTEN` connection open.
|
|
323
|
+
* The flag is read synchronously during construction, so setting it around
|
|
324
|
+
* the call alone changes nothing else in the process.
|
|
325
|
+
*/
|
|
326
|
+
export function constructUpstream(pool) {
|
|
327
|
+
const previous = process.env.WORKFLOW_NODE_HTTP;
|
|
328
|
+
process.env.WORKFLOW_NODE_HTTP = "1";
|
|
329
|
+
try {
|
|
330
|
+
return createPostgresWorld({ pool });
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
if (previous === undefined)
|
|
334
|
+
delete process.env.WORKFLOW_NODE_HTTP;
|
|
335
|
+
else
|
|
336
|
+
process.env.WORKFLOW_NODE_HTTP = previous;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The same stream, calling `done` exactly once when it ends, errors or is
|
|
341
|
+
* cancelled.
|
|
342
|
+
*/
|
|
343
|
+
function holdUntilDone(stream, done) {
|
|
344
|
+
let finished = false;
|
|
345
|
+
const finish = () => {
|
|
346
|
+
if (!finished) {
|
|
347
|
+
finished = true;
|
|
348
|
+
done();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const reader = stream.getReader();
|
|
352
|
+
return new ReadableStream({
|
|
353
|
+
async pull(controller) {
|
|
354
|
+
try {
|
|
355
|
+
const { done: end, value } = await reader.read();
|
|
356
|
+
if (end) {
|
|
357
|
+
finish();
|
|
358
|
+
controller.close();
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
controller.enqueue(value);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
finish();
|
|
366
|
+
controller.error(error);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
async cancel(reason) {
|
|
370
|
+
finish();
|
|
371
|
+
await reader.cancel(reason);
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=world.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/workflow/world.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type MessageId,\n mintedSpecVersion,\n parseQueueName,\n parseQueueRoute,\n type Queue,\n QueuePrefix,\n reenqueueActiveRuns,\n type World,\n type WorldCapabilities,\n} from \"@workflow/world\";\nimport { createWorld as createPostgresWorld } from \"@workflow/world-postgres\";\nimport { Pool } from \"pg\";\nimport { DATABASE_URL_VARS, envPositiveInt, envValue, resolveDatabaseUrl, withLibpqCompat, type WorkflowEnv } from \"./config.js\";\nimport { UnfazedWorkflowError } from \"./errors.js\";\nimport type { WorkflowMigrations } from \"./migration-types.js\";\nimport { applyMigrations } from \"./migrations.js\";\nimport { embeddedMigrations } from \"./migrations.generated.js\";\nimport { applyPublicUrl } from \"./public-url.js\";\nimport {\n byteLength,\n chunkMessages,\n createDeliveryHandler,\n createQueueClient,\n currentDeliveryDeadline,\n describe,\n enqueueBatch,\n MAX_ROUTE_PATH_BYTES,\n toWireMessage,\n type WireMessage,\n type WorkflowFetch,\n} from \"./queue.js\";\n\n/** `getDeploymentId()` outside an unfazed service (no `UNFZ_DEPLOYMENT_ID`). */\nexport const LOCAL_DEPLOYMENT_ID = \"dpl_unfazed_local\";\n\nexport interface UnfazedWorldOptions {\n /** Where every default below is read from. Default: `process.env`. */\n env?: WorkflowEnv;\n /**\n * Postgres connection string for run storage and streams. Default: the first\n * of `WORKFLOW_POSTGRES_URL`, `DATABASE_URL_UNPOOLED`, `DATABASE_URL`.\n * `sslmode=require` gets `uselibpqcompat=true` so node-postgres accepts the\n * unfazed integration's self-signed certificate as libpq would.\n */\n connectionString?: string;\n /** Pool size. Default: `WORKFLOW_POSTGRES_MAX_POOL_SIZE`, else node-postgres' 10. */\n maxPoolSize?: number;\n /** The platform state plane. Default: `UNFZ_STATE_URL`. */\n stateUrl?: string;\n /** The app's state token, also the delivery token. Default: `UNFZ_STATE_TOKEN`. */\n stateToken?: string;\n /** Default: `UNFZ_DEPLOYMENT_ID`, else {@link LOCAL_DEPLOYMENT_ID}. */\n deploymentId?: string;\n /**\n * The upstream migrations to apply on start-up. Default: the copy embedded\n * in this package. The Nitro module passes the copy from the app's own\n * `@workflow/world-postgres`, so schema and storage code always match.\n */\n migrations?: WorkflowMigrations;\n /** How long one queue call keeps retrying 503s and network failures. Default 30 s. */\n retryBudgetMs?: number;\n /**\n * How long the upstream storage (and its `LISTEN` connections) outlives the\n * last open World call or stream reader. Default 2 s.\n */\n idleReleaseMs?: number;\n /**\n * The app's public URL, which `createWebhook().url` and\n * `getWorkflowMetadata().url` are built on. Default: `UNFZ_PUBLIC_URL`.\n * A bare https host (custom domain, `https://<alias>.unfz.to`) is handed to\n * Workflow SDK through `process.env.VERCEL_URL` when that is unset, the\n * only base URL the SDK reads; a path-form URL cannot be. `false` leaves\n * `VERCEL_URL` alone.\n */\n publicUrl?: string | false;\n /** Injection point for tests. Default: global `fetch`. */\n fetch?: WorkflowFetch;\n}\n\n/** The World `createWorld()` returns. */\nexport type UnfazedWorld = World &\n Required<Pick<Queue, \"queueBatch\" | \"registerRoute\">> & {\n getRuntimeDeadline(): Promise<Date | undefined>;\n /**\n * Resolves once start-up succeeded (schema migrated, platform queue\n * reachable), starting it if needed; rejects with the start-up error.\n * Unlike {@link start} it does not wait for route writes or the re-enqueue.\n */\n ready(): Promise<void>;\n start(): Promise<void>;\n close(): Promise<void>;\n };\n\n/**\n * The capabilities `@workflow/world-postgres` declares for its storage (all\n * but `invoke`, a queue feature the platform queue does not offer). Declared\n * here because the upstream World is only built on demand; a test checks the\n * two stay equal.\n */\nexport const UPSTREAM_STORAGE_CAPABILITIES: WorldCapabilities = {\n hookRetention: { active: true },\n dynamicWorkflowCode: true,\n hookResumeDedup: true,\n hookForceClaim: true,\n inBandFence: true,\n};\n\n/**\n * The storage and stream methods of `@workflow/world-postgres` this World\n * forwards. Listed rather than discovered because the upstream World is only\n * built on demand; a test fails when upstream has a method that is missing\n * here, so a new one gets a decision instead of silently going missing.\n */\nexport const FORWARDED_METHODS = {\n runs: [\"get\", \"getMany\", \"list\", \"waitForTerminalStatus\", \"experimentalSetAttributes\"],\n steps: [\"get\", \"list\"],\n events: [\"create\", \"get\", \"list\", \"listByCorrelationId\"],\n hooks: [\"get\", \"getByToken\", \"list\"],\n experimental_snapshots: [\"save\", \"load\", \"delete\"],\n streams: [\"write\", \"writeMulti\", \"close\", \"getChunks\", \"getInfo\", \"get\", \"list\"],\n} as const;\n\n/** Top-level members of the upstream World, and what happens to each. */\nexport const UPSTREAM_MEMBERS = {\n forwarded: Object.keys(FORWARDED_METHODS),\n declared: [\"specVersion\", \"capabilities\", \"streamFlushIntervalMs\"],\n replacedByPlatformQueue: [\"queue\", \"createQueueHandler\", \"getDeploymentId\", \"registerRoute\", \"registerHandler\", \"invoke\", \"start\", \"close\"],\n} as const;\n\ntype Upstream = ReturnType<typeof createPostgresWorld>;\ntype Namespace = keyof typeof FORWARDED_METHODS;\ntype AnyFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Creates the unfazed World for Workflow SDK.\n *\n * Storage, streams and `runs.waitForTerminalStatus` are the upstream\n * `@workflow/world-postgres` code over the app's own Postgres; its\n * graphile-worker queue is never started. The queue is the unfazed\n * platform's: `queue()` enqueues through the worker's state plane, and the\n * platform delivers each message to the flow route, cold-starting an\n * `on_demand` VM, authenticated by a header public ingress strips.\n *\n * Nothing here needs a warm process. `createWorld()` is synchronous and opens\n * nothing: install it with `setWorld(createWorld())` before the server serves.\n * It does point the SDK at the app's public URL (see `publicUrl`), so webhook\n * URLs are right without app code.\n * The first World call of a process applies the upstream migrations (a no-op\n * when current) and probes the queue, and every call waits for that, so a\n * request that arrives during a cold start waits instead of failing. Every\n * wake-up is a platform delivery; Postgres connections, including the\n * `LISTEN` ones behind stream readers and `waitForTerminalStatus`, are held\n * only while a World call or a stream reader is open, plus a short grace.\n */\nexport function createWorld(options: UnfazedWorldOptions = {}): UnfazedWorld {\n const env = options.env ?? process.env;\n const rawUrl = options.connectionString ?? resolveDatabaseUrl(env);\n const connectionString = rawUrl ? withLibpqCompat(rawUrl) : undefined;\n const deploymentId = options.deploymentId ?? envValue(env, \"UNFZ_DEPLOYMENT_ID\") ?? LOCAL_DEPLOYMENT_ID;\n const stateToken = options.stateToken ?? envValue(env, \"UNFZ_STATE_TOKEN\");\n const migrations = options.migrations ?? embeddedMigrations;\n const maxPoolSize = options.maxPoolSize ?? envPositiveInt(env, \"WORKFLOW_POSTGRES_MAX_POOL_SIZE\");\n const idleReleaseMs = options.idleReleaseMs ?? 2_000;\n\n // Before the server serves: the SDK reads the base URL per invocation.\n if (options.publicUrl !== false) applyPublicUrl(options.publicUrl ?? envValue(env, \"UNFZ_PUBLIC_URL\"));\n\n const client = createQueueClient({\n stateUrl: options.stateUrl ?? envValue(env, \"UNFZ_STATE_URL\"),\n stateToken,\n fetch: options.fetch ?? ((input, init) => fetch(input, init)),\n retryBudgetMs: options.retryBudgetMs ?? 30_000,\n requestTimeoutMs: 15_000,\n });\n\n // node-postgres connects lazily, so constructing the pool opens nothing. Idle\n // connections close after 5 s, well inside an on_demand VM's idle window, so\n // a reaped VM leaves no half-open sessions behind on the database.\n let pool: Pool | undefined;\n function getPool(): Pool {\n if (!pool) {\n pool = new Pool({\n connectionString,\n idleTimeoutMillis: 5_000,\n ...(maxPoolSize ? { max: maxPoolSize } : {}),\n });\n // An `error` with no listener ends the process. pg-pool re-emits an idle\n // client's error on the pool, but a checked-out client only on itself.\n pool.on(\"error\", (error) => console.warn(`[unfazed/workflow] idle database connection closed: ${error.message}`));\n pool.on(\"connect\", (connection) => {\n connection.on(\"error\", (error) => console.warn(`[unfazed/workflow] database connection error: ${error.message}`));\n });\n }\n return pool;\n }\n\n let closed = false;\n let ready: Promise<void> | undefined;\n let repairing: Promise<void> | undefined;\n let closing: Promise<void> | undefined;\n\n // ---- Start-up: schema and queue both usable. Every World call waits. ----\n function whenReady(): Promise<void> {\n if (closed) return Promise.reject(new Error(\"unfazed/workflow: the World is closed\"));\n ready ??= (async () => {\n if (!connectionString) {\n throw new UnfazedWorkflowError(\n \"no_database\",\n \"unfazed/workflow: no Postgres database is bound to this app. Workflow runs are stored in \" +\n \"the app's own Postgres: bind a Postgres or Neon integration to the app and redeploy so \" +\n `DATABASE_URL is set, or set WORKFLOW_POSTGRES_URL (checked ${DATABASE_URL_VARS.join(\", \")}).`,\n );\n }\n await Promise.all([applyMigrations(getPool(), migrations), client.stats()]);\n })().then(\n () => {\n // Once per process: put active runs back on the queue in case a\n // message went missing. Deduplicated by key, never needed for\n // correctness, so it runs beside the call that started up.\n void repairOnce().catch((error: unknown) =>\n console.warn(`[unfazed/workflow] re-enqueueing active runs failed: ${describe(error)}`),\n );\n },\n (error: unknown) => {\n ready = undefined; // the next call tries again\n throw error;\n },\n );\n return ready;\n }\n\n // ---- The upstream storage, built on demand and released when idle. ----\n //\n // `@workflow/world-postgres` opens its stream `LISTEN` connection as soon as\n // it is constructed, and `waitForTerminalStatus` keeps another once used.\n // Held for the life of the process, those would outlive every reaped VM as\n // half-open sessions on the database. So an instance lives only while World\n // calls or stream readers are open (plus `idleReleaseMs`), and the next call\n // builds a fresh one. Built only after start-up, when the database answered.\n let upstream: Upstream | undefined;\n let activity = 0;\n let releaseTimer: ReturnType<typeof setTimeout> | undefined;\n const retiring = new Set<Promise<void>>();\n\n function acquire(): Upstream {\n activity++;\n if (releaseTimer) {\n clearTimeout(releaseTimer);\n releaseTimer = undefined;\n }\n upstream ??= constructUpstream(getPool());\n return upstream;\n }\n\n function release(): void {\n activity--;\n if (activity > 0 || releaseTimer || !upstream) return;\n releaseTimer = setTimeout(() => {\n releaseTimer = undefined;\n if (activity > 0 || !upstream) return;\n const idle = upstream;\n upstream = undefined;\n const done: Promise<void> = Promise.resolve(idle.close?.())\n .catch((error: unknown) => console.warn(`[unfazed/workflow] releasing idle storage failed: ${describe(error)}`))\n .finally(() => retiring.delete(done));\n retiring.add(done);\n }, idleReleaseMs);\n (releaseTimer as { unref?: () => void }).unref?.();\n }\n\n function forward(namespace: Namespace, method: string): AnyFn {\n const target = (instance: Upstream): AnyFn => {\n const ns = instance[namespace] as unknown as Record<string, AnyFn | undefined> | undefined;\n const fn = ns?.[method];\n if (typeof fn !== \"function\") {\n throw new TypeError(`unfazed/workflow: the installed @workflow/world-postgres has no ${namespace}.${method}()`);\n }\n return fn.bind(ns);\n };\n if (namespace === \"streams\" && method === \"get\") {\n // A reader outlives the call that opened it: hold the instance (and its\n // LISTEN connection) until the stream ends or is cancelled.\n return async (...args) => {\n await whenReady();\n const instance = acquire();\n let stream: ReadableStream<Uint8Array>;\n try {\n stream = (await target(instance)(...args)) as ReadableStream<Uint8Array>;\n } catch (error) {\n release();\n throw error;\n }\n return holdUntilDone(stream, release);\n };\n }\n return async (...args) => {\n await whenReady();\n const instance = acquire();\n try {\n return await target(instance)(...args);\n } finally {\n release();\n }\n };\n }\n\n function namespace<N extends Namespace>(name: N): Record<string, AnyFn> {\n return Object.fromEntries(FORWARDED_METHODS[name].map((method) => [method, forward(name, method)]));\n }\n\n // ---- Queue ----\n //\n // registerRoute() is synchronous in the World interface; the platform write\n // runs in the background and enqueues wait for it, so a message is never\n // delivered before its prefix's route exists. Each entry settles to the\n // failure, if any.\n const routeWrites: Promise<unknown>[] = [];\n async function routesSettled(): Promise<unknown> {\n const outcomes = await Promise.all(routeWrites);\n return outcomes.find((outcome) => outcome !== undefined);\n }\n\n const queue: Queue[\"queue\"] = async (queueName, message, opts) => {\n parseQueueName(queueName);\n await whenReady();\n await routesSettled();\n const [stored] = await client.enqueue([toWireMessage(queueName, message, opts)]);\n return { messageId: stored!.message_id as MessageId };\n };\n\n const queueBatch: NonNullable<Queue[\"queueBatch\"]> = async (queueName, messages) => {\n parseQueueName(queueName);\n await whenReady();\n await routesSettled();\n return enqueueBatch(client, queueName, messages);\n };\n\n /**\n * Re-enqueues every pending or running run (`reenqueueActiveRuns`, which\n * sends each to the namespace it was created under) in batches of up to 100.\n * The platform drops a repeated `reenqueue:<runId>:<deploymentId>` while\n * that message is stored and for an hour after it settles: a run asleep for\n * days keeps its one repair message (rescheduled until the wake-up) instead\n * of gaining a copy on every on_demand cold start.\n */\n async function repair(): Promise<void> {\n const entries: { wire: WireMessage }[] = [];\n await reenqueueActiveRuns(\n world.runs,\n async (queueName, message, opts) => {\n const runId = (message as { runId?: string }).runId;\n entries.push({\n wire: toWireMessage(queueName, message, { ...opts, idempotencyKey: `reenqueue:${runId}:${deploymentId}` }),\n });\n return { messageId: null };\n },\n \"unfazed/workflow\",\n );\n for (const chunk of chunkMessages(entries)) {\n await client.enqueue(chunk.map((entry) => entry.wire));\n }\n }\n\n function repairOnce(): Promise<void> {\n repairing ??= (async () => {\n await routesSettled();\n await repair();\n })().catch((error: unknown) => {\n repairing = undefined;\n throw error;\n });\n return repairing;\n }\n\n const world: UnfazedWorld = {\n specVersion: mintedSpecVersion(),\n capabilities: UPSTREAM_STORAGE_CAPABILITIES,\n runs: namespace(\"runs\") as unknown as World[\"runs\"],\n steps: namespace(\"steps\") as unknown as World[\"steps\"],\n events: namespace(\"events\") as unknown as World[\"events\"],\n hooks: namespace(\"hooks\") as unknown as World[\"hooks\"],\n experimental_snapshots: namespace(\"experimental_snapshots\") as unknown as NonNullable<World[\"experimental_snapshots\"]>,\n streams: namespace(\"streams\") as unknown as World[\"streams\"],\n\n getDeploymentId: async () => deploymentId,\n queue,\n queueBatch,\n createQueueHandler: (prefix, handler) => createDeliveryHandler(stateToken, QueuePrefix.parse(prefix), handler),\n registerRoute(prefix, path) {\n const validPrefix = QueuePrefix.parse(prefix);\n const validPath = parseQueueRoute(path);\n if (byteLength(validPath) > MAX_ROUTE_PATH_BYTES || !/^[\\x21-\\x7e]+$/.test(validPath)) {\n throw new TypeError(\n `Invalid queue route \"${path}\": the platform takes visible ASCII paths of at most ${MAX_ROUTE_PATH_BYTES} bytes`,\n );\n }\n routeWrites.push(\n client.putRoute(validPrefix, validPath).then(\n () => undefined,\n (error: unknown) => {\n console.error(`[unfazed/workflow] registerRoute(${validPrefix}, ${validPath}) failed: ${describe(error)}`);\n return error;\n },\n ),\n );\n },\n getRuntimeDeadline: async () => currentDeliveryDeadline(),\n ready: () => whenReady(),\n\n /**\n * Optional: everything here also happens lazily on the first World call.\n * Awaiting it surfaces start-up, route and re-enqueue failures.\n */\n async start() {\n await whenReady();\n const routeError = await routesSettled();\n await repairOnce();\n if (routeError) throw routeError;\n },\n close() {\n closing ??= (async () => {\n closed = true;\n await Promise.allSettled([ready, repairing, ...routeWrites]);\n if (releaseTimer) clearTimeout(releaseTimer);\n releaseTimer = undefined;\n const current = upstream;\n upstream = undefined;\n await Promise.allSettled([current?.close?.(), ...retiring]);\n await pool?.end();\n })();\n return closing;\n },\n };\n return world;\n}\n\n/**\n * Builds the upstream World with its internal Local World on the `node:http`\n * transport. Its default, an undici `Agent`, has no `close()` under Bun's\n * built-in undici, and the upstream `close()` closes that queue before the\n * streamer: the throw would leave the streamer's `LISTEN` connection open.\n * The flag is read synchronously during construction, so setting it around\n * the call alone changes nothing else in the process.\n */\nexport function constructUpstream(pool: Pool): Upstream {\n const previous = process.env.WORKFLOW_NODE_HTTP;\n process.env.WORKFLOW_NODE_HTTP = \"1\";\n try {\n return createPostgresWorld({ pool });\n } finally {\n if (previous === undefined) delete process.env.WORKFLOW_NODE_HTTP;\n else process.env.WORKFLOW_NODE_HTTP = previous;\n }\n}\n\n/**\n * The same stream, calling `done` exactly once when it ends, errors or is\n * cancelled.\n */\nfunction holdUntilDone(stream: ReadableStream<Uint8Array>, done: () => void): ReadableStream<Uint8Array> {\n let finished = false;\n const finish = () => {\n if (!finished) {\n finished = true;\n done();\n }\n };\n const reader = stream.getReader();\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done: end, value } = await reader.read();\n if (end) {\n finish();\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (error) {\n finish();\n controller.error(error);\n }\n },\n async cancel(reason) {\n finish();\n await reader.cancel(reason);\n },\n });\n}\n"]}
|