@lostgradient/weft 0.10.0 → 0.12.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 +10 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +4671 -0
- package/dist/cli/generated/operation-client.generated.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli-main.js +227 -401
- package/dist/client/http-operations.js +7 -1
- package/dist/client/http-request.d.ts +23 -1
- package/dist/client/http-request.js +12 -3
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +7 -0
- package/dist/connection.d.ts +8 -0
- package/dist/connection.js +41 -17
- package/dist/core/checkpoint/serialization.js +2 -2
- package/dist/core/engine/callback-creators-core.js +2 -1
- package/dist/core/engine/disposal.js +4 -0
- package/dist/core/engine/engine-leak-warnings.d.ts +2 -0
- package/dist/core/engine/engine-leak-warnings.js +7 -1
- package/dist/core/engine/index.d.ts +49 -3
- package/dist/core/engine/index.js +19 -3
- package/dist/core/engine/inline-parking.js +14 -1
- package/dist/core/engine/internals.d.ts +21 -0
- package/dist/core/engine/lifecycle/shared.d.ts +25 -0
- package/dist/core/engine/lifecycle/transition.js +19 -9
- package/dist/core/engine/operations-time.js +8 -28
- package/dist/core/engine/sleep-timer-acknowledgements.d.ts +15 -0
- package/dist/core/engine/sleep-timer-acknowledgements.js +144 -0
- package/dist/core/engine/termination/cleanup.js +3 -0
- package/dist/core/types/definition-schema-to-json.d.ts +11 -0
- package/dist/core/types/definition-schema-to-json.js +11 -2
- package/dist/http.js +2 -0
- package/dist/indexeddb.js +1 -0
- package/dist/json-schema.js +3 -3
- package/dist/mcp/cli.js +63 -61
- package/dist/mcp/http.d.ts +7 -2
- package/dist/mcp/session.d.ts +3 -3
- package/dist/mcp/session.js +3 -2
- package/dist/mcp/stdio.d.ts +7 -2
- package/dist/observability/index.js +2 -2
- package/dist/runtime/portable.d.ts +33 -0
- package/dist/runtime/portable.js +17 -6
- package/dist/server/engine-event-feed-backend.d.ts +22 -3
- package/dist/server/fleet-event-feed.d.ts +65 -0
- package/dist/server/handler/index.d.ts +11 -2
- package/dist/server/handler/index.js +7 -0
- package/dist/server/handler.js +1 -56
- package/dist/server/index.d.ts +12 -1
- package/dist/server/index.js +87 -64
- package/dist/server/operations/submit-review-decision.d.ts +2 -0
- package/dist/server/operations/submit-review-decision.js +19 -2
- package/dist/server/replay-live-feed-internals.d.ts +19 -0
- package/dist/server/replay-live-feed-internals.js +76 -0
- package/dist/server/runtime/authentication-bridge.d.ts +2 -2
- package/dist/server/serve-internals.js +3 -3
- package/dist/server/workflow-event-feed.d.ts +100 -11
- package/dist/server/workflow-event-feed.js +1 -140
- package/dist/service-worker/index.d.ts +8 -2
- package/dist/service-worker/index.js +37 -56
- package/dist/service-worker/scheduler.js +1 -0
- package/dist/service-worker/setup.d.ts +6 -2
- package/dist/storage/auto.js +1 -1
- package/dist/storage/bun-sql.js +3 -3
- package/dist/storage/cloudflare-durable-object-sql.d.ts +75 -0
- package/dist/storage/cloudflare-durable-object-sql.js +0 -0
- package/dist/storage/cloudflare-value-codec.d.ts +65 -0
- package/dist/storage/cloudflare-value-codec.js +46 -0
- package/dist/storage/cloudflare.d.ts +140 -0
- package/dist/storage/cloudflare.js +127 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/http.js +200 -2
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/indexeddb.js +311 -1
- package/dist/storage/interface.js +1 -1
- package/dist/storage/lazy-postgres-pool.d.ts +38 -0
- package/dist/storage/lazy-postgres-pool.js +38 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon-batch.d.ts +8 -8
- package/dist/storage/neon.d.ts +26 -79
- package/dist/storage/neon.js +4 -4
- package/dist/storage/node-sqlite.js +3 -3
- package/dist/storage/postgres-key-value-queries.d.ts +7 -1
- package/dist/storage/postgres-key-value-queries.js +2 -3
- package/dist/storage/postgres-key-value-storage.d.ts +110 -0
- package/dist/storage/postgres-key-value-storage.js +204 -0
- package/dist/storage/postgres.d.ts +59 -0
- package/dist/storage/postgres.js +15 -0
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/sql-identifier.d.ts +35 -0
- package/dist/storage/sql-identifier.js +5 -0
- package/dist/storage/sqlite-key-value-queries.d.ts +11 -6
- package/dist/storage/sqlite-key-value-queries.js +3 -3
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/storage/typed-storage.js +138 -2
- package/dist/storage/web-extension.js +298 -1
- package/dist/testing/index.js +21 -60
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -0
- package/dist/worker/protocol.js +1 -1
- package/package.json +23 -5
|
@@ -6,6 +6,9 @@ function isJsonRpcFailure(value) {
|
|
|
6
6
|
function isJsonRpcSuccess(value) {
|
|
7
7
|
return typeof value === "object" && value !== null && "result" in value;
|
|
8
8
|
}
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
9
12
|
export function httpClientCatalogTransport(baseUrl, headers) {
|
|
10
13
|
const endpoint = `${baseUrl}/jsonrpc`;
|
|
11
14
|
return async (operationName, input) => {
|
|
@@ -27,7 +30,10 @@ export function httpClientCatalogTransport(baseUrl, headers) {
|
|
|
27
30
|
}
|
|
28
31
|
if (isJsonRpcFailure(body)) {
|
|
29
32
|
const { message, data } = body.error, httpStatus = typeof data?.httpStatus === "number" ? data.httpStatus : response.status, faultCode = isFaultCode(data?.weftCode) ? data.weftCode : void 0;
|
|
30
|
-
throw new HttpClientError(httpStatus, message, {
|
|
33
|
+
throw new HttpClientError(httpStatus, message, {
|
|
34
|
+
faultCode,
|
|
35
|
+
data: isRecord(data) ? data : void 0
|
|
36
|
+
});
|
|
31
37
|
}
|
|
32
38
|
if (isJsonRpcSuccess(body))
|
|
33
39
|
return body.result;
|
|
@@ -67,7 +67,11 @@ export declare function resolveHttpClientConnection(options: HttpClientOptions):
|
|
|
67
67
|
* the wire fault `code` is surfaced as {@link HttpClientError.faultCode} and a
|
|
68
68
|
* derived {@link HttpClientError.category} so callers can branch programmatically
|
|
69
69
|
* instead of string-matching `message`. Both are `undefined` when the body is a
|
|
70
|
-
* plain `{ error: string }` or carries no recognized code.
|
|
70
|
+
* plain `{ error: string }` or carries no recognized code. The fault's typed
|
|
71
|
+
* `data` payload (e.g. `InvalidParams`'s `issues`, `NotFound`/`Conflict`'s
|
|
72
|
+
* `resource`/`identifier`) is surfaced verbatim as {@link HttpClientError.data}
|
|
73
|
+
* when the body carries a structured `data` object; `undefined` otherwise —
|
|
74
|
+
* including for a masked `EngineFailure`, whose flat body carries no `data`.
|
|
71
75
|
*
|
|
72
76
|
* @example
|
|
73
77
|
* ```ts
|
|
@@ -110,9 +114,27 @@ export declare class HttpClientError extends WeftError<'HttpClientError'> {
|
|
|
110
114
|
* carried only the coarse {@link faultCode} (most faults) or no structured body.
|
|
111
115
|
*/
|
|
112
116
|
readonly weftCode?: WeftErrorCode | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* The fault's wire `data` payload, when the response carried a structured
|
|
119
|
+
* body (`{ error: { data } }` for REST, `error.data` for JSON-RPC). Shape is
|
|
120
|
+
* fault-code-dependent — see {@link FaultCode} and the server's
|
|
121
|
+
* `OperationFault` per-code `data` union for what each code carries (e.g.
|
|
122
|
+
* `InvalidParams.data.issues`, `NotFound.data.resource`). `undefined` for
|
|
123
|
+
* plain-string error bodies, bodies with no `data` field, or a `data` field
|
|
124
|
+
* that is not a JSON object.
|
|
125
|
+
*
|
|
126
|
+
* Over JSON-RPC-over-HTTP this is the raw envelope `error.data` verbatim
|
|
127
|
+
* (see `httpClientCatalogTransport` in `http-operations.ts`), so it also
|
|
128
|
+
* carries the envelope's own `weftCode` (the coarse {@link FaultCode}, not
|
|
129
|
+
* a fine-grained {@link WeftErrorCode}) and `httpStatus` keys alongside the
|
|
130
|
+
* per-code payload — those two are not part of the `OperationFault` data
|
|
131
|
+
* union.
|
|
132
|
+
*/
|
|
133
|
+
readonly data?: Readonly<Record<string, unknown>> | undefined;
|
|
113
134
|
constructor(status: number, message: string, options?: {
|
|
114
135
|
faultCode?: FaultCode | undefined;
|
|
115
136
|
weftCode?: WeftErrorCode | undefined;
|
|
137
|
+
data?: Readonly<Record<string, unknown>> | undefined;
|
|
116
138
|
});
|
|
117
139
|
}
|
|
118
140
|
export declare function request<T>(baseUrl: string, path: string, baseHeaders: Record<string, string>, options?: RequestInit): Promise<T>;
|
|
@@ -17,12 +17,14 @@ export class HttpClientError extends WeftError {
|
|
|
17
17
|
faultCode;
|
|
18
18
|
category;
|
|
19
19
|
weftCode;
|
|
20
|
+
data;
|
|
20
21
|
constructor(status, message, options) {
|
|
21
22
|
super("HttpClientError", message);
|
|
22
23
|
this.status = status;
|
|
23
24
|
this.faultCode = options?.faultCode;
|
|
24
25
|
this.category = options?.faultCode === void 0 ? void 0 : failureCategoryForFaultCode(options.faultCode);
|
|
25
26
|
this.weftCode = options?.weftCode;
|
|
27
|
+
this.data = options?.data;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
function buildRequestHeaders(baseHeaders, options) {
|
|
@@ -53,11 +55,18 @@ function weftCodeFromData(data) {
|
|
|
53
55
|
const candidate = data.weftCode;
|
|
54
56
|
return isWeftErrorCode(candidate) ? candidate : void 0;
|
|
55
57
|
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
56
61
|
async function parseErrorBody(response) {
|
|
57
62
|
try {
|
|
58
63
|
const body = await response.json();
|
|
59
64
|
if (isStructuredErrorBody(body)) {
|
|
60
|
-
const { code, message, data } = body.error, weftCode = weftCodeFromData(data), base =
|
|
65
|
+
const { code, message, data } = body.error, weftCode = weftCodeFromData(data), base = { message };
|
|
66
|
+
if (weftCode !== void 0)
|
|
67
|
+
base.weftCode = weftCode;
|
|
68
|
+
if (isRecord(data))
|
|
69
|
+
base.data = data;
|
|
61
70
|
return isFaultCode(code) ? { ...base, faultCode: code } : base;
|
|
62
71
|
}
|
|
63
72
|
if (isFlatErrorBody(body) && body.error) {
|
|
@@ -74,8 +83,8 @@ export async function request(baseUrl, path, baseHeaders, options) {
|
|
|
74
83
|
if (response.status === 404 && (!options?.method || options.method === "GET"))
|
|
75
84
|
return null;
|
|
76
85
|
if (!response.ok) {
|
|
77
|
-
const { message, faultCode, weftCode } = await parseErrorBody(response);
|
|
78
|
-
throw new HttpClientError(response.status, message, { faultCode, weftCode });
|
|
86
|
+
const { message, faultCode, weftCode, data } = await parseErrorBody(response);
|
|
87
|
+
throw new HttpClientError(response.status, message, { faultCode, weftCode, data });
|
|
79
88
|
}
|
|
80
89
|
if (response.status === 204)
|
|
81
90
|
return;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module client/index
|
|
9
9
|
*/
|
|
10
|
+
export { isWeftError, isWeftErrorCode, isWeftErrorLike, isWeftFault, WeftError, } from '../core/weft-error.ts';
|
|
11
|
+
export type { WeftErrorCode } from '../core/weft-error.ts';
|
|
10
12
|
export type { WorkflowEventStreamOptions, WorkflowEventTransport } from './event-stream-options.ts';
|
|
11
13
|
export type { WorkflowEventTail } from './event-tail.ts';
|
|
12
14
|
export { HttpClient } from './http-client.ts';
|
package/dist/client/index.js
CHANGED
package/dist/connection.d.ts
CHANGED
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* A profile token is only applied when neither an explicit `server` option nor
|
|
15
15
|
* `WEFT_ADDR` redirected the request to a different destination.
|
|
16
16
|
*
|
|
17
|
+
* This module is imported from `@lostgradient/weft/client` (browser-reachable),
|
|
18
|
+
* so it must stay statically free of `node:*` and Bun-only imports. Environment
|
|
19
|
+
* variables go through {@link readEnvironmentVariable}; `~/.weft/config` and the
|
|
20
|
+
* run lockfile are read through {@link tryLoadNodeBuiltin}, which resolves
|
|
21
|
+
* `node:fs`/`node:fs/promises` via `process.getBuiltinModule` instead of a
|
|
22
|
+
* static import. Both return `undefined` outside Bun/Node, so a browser caller
|
|
23
|
+
* that supplies explicit `server`/`token` never touches either.
|
|
24
|
+
*
|
|
17
25
|
* @module connection
|
|
18
26
|
*/
|
|
19
27
|
/**
|
package/dist/connection.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
1
|
+
import { isBunRuntime, readEnvironmentVariable, tryLoadNodeBuiltin } from "./runtime/portable.js";
|
|
3
2
|
export const DEFAULT_WEFT_ADDRESS = "http://localhost:7233";
|
|
4
3
|
|
|
5
4
|
export class ConnectionConfigurationError extends Error {
|
|
@@ -9,7 +8,7 @@ export class ConnectionConfigurationError extends Error {
|
|
|
9
8
|
}
|
|
10
9
|
}
|
|
11
10
|
export function resolveConnection(options = {}) {
|
|
12
|
-
const context = resolveConnectionContext(options), server = resolveServerString(context), fallbackProfile = profileForToken(context, server), token = resolveToken(options.token ??
|
|
11
|
+
const context = resolveConnectionContext(options), server = resolveServerString(context), fallbackProfile = profileForToken(context, server), token = resolveToken(options.token ?? readEnvironmentVariable("WEFT_TOKEN"), fallbackProfile);
|
|
13
12
|
return {
|
|
14
13
|
server: parseServerUrl(server),
|
|
15
14
|
...token === void 0 ? {} : { token }
|
|
@@ -26,7 +25,7 @@ function parseServerUrl(server) {
|
|
|
26
25
|
function profileForToken(context, resolvedServer) {
|
|
27
26
|
if (context.profile === void 0)
|
|
28
27
|
return;
|
|
29
|
-
if (!(context.options.server !== void 0 ||
|
|
28
|
+
if (!(context.options.server !== void 0 || readEnvironmentVariable("WEFT_ADDR") !== void 0))
|
|
30
29
|
return context.profile;
|
|
31
30
|
const profileServer = context.profile.server;
|
|
32
31
|
if (profileServer === void 0)
|
|
@@ -44,7 +43,7 @@ function sameDestination(a, b) {
|
|
|
44
43
|
return left.origin === right.origin && left.pathname.replace(/\/+$/, "") === right.pathname.replace(/\/+$/, "");
|
|
45
44
|
}
|
|
46
45
|
function resolveConnectionContext(options) {
|
|
47
|
-
const configuration = readWeftConfiguration(), profileName = options.profile ??
|
|
46
|
+
const configuration = readWeftConfiguration(), profileName = options.profile ?? readEnvironmentVariable("WEFT_PROFILE") ?? configuration.defaultProfile, profile = profileName === void 0 ? void 0 : configuration.profiles?.[profileName], runLockfile = options.includeRunLockfile === !1 ? void 0 : readRunLockfile();
|
|
48
47
|
return {
|
|
49
48
|
options,
|
|
50
49
|
...profile === void 0 ? {} : { profile },
|
|
@@ -52,12 +51,26 @@ function resolveConnectionContext(options) {
|
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
53
|
function resolveServerString(context) {
|
|
55
|
-
return context.options.server ??
|
|
54
|
+
return context.options.server ?? readEnvironmentVariable("WEFT_ADDR") ?? context.profile?.server ?? context.runLockfile?.server ?? context.runLockfile?.url ?? DEFAULT_WEFT_ADDRESS;
|
|
55
|
+
}
|
|
56
|
+
function loadFsModule() {
|
|
57
|
+
return tryLoadNodeBuiltin("node:fs");
|
|
58
|
+
}
|
|
59
|
+
function loadFsPromisesModule() {
|
|
60
|
+
return tryLoadNodeBuiltin("node:fs/promises");
|
|
56
61
|
}
|
|
57
62
|
export async function writeRunLockfile(server) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
const fsPromises = loadFsPromisesModule();
|
|
64
|
+
if (fsPromises === void 0)
|
|
65
|
+
throw Error("writeRunLockfile requires Bun or Node 22.5+ (process.getBuiltinModule); not available in this runtime.");
|
|
66
|
+
await fsPromises.mkdir(weftHome(), { recursive: !0 });
|
|
67
|
+
const contents = `${JSON.stringify({ server }, null, 2)}
|
|
68
|
+
`;
|
|
69
|
+
if (isBunRuntime()) {
|
|
70
|
+
await Bun.write(runLockfilePath(), contents);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await fsPromises.writeFile(runLockfilePath(), contents, "utf8");
|
|
61
74
|
}
|
|
62
75
|
export async function removeRunLockfile(server) {
|
|
63
76
|
const lockfile = readRunLockfile();
|
|
@@ -65,15 +78,23 @@ export async function removeRunLockfile(server) {
|
|
|
65
78
|
return;
|
|
66
79
|
if ((lockfile.server ?? lockfile.url) !== server)
|
|
67
80
|
return;
|
|
68
|
-
|
|
81
|
+
const fsPromises = loadFsPromisesModule();
|
|
82
|
+
if (fsPromises === void 0)
|
|
83
|
+
return;
|
|
84
|
+
await fsPromises.rm(runLockfilePath(), { force: !0 });
|
|
69
85
|
}
|
|
70
86
|
function readWeftConfiguration() {
|
|
87
|
+
if (!isBunRuntime())
|
|
88
|
+
return {};
|
|
89
|
+
const fs = loadFsModule();
|
|
90
|
+
if (fs === void 0)
|
|
91
|
+
return {};
|
|
71
92
|
const path = configurationPath();
|
|
72
|
-
if (!existsSync(path))
|
|
93
|
+
if (!fs.existsSync(path))
|
|
73
94
|
return {};
|
|
74
95
|
let parsed;
|
|
75
96
|
try {
|
|
76
|
-
parsed = Bun.TOML.parse(readFileSync(path, "utf8"));
|
|
97
|
+
parsed = Bun.TOML.parse(fs.readFileSync(path, "utf8"));
|
|
77
98
|
} catch (error) {
|
|
78
99
|
const message = error instanceof Error ? error.message : String(error);
|
|
79
100
|
throw new ConnectionConfigurationError(`Failed to read connection configuration at ${path}: ${message}`);
|
|
@@ -81,10 +102,13 @@ function readWeftConfiguration() {
|
|
|
81
102
|
return normalizeConfiguration(parsed);
|
|
82
103
|
}
|
|
83
104
|
function readRunLockfile() {
|
|
105
|
+
const fs = loadFsModule();
|
|
106
|
+
if (fs === void 0)
|
|
107
|
+
return;
|
|
84
108
|
const path = runLockfilePath();
|
|
85
|
-
if (!existsSync(path))
|
|
109
|
+
if (!fs.existsSync(path))
|
|
86
110
|
return;
|
|
87
|
-
const text = readFileSync(path, "utf8").trim();
|
|
111
|
+
const text = fs.readFileSync(path, "utf8").trim();
|
|
88
112
|
if (text === "")
|
|
89
113
|
return;
|
|
90
114
|
try {
|
|
@@ -137,11 +161,11 @@ function normalizeRunLockfile(value) {
|
|
|
137
161
|
function resolveToken(token, profile) {
|
|
138
162
|
const directToken = token ?? profile?.token;
|
|
139
163
|
if (directToken?.startsWith("env:"))
|
|
140
|
-
return
|
|
164
|
+
return readEnvironmentVariable(directToken.slice(4));
|
|
141
165
|
if (directToken !== void 0)
|
|
142
166
|
return directToken;
|
|
143
167
|
if (profile?.tokenEnv !== void 0)
|
|
144
|
-
return
|
|
168
|
+
return readEnvironmentVariable(profile.tokenEnv);
|
|
145
169
|
return;
|
|
146
170
|
}
|
|
147
171
|
function configurationPath() {
|
|
@@ -151,7 +175,7 @@ function runLockfilePath() {
|
|
|
151
175
|
return `${weftHome()}/run`;
|
|
152
176
|
}
|
|
153
177
|
function weftHome() {
|
|
154
|
-
return
|
|
178
|
+
return readEnvironmentVariable("WEFT_HOME") ?? `${readEnvironmentVariable("HOME") ?? "."}/.weft`;
|
|
155
179
|
}
|
|
156
180
|
function stringValue(value) {
|
|
157
181
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -29,7 +29,7 @@ export function validateCheckpointShape(value) {
|
|
|
29
29
|
validateAccumulatedResultReplayWatermark(record);
|
|
30
30
|
validateWorkerReplaySignatures(record);
|
|
31
31
|
validateWorkerReplayFailures(record);
|
|
32
|
-
|
|
32
|
+
dropRetiredPendingSignals(record);
|
|
33
33
|
assertRecordField(record, "searchAttributes");
|
|
34
34
|
assertStringField(record, "version");
|
|
35
35
|
assertNumberField(record, "createdAt");
|
|
@@ -148,7 +148,7 @@ function validateAccumulatedResultReplayWatermark(record) {
|
|
|
148
148
|
if (typeof watermark !== "number" || !Number.isSafeInteger(watermark) || watermark < 0)
|
|
149
149
|
throw Error('Invalid checkpoint: invalid "accumulatedResultReplayWatermark" (expected non-negative safe integer)');
|
|
150
150
|
}
|
|
151
|
-
function
|
|
151
|
+
function dropRetiredPendingSignals(record) {
|
|
152
152
|
delete record.pendingSignals;
|
|
153
153
|
}
|
|
154
154
|
function assertCurrentSchemaVersion(record) {
|
|
@@ -77,7 +77,8 @@ export function createLifecycleCallbacks(engine) {
|
|
|
77
77
|
enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
|
|
78
78
|
failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
|
|
79
79
|
failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
|
|
80
|
-
failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
|
|
80
|
+
failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
|
|
81
|
+
failWorkflowForVersionMismatch: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { disposeEngineCleanupInterval } from "./engine-runtime-helpers.js";
|
|
2
2
|
import { EngineDisposedError } from "./errors.js";
|
|
3
3
|
import { disposeQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
|
|
4
|
+
import { rejectAllSleepTimerAcknowledgements } from "./sleep-timer-acknowledgements.js";
|
|
4
5
|
export function disposeEngine(internals) {
|
|
5
6
|
internals.disposed = !0;
|
|
6
7
|
internals.alertManager?.[Symbol.dispose]();
|
|
@@ -30,6 +31,7 @@ export function disposeEngine(internals) {
|
|
|
30
31
|
internals.handleCache.clear();
|
|
31
32
|
for (const waiter of internals.resultResolvers.values())
|
|
32
33
|
waiter.reject(new EngineDisposedError);
|
|
34
|
+
rejectAllSleepTimerAcknowledgements(internals, new EngineDisposedError);
|
|
33
35
|
internals.resultResolvers.clear();
|
|
34
36
|
internals.updateWaiters.clear();
|
|
35
37
|
internals.updateWaitersByWorkflow.clear();
|
|
@@ -46,6 +48,8 @@ export function disposeEngine(internals) {
|
|
|
46
48
|
internals.pendingWebhooks.clear();
|
|
47
49
|
internals.sleepResolvers.clear();
|
|
48
50
|
internals.sleepResolversByWorkflow.clear();
|
|
51
|
+
internals.sleepTimerAcknowledgementWaiters.clear();
|
|
52
|
+
internals.durableInlineOperations.clear();
|
|
49
53
|
internals.sleepTimersFiredWithoutResolver.clear();
|
|
50
54
|
internals.checkpoints.clear();
|
|
51
55
|
internals.pendingExecutionStateOwnerId = void 0;
|
|
@@ -15,6 +15,8 @@ export type EngineCleanupIntervalDisposalTracker = {
|
|
|
15
15
|
testToken: symbol | undefined;
|
|
16
16
|
};
|
|
17
17
|
export declare const engineCleanupIntervalFinalizer: FinalizationRegistry<EngineCleanupIntervalDisposalTracker>;
|
|
18
|
+
/** Test-only hook for the finalizer callback's synchronous warning gate. */
|
|
19
|
+
export declare function finalizeEngineCleanupIntervalTrackerForTesting(tracker: EngineCleanupIntervalDisposalTracker): void;
|
|
18
20
|
export declare function shouldEmitEngineLeakWarning(): boolean;
|
|
19
21
|
/** Test-only override for the engine leak-warning environment gate. */
|
|
20
22
|
export declare function setEngineLeakWarningOverrideForTesting(value: boolean | undefined): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
let engineLeakWarningOverrideForTesting, engineLeakCollectionCountForTesting = 0, nextEngineLeakWarningTokenForTesting;
|
|
2
2
|
const engineLeakWarningTokensForTesting = new Set;
|
|
3
|
-
|
|
3
|
+
function finalizeEngineCleanupIntervalTracker(tracker) {
|
|
4
4
|
engineLeakCollectionCountForTesting++;
|
|
5
5
|
if (tracker.cleanupInterval !== null) {
|
|
6
6
|
clearInterval(tracker.cleanupInterval);
|
|
@@ -15,7 +15,13 @@ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker)
|
|
|
15
15
|
engineLeakWarningTokensForTesting.add(tracker.testToken);
|
|
16
16
|
process.emitWarning("WeftEngineLeakWarning: A Weft Engine was garbage-collected without calling [Symbol.dispose](). Use `using`, `await using`, or call engine[Symbol.dispose]() to clear background timers and release runtime resources.");
|
|
17
17
|
}
|
|
18
|
+
}
|
|
19
|
+
export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
|
|
20
|
+
finalizeEngineCleanupIntervalTracker(tracker);
|
|
18
21
|
});
|
|
22
|
+
export function finalizeEngineCleanupIntervalTrackerForTesting(tracker) {
|
|
23
|
+
finalizeEngineCleanupIntervalTracker(tracker);
|
|
24
|
+
}
|
|
19
25
|
export function shouldEmitEngineLeakWarning() {
|
|
20
26
|
if (engineLeakWarningOverrideForTesting !== void 0)
|
|
21
27
|
return engineLeakWarningOverrideForTesting;
|
|
@@ -220,9 +220,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
220
220
|
* (signal-with-start). With an absent target, the workflow record and the
|
|
221
221
|
* first signal commit in one batch and the freshly-launched run consumes the
|
|
222
222
|
* signal on its first drive. A non-terminal target (running, pending, or
|
|
223
|
-
* suspended) is signalled through
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* suspended) is signalled through a workflow-serialized signal path. If
|
|
224
|
+
* terminal completion wins that handoff, the target is treated as terminal:
|
|
225
|
+
* the default path throws {@link StartOrSignalConflictError}, while
|
|
226
|
+
* `options.onTerminalConflict: 'start-new'` transfers the signal to the
|
|
227
|
+
* replacement run. A terminal target throws {@link StartOrSignalConflictError}
|
|
228
|
+
* unless `options.onTerminalConflict: 'start-new'` is supplied with an explicit
|
|
226
229
|
* workflow id and deterministic `signal.signalId`.
|
|
227
230
|
*
|
|
228
231
|
* Concurrent callers converge on one workflow and one delivered signal. Pass
|
|
@@ -368,6 +371,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
368
371
|
* deploys or explicit operator storage repair.
|
|
369
372
|
* When set, unknown workflow types are skipped and reported through
|
|
370
373
|
* {@link WorkflowRecoverySkippedEvent}.
|
|
374
|
+
*
|
|
375
|
+
* A recovered workflow whose persisted version metadata no longer matches
|
|
376
|
+
* its registered `WorkflowDefinition.version` is isolated per
|
|
377
|
+
* {@link RecoverAllOptions.versionMismatchPolicy} (default `'fail-run'`):
|
|
378
|
+
* only that workflow fails, and `recoverAll()` continues recovering its
|
|
379
|
+
* siblings in the same call.
|
|
371
380
|
*/
|
|
372
381
|
recoverAll(options?: RecoverAllOptions): Promise<WorkflowHandle[]>;
|
|
373
382
|
/**
|
|
@@ -487,3 +496,40 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
487
496
|
*/
|
|
488
497
|
fireTimer(entry: TimerEntry): Promise<void>;
|
|
489
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* `Engine` with its two chained-builder registration methods removed —
|
|
501
|
+
* `register` and `registerWorkflows`, whose return type is itself
|
|
502
|
+
* `Engine<NarrowedRegistry>` (see `register()`'s JSDoc: registering returns
|
|
503
|
+
* "this same engine with the definition added to its phantom type
|
|
504
|
+
* registry"). That self-reference is what makes `Engine` invariant in its
|
|
505
|
+
* registry generics: a concretely narrowed `Engine<Concrete>` (e.g. from
|
|
506
|
+
* `Engine.create({ workflows })`) is not structurally assignable to the
|
|
507
|
+
* plain default `Engine<DefaultWorkflowRegistry>`, even though every other
|
|
508
|
+
* member — including `start` and `startOrSignal`, whose *parameter* types
|
|
509
|
+
* (not return types) reference `TWorkflows` but never produce another
|
|
510
|
+
* `Engine<T>` — is not part of that recursive comparison.
|
|
511
|
+
*
|
|
512
|
+
* `start` and `startOrSignal` are deliberately KEPT (not omitted): hosted
|
|
513
|
+
* transports genuinely call them at runtime (REST/JSON-RPC workflow starts,
|
|
514
|
+
* MCP tool invocations) via `runtimeWorkflowEngine()`'s registry-erased
|
|
515
|
+
* dynamic-name overload, so a value satisfying this type must still provide
|
|
516
|
+
* them — a duck-typed engine substitute lacking `start` correctly fails to
|
|
517
|
+
* satisfy `RegistryAgnosticEngine` (see this type's `.test-d.ts` coverage).
|
|
518
|
+
*
|
|
519
|
+
* Host-facing options that accept an already-constructed `Engine` without
|
|
520
|
+
* needing the two chained-builder registration methods — `serve({ engine })`,
|
|
521
|
+
* the Service Worker helpers, the MCP session/HTTP/stdio surfaces — use this
|
|
522
|
+
* type instead of the bare default `Engine`, so both `new Engine({ storage })`
|
|
523
|
+
* and `Engine.create({ workflows })` are accepted without a call-site cast.
|
|
524
|
+
* See #708.
|
|
525
|
+
*
|
|
526
|
+
* (`Engine<object, object>` looks like the obvious fix — the widest legal
|
|
527
|
+
* instantiation of the registry generics — but TypeScript's structural check
|
|
528
|
+
* on `register()`'s self-referential return type does not reliably resolve
|
|
529
|
+
* that relationship: it can pass or fail for the identical `Engine<A>` /
|
|
530
|
+
* `Engine<B>` pair depending on unrelated compilation context, such as other
|
|
531
|
+
* files in the same program. Removing the registry-typed members entirely,
|
|
532
|
+
* rather than widening their generic arguments, avoids the recursive
|
|
533
|
+
* comparison altogether.)
|
|
534
|
+
*/
|
|
535
|
+
export type RegistryAgnosticEngine = Omit<Engine, 'register' | 'registerWorkflows'>;
|
|
@@ -74,7 +74,11 @@ import {
|
|
|
74
74
|
shouldStartEngineScheduler,
|
|
75
75
|
validateEngineCreateBackgroundTaskOptions
|
|
76
76
|
} from "./engine-runtime-helpers.js";
|
|
77
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
EngineCreateNameMismatchError,
|
|
79
|
+
EngineDisposedError,
|
|
80
|
+
StartOrSignalConflictError
|
|
81
|
+
} from "./errors.js";
|
|
78
82
|
import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
|
|
79
83
|
import { recordFinalizerState } from "./finalizer-state.js";
|
|
80
84
|
import {
|
|
@@ -145,7 +149,11 @@ import {
|
|
|
145
149
|
createSecondInstanceDetector
|
|
146
150
|
} from "./second-instance-detector.js";
|
|
147
151
|
import { signal as signalWorkflow } from "./signals.js";
|
|
148
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
loadScheduleState,
|
|
154
|
+
loadWorkflowState,
|
|
155
|
+
runSerializedWorkflowStateWrite
|
|
156
|
+
} from "./storage-io.js";
|
|
149
157
|
import {
|
|
150
158
|
feedOperationResult,
|
|
151
159
|
getComposedWorkflowInterceptor,
|
|
@@ -163,6 +171,7 @@ import {
|
|
|
163
171
|
submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
|
|
164
172
|
update as updateFromInternals
|
|
165
173
|
} from "./updates.js";
|
|
174
|
+
import { isTerminalWorkflowStatus } from "./validation.js";
|
|
166
175
|
import { coerceScheduleId } from "./validation/schedule.js";
|
|
167
176
|
import {
|
|
168
177
|
replayWorkflowFeed,
|
|
@@ -280,6 +289,8 @@ export class Engine extends EventTarget {
|
|
|
280
289
|
getInternals(this).updateWaitersByWorkflow = new Map;
|
|
281
290
|
getInternals(this).sleepResolvers = new Map;
|
|
282
291
|
getInternals(this).sleepResolversByWorkflow = new Map;
|
|
292
|
+
getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
|
|
293
|
+
getInternals(this).durableInlineOperations = new Map;
|
|
283
294
|
getInternals(this).sleepTimersFiredWithoutResolver = new Map;
|
|
284
295
|
getInternals(this).interceptors = resolveEngineInterceptors(options);
|
|
285
296
|
getInternals(this).composedWorkflowInterceptor = void 0;
|
|
@@ -568,7 +579,12 @@ export class Engine extends EventTarget {
|
|
|
568
579
|
#createStartOrSignalCallbacks() {
|
|
569
580
|
return {
|
|
570
581
|
...this.#createLifecycleCallbacks(),
|
|
571
|
-
signalExistingWorkflow: (workflowId, signalName, payload, signalId) => this
|
|
582
|
+
signalExistingWorkflow: (workflowId, signalName, payload, signalId) => runSerializedWorkflowStateWrite(getInternals(this), workflowId, async () => {
|
|
583
|
+
const state = await loadWorkflowState(getInternals(this), workflowId);
|
|
584
|
+
if (state && isTerminalWorkflowStatus(state.status))
|
|
585
|
+
throw new StartOrSignalConflictError(workflowId, state.status);
|
|
586
|
+
await this.signal(workflowId, signalName, payload, { signalId });
|
|
587
|
+
})
|
|
572
588
|
};
|
|
573
589
|
}
|
|
574
590
|
getHandle(workflowId) {
|
|
@@ -2,6 +2,10 @@ import { hasExposedAccessors, hasUpdateHandlers } from "../context/context-prese
|
|
|
2
2
|
import {
|
|
3
3
|
resumeWorkflowFromStorage as resumeWorkflowFromStorageFromLifecycle
|
|
4
4
|
} from "./lifecycle.js";
|
|
5
|
+
import {
|
|
6
|
+
recordDurableInlineOperation,
|
|
7
|
+
rejectSleepTimerAcknowledgements
|
|
8
|
+
} from "./sleep-timer-acknowledgements.js";
|
|
5
9
|
import {
|
|
6
10
|
completeWorkflow as completeWorkflowFromTermination,
|
|
7
11
|
failWorkflow as failWorkflowFromTermination
|
|
@@ -53,7 +57,7 @@ export async function getParkedWorkflowResumeDisposition(internals, workflowId,
|
|
|
53
57
|
return "corrupt";
|
|
54
58
|
return "resumable";
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
async function dispatchStrategyMessage(internals, message, callbacks) {
|
|
57
61
|
switch (message.type) {
|
|
58
62
|
case "completed":
|
|
59
63
|
await completeWorkflowFromTermination(internals, message.workflowId, message.result, callbacks.createTerminationCallbacks());
|
|
@@ -68,6 +72,7 @@ export async function handleStrategyMessage(internals, message, callbacks) {
|
|
|
68
72
|
case "checkpoint": {
|
|
69
73
|
const operation = callbacks.translateOperationRequest(message.operationRequest);
|
|
70
74
|
await callbacks.persistCheckpoint(message.workflowId, operation, message.checkpoint);
|
|
75
|
+
recordDurableInlineOperation(internals, message.workflowId, operation);
|
|
71
76
|
callbacks.validateDevelopmentCheckpoint(message.workflowId);
|
|
72
77
|
if (await callbacks.evaluateConstraints(message.workflowId))
|
|
73
78
|
break;
|
|
@@ -85,3 +90,11 @@ export async function handleStrategyMessage(internals, message, callbacks) {
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
}
|
|
93
|
+
export async function handleStrategyMessage(internals, message, callbacks) {
|
|
94
|
+
try {
|
|
95
|
+
await dispatchStrategyMessage(internals, message, callbacks);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
rejectSleepTimerAcknowledgements(internals, message.workflowId, error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -22,6 +22,7 @@ import type { AlertManager } from '../../alerting/alert-manager.ts';
|
|
|
22
22
|
import type { Storage as WeftStorage } from '../../storage/interface.ts';
|
|
23
23
|
import type { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
|
|
24
24
|
import type { ActivityRegistry } from '../activity-registry.ts';
|
|
25
|
+
import type { ContextOperationRequest } from '../context.ts';
|
|
25
26
|
import type { EventHeadRecord } from '../event-log.ts';
|
|
26
27
|
import type { ExecutionStrategy } from '../execution-strategy.ts';
|
|
27
28
|
import type { InlineExecutionStrategy } from '../inline-execution-strategy.ts';
|
|
@@ -39,6 +40,17 @@ import type { WorkflowFeedListener } from './index.ts';
|
|
|
39
40
|
import type { LeaseManager } from './lease-manager.ts';
|
|
40
41
|
import type { ScheduleHandleEngine } from './schedule-handle.ts';
|
|
41
42
|
import type { SecondInstanceDetector } from './second-instance-detector.ts';
|
|
43
|
+
export type SleepTimerAcknowledgementWaiter = {
|
|
44
|
+
fireAt: number;
|
|
45
|
+
operationId: string;
|
|
46
|
+
reject: (error: Error) => void;
|
|
47
|
+
resolve: () => void;
|
|
48
|
+
};
|
|
49
|
+
export type DurableInlineOperation = {
|
|
50
|
+
operationId: string;
|
|
51
|
+
scheduledFireAt?: number;
|
|
52
|
+
type: ContextOperationRequest['type'];
|
|
53
|
+
};
|
|
42
54
|
type EngineRuntime = WorkflowHandleEngine & ScheduleHandleEngine & {
|
|
43
55
|
start(type: string, input: unknown, options?: StartWorkflowOptions): Promise<WorkflowHandle>;
|
|
44
56
|
};
|
|
@@ -102,6 +114,15 @@ export interface EngineInternals {
|
|
|
102
114
|
fireAt: number;
|
|
103
115
|
}>;
|
|
104
116
|
sleepResolversByWorkflow: Map<string, Set<string>>;
|
|
117
|
+
/**
|
|
118
|
+
* Fired sleep timers awaiting proof that the awakened inline workflow reached
|
|
119
|
+
* its next durable checkpoint or terminal state. External schedulers must not
|
|
120
|
+
* delete a timer before these waiters settle, or a Service Worker eviction can
|
|
121
|
+
* lose the only durable wake-up between resolver settlement and checkpointing.
|
|
122
|
+
*/
|
|
123
|
+
sleepTimerAcknowledgementWaiters: Map<string, Set<SleepTimerAcknowledgementWaiter>>;
|
|
124
|
+
/** Most recently persisted inline operation, used to reject stale sleep-timer callbacks. */
|
|
125
|
+
durableInlineOperations: Map<string, DurableInlineOperation>;
|
|
105
126
|
/**
|
|
106
127
|
* Per-workflow maps of `operationId` → fired timer `fireAt` for sleep timers
|
|
107
128
|
* the scheduler tick fired before the resolver was registered. Keyed by
|
|
@@ -57,6 +57,22 @@ export type RecoverAllOptions = {
|
|
|
57
57
|
* recovery continues with its siblings.
|
|
58
58
|
*/
|
|
59
59
|
onRecoveredWorkflow?: (info: RecoveredWorkflowInfo) => void | Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Policy for a recovered workflow whose stored {@link WorkflowVersionTuple}
|
|
62
|
+
* (or legacy checkpoint `version`) no longer matches the registered
|
|
63
|
+
* {@link WorkflowDefinition.version}. The mismatch is detected before
|
|
64
|
+
* `resolveWorkflowServices` and `onRecoveredWorkflow` run for that workflow,
|
|
65
|
+
* so a mismatched run never re-provides services or invokes the hook.
|
|
66
|
+
*
|
|
67
|
+
* - `'fail-run'` (default): fail only the mismatched run to a terminal
|
|
68
|
+
* `failed` state with a `system` failure category carrying the
|
|
69
|
+
* {@link VersionMismatchError} message, then continue recovering its
|
|
70
|
+
* siblings. The run never advances user workflow code.
|
|
71
|
+
* - `'throw'`: preserve the pre-#702 behavior — rethrow the
|
|
72
|
+
* {@link VersionMismatchError} out of `recoverAll()` immediately, aborting
|
|
73
|
+
* recovery for every workflow not yet processed in this batch.
|
|
74
|
+
*/
|
|
75
|
+
versionMismatchPolicy?: 'fail-run' | 'throw';
|
|
60
76
|
};
|
|
61
77
|
export type LifecycleCallbacks = {
|
|
62
78
|
dispatchEvent: (event: Event) => void;
|
|
@@ -97,6 +113,15 @@ export type LifecycleCallbacks = {
|
|
|
97
113
|
* other workflows from the same storage backend.
|
|
98
114
|
*/
|
|
99
115
|
failWorkflowForCheckpointDecodeError: (workflowId: string, error: Error) => Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Force a recovered workflow to a terminal `failed` state because its
|
|
118
|
+
* persisted version metadata no longer matches the registered
|
|
119
|
+
* `WorkflowDefinition.version` (a {@link VersionMismatchError}). Fails just
|
|
120
|
+
* this run with a `system` failure category so `recoverAll()` can continue
|
|
121
|
+
* recovering other workflows under the default `'fail-run'`
|
|
122
|
+
* {@link RecoverAllOptions.versionMismatchPolicy}.
|
|
123
|
+
*/
|
|
124
|
+
failWorkflowForVersionMismatch: (workflowId: string, error: Error) => Promise<void>;
|
|
100
125
|
};
|
|
101
126
|
/**
|
|
102
127
|
* Pre-replay history circuit breaker. Called at every restore-from-checkpoint
|