@temporal-contract/client 0.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +516 -159
- package/dist/index.d.cts +286 -47
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +286 -47
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +511 -158
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -27
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
2
|
-
let
|
|
3
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _temporalio_common = require("@temporalio/common");
|
|
3
|
+
let neverthrow = require("neverthrow");
|
|
4
|
+
let _temporalio_client = require("@temporalio/client");
|
|
4
5
|
//#region src/errors.ts
|
|
5
6
|
/**
|
|
6
7
|
* Base class for all typed client errors with boxed pattern
|
|
@@ -33,11 +34,117 @@ var WorkflowNotFoundError = class extends TypedClientError {
|
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
36
|
/**
|
|
37
|
+
* Discriminated variant of {@link RuntimeClientError} surfaced when starting
|
|
38
|
+
* a workflow collides with an existing execution — Temporal's
|
|
39
|
+
* `WorkflowExecutionAlreadyStartedError`. The most common cause is a
|
|
40
|
+
* workflowId reuse policy that rejects duplicates while a previous run is
|
|
41
|
+
* still in retention.
|
|
42
|
+
*
|
|
43
|
+
* Distinguishing this from `RuntimeClientError` lets idempotent callers
|
|
44
|
+
* branch on it explicitly (e.g. fetch the existing handle and continue)
|
|
45
|
+
* without inspecting `error.cause` against a Temporal SDK class.
|
|
46
|
+
*/
|
|
47
|
+
var WorkflowAlreadyStartedError = class extends TypedClientError {
|
|
48
|
+
constructor(workflowType, workflowId, cause) {
|
|
49
|
+
super(`Workflow "${workflowType}" with ID "${workflowId}" is already started or in retention.`);
|
|
50
|
+
this.workflowType = workflowType;
|
|
51
|
+
this.workflowId = workflowId;
|
|
52
|
+
this.cause = cause;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Discriminated variant of {@link RuntimeClientError} surfaced when an
|
|
57
|
+
* operation targets a workflow execution that doesn't exist in the
|
|
58
|
+
* namespace — Temporal's `WorkflowNotFoundError` (distinct from this
|
|
59
|
+
* package's contract-level {@link WorkflowNotFoundError}).
|
|
60
|
+
*
|
|
61
|
+
* Returned from:
|
|
62
|
+
* - handle methods: `signal`, `query`, `executeUpdate`, `result`,
|
|
63
|
+
* `terminate`, `cancel`, `describe`, `fetchHistory`
|
|
64
|
+
* - `executeWorkflow` (when the underlying execute call hits a missing
|
|
65
|
+
* execution mid-flight)
|
|
66
|
+
*/
|
|
67
|
+
var WorkflowExecutionNotFoundError = class extends TypedClientError {
|
|
68
|
+
constructor(workflowId, runId, cause) {
|
|
69
|
+
super(`Workflow execution "${workflowId}"${runId ? ` (run "${runId}")` : ""} not found in namespace.`);
|
|
70
|
+
this.workflowId = workflowId;
|
|
71
|
+
this.runId = runId;
|
|
72
|
+
this.cause = cause;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Discriminated variant of {@link RuntimeClientError} surfaced when waiting
|
|
77
|
+
* on a workflow's result and the workflow completes with a failure —
|
|
78
|
+
* Temporal's `WorkflowFailedError`.
|
|
79
|
+
*
|
|
80
|
+
* `cause` is the *unwrapped* underlying failure (typically an
|
|
81
|
+
* `ApplicationFailure`, `CancelledFailure`, `TerminatedFailure`, or
|
|
82
|
+
* `TimeoutFailure`) lifted from Temporal's wrapper, so callers can branch
|
|
83
|
+
* on the failure category in one step (`err.cause instanceof
|
|
84
|
+
* ApplicationFailure`) instead of unwrapping twice via the SDK wrapper.
|
|
85
|
+
*
|
|
86
|
+
* Returned from `executeWorkflow` and `handle.result()`.
|
|
87
|
+
*/
|
|
88
|
+
var WorkflowFailedError = class extends TypedClientError {
|
|
89
|
+
constructor(workflowId, cause) {
|
|
90
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause ?? "unknown failure");
|
|
91
|
+
super(`Workflow "${workflowId}" completed with failure: ${causeMessage}`);
|
|
92
|
+
this.workflowId = workflowId;
|
|
93
|
+
this.cause = cause;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Pattern for string keys safe to render with dot notation. A "safe" key is a
|
|
98
|
+
* JavaScript identifier (letters/digits/underscore/$, not starting with a
|
|
99
|
+
* digit). Anything else — keys containing dots, spaces, leading digits, the
|
|
100
|
+
* empty string, the literal string `"0"` etc. — gets bracket-quoted so the
|
|
101
|
+
* path is unambiguous.
|
|
102
|
+
*
|
|
103
|
+
* This helper is intentionally duplicated with the worker package so each
|
|
104
|
+
* entry point is self-contained; keep the two copies in sync.
|
|
105
|
+
*/
|
|
106
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
107
|
+
/**
|
|
108
|
+
* Render a Standard Schema {@link StandardSchemaV1.Issue} into a human-readable
|
|
109
|
+
* string that includes the failing field's path.
|
|
110
|
+
*
|
|
111
|
+
* Example output:
|
|
112
|
+
* - `at items[0].quantity: Expected number, received undefined`
|
|
113
|
+
* - `at customerId: Expected string, received undefined`
|
|
114
|
+
* - `at user["first name"]: Expected string, received undefined`
|
|
115
|
+
* - `Validation error` *(no path)*
|
|
116
|
+
*
|
|
117
|
+
* Path segments come either as bare `PropertyKey` values or as
|
|
118
|
+
* `{ key: PropertyKey }` objects (per the spec); both are normalized.
|
|
119
|
+
* - Numeric keys → `[N]`
|
|
120
|
+
* - String keys that are valid JS identifiers → bare (first) or `.key`
|
|
121
|
+
* - String keys that aren't valid identifiers → `["..."]` with JSON-style
|
|
122
|
+
* escaping (handles dots, spaces, leading digits, the empty string, the
|
|
123
|
+
* literal string `"0"`, embedded quotes, etc.)
|
|
124
|
+
* - Symbol / other `PropertyKey` → `[Symbol(name)]`
|
|
125
|
+
*/
|
|
126
|
+
function formatIssue(issue) {
|
|
127
|
+
if (issue.path === void 0 || issue.path.length === 0) return issue.message;
|
|
128
|
+
let path = "";
|
|
129
|
+
for (let i = 0; i < issue.path.length; i++) {
|
|
130
|
+
const segment = issue.path[i];
|
|
131
|
+
const key = segment !== null && typeof segment === "object" && "key" in segment ? segment.key : segment;
|
|
132
|
+
if (typeof key === "number") path += `[${key}]`;
|
|
133
|
+
else if (typeof key === "string" && SAFE_IDENTIFIER.test(key)) path += i === 0 ? key : `.${key}`;
|
|
134
|
+
else if (typeof key === "string") path += `[${JSON.stringify(key)}]`;
|
|
135
|
+
else path += `[${String(key)}]`;
|
|
136
|
+
}
|
|
137
|
+
return `at ${path}: ${issue.message}`;
|
|
138
|
+
}
|
|
139
|
+
function summarizeIssues(issues) {
|
|
140
|
+
return issues.map(formatIssue).join("; ");
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
36
143
|
* Thrown when workflow input or output validation fails
|
|
37
144
|
*/
|
|
38
145
|
var WorkflowValidationError = class extends TypedClientError {
|
|
39
146
|
constructor(workflowName, direction, issues) {
|
|
40
|
-
super(`Validation failed for workflow "${workflowName}" ${direction}: ${
|
|
147
|
+
super(`Validation failed for workflow "${workflowName}" ${direction}: ${summarizeIssues(issues)}`);
|
|
41
148
|
this.workflowName = workflowName;
|
|
42
149
|
this.direction = direction;
|
|
43
150
|
this.issues = issues;
|
|
@@ -48,7 +155,7 @@ var WorkflowValidationError = class extends TypedClientError {
|
|
|
48
155
|
*/
|
|
49
156
|
var QueryValidationError = class extends TypedClientError {
|
|
50
157
|
constructor(queryName, direction, issues) {
|
|
51
|
-
super(`Validation failed for query "${queryName}" ${direction}: ${
|
|
158
|
+
super(`Validation failed for query "${queryName}" ${direction}: ${summarizeIssues(issues)}`);
|
|
52
159
|
this.queryName = queryName;
|
|
53
160
|
this.direction = direction;
|
|
54
161
|
this.issues = issues;
|
|
@@ -59,7 +166,7 @@ var QueryValidationError = class extends TypedClientError {
|
|
|
59
166
|
*/
|
|
60
167
|
var SignalValidationError = class extends TypedClientError {
|
|
61
168
|
constructor(signalName, issues) {
|
|
62
|
-
super(`Validation failed for signal "${signalName}": ${
|
|
169
|
+
super(`Validation failed for signal "${signalName}": ${summarizeIssues(issues)}`);
|
|
63
170
|
this.signalName = signalName;
|
|
64
171
|
this.issues = issues;
|
|
65
172
|
}
|
|
@@ -69,28 +176,221 @@ var SignalValidationError = class extends TypedClientError {
|
|
|
69
176
|
*/
|
|
70
177
|
var UpdateValidationError = class extends TypedClientError {
|
|
71
178
|
constructor(updateName, direction, issues) {
|
|
72
|
-
super(`Validation failed for update "${updateName}" ${direction}: ${
|
|
179
|
+
super(`Validation failed for update "${updateName}" ${direction}: ${summarizeIssues(issues)}`);
|
|
73
180
|
this.updateName = updateName;
|
|
74
181
|
this.direction = direction;
|
|
75
182
|
this.issues = issues;
|
|
76
183
|
}
|
|
77
184
|
};
|
|
78
|
-
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/internal.ts
|
|
187
|
+
/**
|
|
188
|
+
* Internal helpers shared across the client package's modules.
|
|
189
|
+
*
|
|
190
|
+
* Not part of the public API — this module is not listed in the package's
|
|
191
|
+
* `exports` map, so consumers can't import from `@temporal-contract/client/internal`.
|
|
192
|
+
* In-package modules and tests import it directly via relative path.
|
|
193
|
+
*/
|
|
194
|
+
/**
|
|
195
|
+
* Wrap an async result-producing function in a `ResultAsync`, catching any
|
|
196
|
+
* unhandled rejection as a `RuntimeClientError("unexpected", error)`.
|
|
197
|
+
*
|
|
198
|
+
* The work function is expected to handle its own domain errors and return
|
|
199
|
+
* an `err(...)` for them; the catch here is a safety net for thrown
|
|
200
|
+
* exceptions the work didn't anticipate.
|
|
201
|
+
*
|
|
202
|
+
* Used by `client.ts` (workflow operations) and `schedule.ts` (schedule
|
|
203
|
+
* operations) so the unexpected-rejection shape is identical across the
|
|
204
|
+
* typed client surface.
|
|
205
|
+
*/
|
|
206
|
+
function makeResultAsync(work) {
|
|
207
|
+
return new neverthrow.ResultAsync(work().catch((e) => (0, neverthrow.err)(new RuntimeClientError("unexpected", e))));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Map a thrown error from `client.workflow.start` / `signalWithStart` into
|
|
211
|
+
* the discriminated union surfaced by the typed client. Specifically
|
|
212
|
+
* recognizes Temporal's `WorkflowExecutionAlreadyStartedError`; everything
|
|
213
|
+
* else falls through to {@link RuntimeClientError}.
|
|
214
|
+
*/
|
|
215
|
+
function classifyStartError(operation, error) {
|
|
216
|
+
if (error instanceof _temporalio_client.WorkflowExecutionAlreadyStartedError) return new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error);
|
|
217
|
+
return new RuntimeClientError(operation, error);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Map a thrown error from a workflow handle method (signal, query,
|
|
221
|
+
* executeUpdate, terminate, cancel, describe, fetchHistory) into the
|
|
222
|
+
* discriminated union surfaced by the typed client. Recognizes Temporal's
|
|
223
|
+
* `WorkflowNotFoundError`; everything else falls through to
|
|
224
|
+
* {@link RuntimeClientError}.
|
|
225
|
+
*
|
|
226
|
+
* `fallbackWorkflowId` is used when Temporal's error carries an empty
|
|
227
|
+
* `workflowId` (it normalizes missing IDs to the empty string), so the
|
|
228
|
+
* surfaced error always identifies the targeted execution.
|
|
229
|
+
*/
|
|
230
|
+
function classifyHandleError(operation, error, fallbackWorkflowId) {
|
|
231
|
+
if (error instanceof _temporalio_common.WorkflowNotFoundError) return new WorkflowExecutionNotFoundError(error.workflowId || fallbackWorkflowId, error.runId, error);
|
|
232
|
+
return new RuntimeClientError(operation, error);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Map a thrown error from `handle.result()` / `client.workflow.execute()`
|
|
236
|
+
* (the latter when waiting on the result phase). Recognizes Temporal's
|
|
237
|
+
* `WorkflowFailedError` and `WorkflowNotFoundError`; everything else falls
|
|
238
|
+
* through to {@link RuntimeClientError}.
|
|
239
|
+
*
|
|
240
|
+
* Temporal's `WorkflowFailedError` is itself a wrapper — the actionable
|
|
241
|
+
* failure (ApplicationFailure, CancelledFailure, TerminatedFailure, etc.)
|
|
242
|
+
* lives on its `cause` field. We forward that inner cause directly so
|
|
243
|
+
* consumers can match `err.cause` against the underlying failure class
|
|
244
|
+
* without an extra unwrap step. (If Temporal's cause is `undefined`, our
|
|
245
|
+
* `cause` is too — same shape as before.)
|
|
246
|
+
*/
|
|
247
|
+
function classifyResultError(operation, error, workflowId) {
|
|
248
|
+
if (error instanceof _temporalio_client.WorkflowFailedError) return new WorkflowFailedError(workflowId, error.cause);
|
|
249
|
+
if (error instanceof _temporalio_common.WorkflowNotFoundError) return new WorkflowExecutionNotFoundError(error.workflowId || workflowId, error.runId, error);
|
|
250
|
+
return new RuntimeClientError(operation, error);
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/schedule.ts
|
|
254
|
+
/**
|
|
255
|
+
* Typed wrapper around Temporal's `ScheduleClient`. Exposed as
|
|
256
|
+
* `typedClient.schedule` — keeps the typed-client surface organized the
|
|
257
|
+
* same way Temporal's own `Client.schedule` does.
|
|
258
|
+
*/
|
|
259
|
+
var TypedScheduleClient = class {
|
|
260
|
+
constructor(contract, scheduleClient) {
|
|
261
|
+
this.contract = contract;
|
|
262
|
+
this.scheduleClient = scheduleClient;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Create a new schedule that, on each fire, starts the named contract
|
|
266
|
+
* workflow with validated args.
|
|
267
|
+
*
|
|
268
|
+
* Validates `args` against the workflow's input schema before dispatching
|
|
269
|
+
* the create request to Temporal. The workflow's `taskQueue` and
|
|
270
|
+
* `workflowType` are pulled from the contract automatically; the typed
|
|
271
|
+
* options shape omits them so call sites don't have to repeat themselves.
|
|
272
|
+
*/
|
|
273
|
+
create(workflowName, options) {
|
|
274
|
+
const work = async () => {
|
|
275
|
+
const definition = this.contract.workflows[workflowName];
|
|
276
|
+
if (!definition) return (0, neverthrow.err)(new WorkflowNotFoundError(String(workflowName), Object.keys(this.contract.workflows)));
|
|
277
|
+
const inputResult = await definition.input["~standard"].validate(options.args);
|
|
278
|
+
if (inputResult.issues) return (0, neverthrow.err)(new WorkflowValidationError(String(workflowName), "input", inputResult.issues));
|
|
279
|
+
try {
|
|
280
|
+
const overrides = options.action ?? {};
|
|
281
|
+
const action = {
|
|
282
|
+
type: "startWorkflow",
|
|
283
|
+
workflowType: workflowName,
|
|
284
|
+
taskQueue: this.contract.taskQueue,
|
|
285
|
+
args: [inputResult.value],
|
|
286
|
+
...overrides.workflowId !== void 0 ? { workflowId: overrides.workflowId } : {},
|
|
287
|
+
...overrides.workflowExecutionTimeout !== void 0 ? { workflowExecutionTimeout: overrides.workflowExecutionTimeout } : {},
|
|
288
|
+
...overrides.workflowRunTimeout !== void 0 ? { workflowRunTimeout: overrides.workflowRunTimeout } : {},
|
|
289
|
+
...overrides.workflowTaskTimeout !== void 0 ? { workflowTaskTimeout: overrides.workflowTaskTimeout } : {},
|
|
290
|
+
...overrides.retry !== void 0 ? { retry: overrides.retry } : {},
|
|
291
|
+
...overrides.memo !== void 0 ? { memo: overrides.memo } : {},
|
|
292
|
+
...overrides.staticDetails !== void 0 ? { staticDetails: overrides.staticDetails } : {},
|
|
293
|
+
...overrides.staticSummary !== void 0 ? { staticSummary: overrides.staticSummary } : {}
|
|
294
|
+
};
|
|
295
|
+
return (0, neverthrow.ok)(wrapScheduleHandle(await this.scheduleClient.create({
|
|
296
|
+
scheduleId: options.scheduleId,
|
|
297
|
+
spec: options.spec,
|
|
298
|
+
action,
|
|
299
|
+
...options.policies !== void 0 ? { policies: options.policies } : {},
|
|
300
|
+
...options.state !== void 0 ? { state: options.state } : {},
|
|
301
|
+
...options.memo !== void 0 ? { memo: options.memo } : {}
|
|
302
|
+
})));
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return (0, neverthrow.err)(new RuntimeClientError("schedule.create", error));
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return makeResultAsync(work);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get a typed handle to an existing schedule. Does not validate that the
|
|
311
|
+
* schedule exists — handle methods (`describe`, `pause`, etc.) will
|
|
312
|
+
* surface a `RuntimeClientError` if the underlying ID is unknown.
|
|
313
|
+
*/
|
|
314
|
+
getHandle(scheduleId) {
|
|
315
|
+
return wrapScheduleHandle(this.scheduleClient.getHandle(scheduleId));
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function wrapScheduleHandle(handle) {
|
|
319
|
+
return {
|
|
320
|
+
scheduleId: handle.scheduleId,
|
|
321
|
+
pause: (note) => neverthrow.ResultAsync.fromPromise(handle.pause(note), (error) => new RuntimeClientError("schedule.pause", error)).map(() => void 0),
|
|
322
|
+
unpause: (note) => neverthrow.ResultAsync.fromPromise(handle.unpause(note), (error) => new RuntimeClientError("schedule.unpause", error)).map(() => void 0),
|
|
323
|
+
trigger: (overlap) => neverthrow.ResultAsync.fromPromise(handle.trigger(overlap), (error) => new RuntimeClientError("schedule.trigger", error)).map(() => void 0),
|
|
324
|
+
delete: () => neverthrow.ResultAsync.fromPromise(handle.delete(), (error) => new RuntimeClientError("schedule.delete", error)).map(() => void 0),
|
|
325
|
+
describe: () => neverthrow.ResultAsync.fromPromise(handle.describe(), (error) => new RuntimeClientError("schedule.describe", error))
|
|
326
|
+
};
|
|
327
|
+
}
|
|
79
328
|
//#endregion
|
|
80
329
|
//#region src/client.ts
|
|
81
330
|
/**
|
|
82
|
-
*
|
|
331
|
+
* Translate the contract's typed `searchAttributes` map (declared
|
|
332
|
+
* name → value) into a Temporal `TypedSearchAttributes` instance, so the
|
|
333
|
+
* Temporal client honours indexing when starting the workflow.
|
|
334
|
+
*
|
|
335
|
+
* Workflows without a `searchAttributes` block (or callers passing no
|
|
336
|
+
* values) skip the conversion entirely and return `undefined`, matching
|
|
337
|
+
* the Temporal SDK's "absent ≠ empty" semantics.
|
|
338
|
+
*/
|
|
339
|
+
function toTypedSearchAttributes(workflowDef, values) {
|
|
340
|
+
if (!values || !workflowDef.searchAttributes) return void 0;
|
|
341
|
+
const pairs = [];
|
|
342
|
+
for (const [name, value] of Object.entries(values)) {
|
|
343
|
+
if (value === void 0) continue;
|
|
344
|
+
const def = workflowDef.searchAttributes[name];
|
|
345
|
+
if (!def) continue;
|
|
346
|
+
const key = (0, _temporalio_common.defineSearchAttributeKey)(name, def.kind);
|
|
347
|
+
pairs.push({
|
|
348
|
+
key,
|
|
349
|
+
value
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return pairs.length > 0 ? new _temporalio_common.TypedSearchAttributes(pairs) : void 0;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Typed Temporal client with neverthrow Result/ResultAsync pattern based on a contract
|
|
83
356
|
*
|
|
84
357
|
* Provides type-safe methods to start and execute workflows
|
|
85
358
|
* defined in the contract, with explicit error handling using Result pattern.
|
|
86
359
|
*/
|
|
87
360
|
var TypedClient = class TypedClient {
|
|
361
|
+
/**
|
|
362
|
+
* Typed wrapper around Temporal's `client.schedule.create(...)` and
|
|
363
|
+
* related lifecycle methods. Fires the underlying `startWorkflow` action
|
|
364
|
+
* with args validated against the contract's input schema.
|
|
365
|
+
*
|
|
366
|
+
* **Requires `@temporalio/client` 1.16+.** The Schedule API was added in
|
|
367
|
+
* 1.16; on older versions this property is unset and any access throws.
|
|
368
|
+
* The package's peer dep is pinned to `^1.16.0` so the standard install
|
|
369
|
+
* paths surface a peer-dependency warning rather than a runtime crash.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```ts
|
|
373
|
+
* const result = await client.schedule.create("processOrder", {
|
|
374
|
+
* scheduleId: "daily-sweep",
|
|
375
|
+
* spec: { cronExpressions: ["0 2 * * *"] },
|
|
376
|
+
* args: { orderId: "sweep" },
|
|
377
|
+
* });
|
|
378
|
+
*
|
|
379
|
+
* result.match(
|
|
380
|
+
* async (handle) => { await handle.pause("maintenance"); },
|
|
381
|
+
* (error) => console.error("schedule create failed", error),
|
|
382
|
+
* );
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
schedule;
|
|
88
386
|
constructor(contract, client) {
|
|
89
387
|
this.contract = contract;
|
|
90
388
|
this.client = client;
|
|
389
|
+
if (!client.schedule) throw new Error("TypedClient requires @temporalio/client >= 1.16 (the Schedule API was added in 1.16). Found a Client instance without a `schedule` property — please upgrade.");
|
|
390
|
+
this.schedule = new TypedScheduleClient(contract, client.schedule);
|
|
91
391
|
}
|
|
92
392
|
/**
|
|
93
|
-
* Create a typed Temporal client with
|
|
393
|
+
* Create a typed Temporal client with neverthrow pattern from a contract
|
|
94
394
|
*
|
|
95
395
|
* @example
|
|
96
396
|
* ```ts
|
|
@@ -103,17 +403,17 @@ var TypedClient = class TypedClient {
|
|
|
103
403
|
* args: { ... },
|
|
104
404
|
* });
|
|
105
405
|
*
|
|
106
|
-
* result.match(
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
406
|
+
* result.match(
|
|
407
|
+
* (output) => console.log('Success:', output),
|
|
408
|
+
* (error) => console.error('Failed:', error),
|
|
409
|
+
* );
|
|
110
410
|
* ```
|
|
111
411
|
*/
|
|
112
412
|
static create(contract, client) {
|
|
113
413
|
return new TypedClient(contract, client);
|
|
114
414
|
}
|
|
115
415
|
/**
|
|
116
|
-
* Start a workflow and return a typed handle with
|
|
416
|
+
* Start a workflow and return a typed handle with ResultAsync pattern
|
|
117
417
|
*
|
|
118
418
|
* @example
|
|
119
419
|
* ```ts
|
|
@@ -124,39 +424,94 @@ var TypedClient = class TypedClient {
|
|
|
124
424
|
* retry: { maximumAttempts: 3 },
|
|
125
425
|
* });
|
|
126
426
|
*
|
|
127
|
-
* handleResult.match(
|
|
128
|
-
*
|
|
427
|
+
* handleResult.match(
|
|
428
|
+
* async (handle) => {
|
|
129
429
|
* const result = await handle.result();
|
|
130
430
|
* // ... handle result
|
|
131
431
|
* },
|
|
132
|
-
*
|
|
432
|
+
* (error) => console.error('Failed to start:', error),
|
|
433
|
+
* );
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
startWorkflow(workflowName, { args, searchAttributes, ...temporalOptions }) {
|
|
437
|
+
const work = async () => {
|
|
438
|
+
const definition = this.contract.workflows[workflowName];
|
|
439
|
+
if (!definition) return (0, neverthrow.err)(createWorkflowNotFoundError(workflowName, this.contract));
|
|
440
|
+
const inputResult = await definition.input["~standard"].validate(args);
|
|
441
|
+
if (inputResult.issues) return (0, neverthrow.err)(createWorkflowValidationError(workflowName, "input", inputResult.issues));
|
|
442
|
+
const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
|
|
443
|
+
try {
|
|
444
|
+
const handle = await this.client.workflow.start(workflowName, {
|
|
445
|
+
...temporalOptions,
|
|
446
|
+
taskQueue: this.contract.taskQueue,
|
|
447
|
+
args: [inputResult.value],
|
|
448
|
+
...typedSearchAttributes ? { typedSearchAttributes } : {}
|
|
449
|
+
});
|
|
450
|
+
return (0, neverthrow.ok)(this.createTypedHandle(handle, definition));
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return (0, neverthrow.err)(classifyStartError("startWorkflow", error));
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
return makeResultAsync(work);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Send a signal to a workflow, starting it first if it doesn't already exist.
|
|
459
|
+
*
|
|
460
|
+
* Validates both halves of the call against the contract:
|
|
461
|
+
* - `args` against the workflow's input schema
|
|
462
|
+
* - `signalArgs` against the named signal's input schema
|
|
463
|
+
*
|
|
464
|
+
* Returns a `TypedWorkflowHandleWithSignaledRunId` — the same shape as
|
|
465
|
+
* `startWorkflow`'s handle, plus a `signaledRunId` field for correlating
|
|
466
|
+
* the signal with the (possibly pre-existing) workflow execution chain.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```ts
|
|
470
|
+
* const result = await client.signalWithStart('processOrder', {
|
|
471
|
+
* workflowId: 'order-123',
|
|
472
|
+
* args: { orderId: 'ORD-123', customerId: 'CUST-1' },
|
|
473
|
+
* signalName: 'cancel',
|
|
474
|
+
* signalArgs: { reason: 'duplicate' },
|
|
133
475
|
* });
|
|
476
|
+
*
|
|
477
|
+
* result.match(
|
|
478
|
+
* (handle) => console.log('signaled run', handle.signaledRunId),
|
|
479
|
+
* (error) => console.error('signalWithStart failed', error),
|
|
480
|
+
* );
|
|
134
481
|
* ```
|
|
135
482
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
483
|
+
signalWithStart(workflowName, { args, signalName, signalArgs, searchAttributes, ...temporalOptions }) {
|
|
484
|
+
const work = async () => {
|
|
485
|
+
const definition = this.contract.workflows[workflowName];
|
|
486
|
+
if (!definition) return (0, neverthrow.err)(createWorkflowNotFoundError(workflowName, this.contract));
|
|
487
|
+
const inputResult = await definition.input["~standard"].validate(args);
|
|
488
|
+
if (inputResult.issues) return (0, neverthrow.err)(createWorkflowValidationError(workflowName, "input", inputResult.issues));
|
|
489
|
+
const signalDef = definition.signals?.[signalName];
|
|
490
|
+
if (!signalDef) return (0, neverthrow.err)(new SignalValidationError(signalName, [{ message: `Signal "${signalName}" is not declared on workflow "${String(workflowName)}".` }]));
|
|
491
|
+
const signalInputResult = await signalDef.input["~standard"].validate(signalArgs);
|
|
492
|
+
if (signalInputResult.issues) return (0, neverthrow.err)(new SignalValidationError(signalName, signalInputResult.issues));
|
|
493
|
+
const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
|
|
494
|
+
try {
|
|
495
|
+
const handle = await this.client.workflow.signalWithStart(workflowName, {
|
|
496
|
+
...temporalOptions,
|
|
497
|
+
taskQueue: this.contract.taskQueue,
|
|
498
|
+
args: [inputResult.value],
|
|
499
|
+
signal: signalName,
|
|
500
|
+
signalArgs: [signalInputResult.value],
|
|
501
|
+
...typedSearchAttributes ? { typedSearchAttributes } : {}
|
|
502
|
+
});
|
|
503
|
+
return (0, neverthrow.ok)({
|
|
504
|
+
...this.createTypedHandle(handle, definition),
|
|
505
|
+
signaledRunId: handle.signaledRunId
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return (0, neverthrow.err)(classifyStartError("signalWithStart", error));
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
return makeResultAsync(work);
|
|
157
512
|
}
|
|
158
513
|
/**
|
|
159
|
-
* Execute a workflow (start and wait for result) with
|
|
514
|
+
* Execute a workflow (start and wait for result) with ResultAsync pattern
|
|
160
515
|
*
|
|
161
516
|
* @example
|
|
162
517
|
* ```ts
|
|
@@ -167,146 +522,115 @@ var TypedClient = class TypedClient {
|
|
|
167
522
|
* retry: { maximumAttempts: 3 },
|
|
168
523
|
* });
|
|
169
524
|
*
|
|
170
|
-
* result.match(
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
525
|
+
* result.match(
|
|
526
|
+
* (output) => console.log('Order processed:', output.status),
|
|
527
|
+
* (error) => console.error('Processing failed:', error),
|
|
528
|
+
* );
|
|
174
529
|
* ```
|
|
175
530
|
*/
|
|
176
|
-
executeWorkflow(workflowName, { args, ...temporalOptions }) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
531
|
+
executeWorkflow(workflowName, { args, searchAttributes, ...temporalOptions }) {
|
|
532
|
+
const work = async () => {
|
|
533
|
+
const definition = this.contract.workflows[workflowName];
|
|
534
|
+
if (!definition) return (0, neverthrow.err)(createWorkflowNotFoundError(workflowName, this.contract));
|
|
535
|
+
const inputResult = await definition.input["~standard"].validate(args);
|
|
536
|
+
if (inputResult.issues) return (0, neverthrow.err)(createWorkflowValidationError(workflowName, "input", inputResult.issues));
|
|
537
|
+
const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
|
|
538
|
+
try {
|
|
539
|
+
const result = await this.client.workflow.execute(workflowName, {
|
|
540
|
+
...temporalOptions,
|
|
541
|
+
taskQueue: this.contract.taskQueue,
|
|
542
|
+
args: [inputResult.value],
|
|
543
|
+
...typedSearchAttributes ? { typedSearchAttributes } : {}
|
|
544
|
+
});
|
|
545
|
+
const outputResult = await definition.output["~standard"].validate(result);
|
|
546
|
+
if (outputResult.issues) return (0, neverthrow.err)(createWorkflowValidationError(workflowName, "output", outputResult.issues));
|
|
547
|
+
return (0, neverthrow.ok)(outputResult.value);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (error instanceof _temporalio_client.WorkflowExecutionAlreadyStartedError) return (0, neverthrow.err)(new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error));
|
|
550
|
+
if (error instanceof _temporalio_client.WorkflowFailedError) return (0, neverthrow.err)(new WorkflowFailedError(temporalOptions.workflowId, error.cause));
|
|
551
|
+
if (error instanceof _temporalio_common.WorkflowNotFoundError) return (0, neverthrow.err)(new WorkflowExecutionNotFoundError(error.workflowId || temporalOptions.workflowId, error.runId, error));
|
|
552
|
+
return (0, neverthrow.err)(createRuntimeClientError("executeWorkflow", error));
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
return makeResultAsync(work);
|
|
198
556
|
}
|
|
199
557
|
/**
|
|
200
|
-
* Get a handle to an existing workflow with
|
|
558
|
+
* Get a handle to an existing workflow with ResultAsync pattern
|
|
201
559
|
*
|
|
202
560
|
* @example
|
|
203
561
|
* ```ts
|
|
204
562
|
* const handleResult = await client.getHandle('processOrder', 'order-123');
|
|
205
|
-
* handleResult.match(
|
|
206
|
-
*
|
|
563
|
+
* handleResult.match(
|
|
564
|
+
* async (handle) => {
|
|
207
565
|
* const result = await handle.result();
|
|
208
566
|
* // ... handle result
|
|
209
567
|
* },
|
|
210
|
-
*
|
|
211
|
-
*
|
|
568
|
+
* (error) => console.error('Failed to get handle:', error),
|
|
569
|
+
* );
|
|
212
570
|
* ```
|
|
213
571
|
*/
|
|
214
572
|
getHandle(workflowName, workflowId) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
})().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
|
|
227
|
-
});
|
|
573
|
+
const work = async () => {
|
|
574
|
+
const definition = this.contract.workflows[workflowName];
|
|
575
|
+
if (!definition) return (0, neverthrow.err)(createWorkflowNotFoundError(workflowName, this.contract));
|
|
576
|
+
try {
|
|
577
|
+
const handle = this.client.workflow.getHandle(workflowId);
|
|
578
|
+
return (0, neverthrow.ok)(this.createTypedHandle(handle, definition));
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return (0, neverthrow.err)(createRuntimeClientError("getHandle", error));
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
return makeResultAsync(work);
|
|
228
584
|
}
|
|
229
585
|
createTypedHandle(workflowHandle, definition) {
|
|
230
|
-
const queries = {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} catch (error) {
|
|
257
|
-
return _swan_io_boxed.Result.Error(createRuntimeClientError("signal", error));
|
|
258
|
-
}
|
|
259
|
-
})().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
|
|
260
|
-
});
|
|
261
|
-
};
|
|
262
|
-
const updates = {};
|
|
263
|
-
for (const [updateName, updateDef] of Object.entries(definition.updates ?? {})) updates[updateName] = (args) => {
|
|
264
|
-
return _swan_io_boxed.Future.make((resolve) => {
|
|
265
|
-
(async () => {
|
|
266
|
-
const inputResult = await updateDef.input["~standard"].validate(args);
|
|
267
|
-
if (inputResult.issues) return _swan_io_boxed.Result.Error(new UpdateValidationError(updateName, "input", inputResult.issues));
|
|
268
|
-
try {
|
|
269
|
-
const result = await workflowHandle.executeUpdate(updateName, { args: [inputResult.value] });
|
|
270
|
-
const outputResult = await updateDef.output["~standard"].validate(result);
|
|
271
|
-
if (outputResult.issues) return _swan_io_boxed.Result.Error(new UpdateValidationError(updateName, "output", outputResult.issues));
|
|
272
|
-
return _swan_io_boxed.Result.Ok(outputResult.value);
|
|
273
|
-
} catch (error) {
|
|
274
|
-
return _swan_io_boxed.Result.Error(createRuntimeClientError("update", error));
|
|
275
|
-
}
|
|
276
|
-
})().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
|
|
277
|
-
});
|
|
278
|
-
};
|
|
586
|
+
const queries = buildValidatedProxy({
|
|
587
|
+
defs: definition.queries,
|
|
588
|
+
operation: "query",
|
|
589
|
+
workflowId: workflowHandle.workflowId,
|
|
590
|
+
makeValidationError: (name, direction, issues) => new QueryValidationError(name, direction, issues),
|
|
591
|
+
invoke: (name, validated) => workflowHandle.query(name, validated),
|
|
592
|
+
validateOutput: (def) => def.output
|
|
593
|
+
});
|
|
594
|
+
const signals = buildValidatedProxy({
|
|
595
|
+
defs: definition.signals,
|
|
596
|
+
operation: "signal",
|
|
597
|
+
workflowId: workflowHandle.workflowId,
|
|
598
|
+
makeValidationError: (name, _direction, issues) => new SignalValidationError(name, issues),
|
|
599
|
+
invoke: async (name, validated) => {
|
|
600
|
+
await workflowHandle.signal(name, validated);
|
|
601
|
+
},
|
|
602
|
+
validateOutput: () => null
|
|
603
|
+
});
|
|
604
|
+
const updates = buildValidatedProxy({
|
|
605
|
+
defs: definition.updates,
|
|
606
|
+
operation: "update",
|
|
607
|
+
workflowId: workflowHandle.workflowId,
|
|
608
|
+
makeValidationError: (name, direction, issues) => new UpdateValidationError(name, direction, issues),
|
|
609
|
+
invoke: (name, validated) => workflowHandle.executeUpdate(name, { args: [validated] }),
|
|
610
|
+
validateOutput: (def) => def.output
|
|
611
|
+
});
|
|
279
612
|
return {
|
|
280
613
|
workflowId: workflowHandle.workflowId,
|
|
281
614
|
queries,
|
|
282
615
|
signals,
|
|
283
616
|
updates,
|
|
284
617
|
result: () => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
},
|
|
298
|
-
terminate: (reason) => {
|
|
299
|
-
return _swan_io_boxed.Future.fromPromise(workflowHandle.terminate(reason)).mapError((error) => createRuntimeClientError("terminate", error)).mapOk(() => void 0);
|
|
300
|
-
},
|
|
301
|
-
cancel: () => {
|
|
302
|
-
return _swan_io_boxed.Future.fromPromise(workflowHandle.cancel()).mapError((error) => createRuntimeClientError("cancel", error)).mapOk(() => void 0);
|
|
303
|
-
},
|
|
304
|
-
describe: () => {
|
|
305
|
-
return _swan_io_boxed.Future.fromPromise(workflowHandle.describe()).mapError((error) => createRuntimeClientError("describe", error));
|
|
618
|
+
const work = async () => {
|
|
619
|
+
try {
|
|
620
|
+
const result = await workflowHandle.result();
|
|
621
|
+
const outputResult = await definition.output["~standard"].validate(result);
|
|
622
|
+
if (outputResult.issues) return (0, neverthrow.err)(new WorkflowValidationError(workflowHandle.workflowId, "output", outputResult.issues));
|
|
623
|
+
return (0, neverthrow.ok)(outputResult.value);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
return (0, neverthrow.err)(classifyResultError("result", error, workflowHandle.workflowId));
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
return makeResultAsync(work);
|
|
306
629
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
630
|
+
terminate: (reason) => neverthrow.ResultAsync.fromPromise(workflowHandle.terminate(reason), (error) => classifyHandleError("terminate", error, workflowHandle.workflowId)).map(() => void 0),
|
|
631
|
+
cancel: () => neverthrow.ResultAsync.fromPromise(workflowHandle.cancel(), (error) => classifyHandleError("cancel", error, workflowHandle.workflowId)).map(() => void 0),
|
|
632
|
+
describe: () => neverthrow.ResultAsync.fromPromise(workflowHandle.describe(), (error) => classifyHandleError("describe", error, workflowHandle.workflowId)),
|
|
633
|
+
fetchHistory: () => neverthrow.ResultAsync.fromPromise(workflowHandle.fetchHistory(), (error) => classifyHandleError("fetchHistory", error, workflowHandle.workflowId))
|
|
310
634
|
};
|
|
311
635
|
}
|
|
312
636
|
};
|
|
@@ -319,11 +643,44 @@ function createWorkflowNotFoundError(workflowName, contract) {
|
|
|
319
643
|
function createWorkflowValidationError(workflowName, direction, issues) {
|
|
320
644
|
return new WorkflowValidationError(String(workflowName), direction, issues);
|
|
321
645
|
}
|
|
322
|
-
|
|
646
|
+
/**
|
|
647
|
+
* Build a `{ name: (args) => ResultAsync<...> }` proxy for a contract's
|
|
648
|
+
* queries/signals/updates. The three call sites differ only in how they
|
|
649
|
+
* invoke Temporal and whether they validate output, so the shared
|
|
650
|
+
* input-validate → invoke → output-validate → wrap-Result pipeline lives
|
|
651
|
+
* here once.
|
|
652
|
+
*/
|
|
653
|
+
function buildValidatedProxy({ defs, operation, workflowId, makeValidationError, invoke, validateOutput }) {
|
|
654
|
+
const proxy = {};
|
|
655
|
+
if (!defs) return proxy;
|
|
656
|
+
for (const [name, def] of Object.entries(defs)) proxy[name] = (args) => {
|
|
657
|
+
const work = async () => {
|
|
658
|
+
const inputResult = await def.input["~standard"].validate(args);
|
|
659
|
+
if (inputResult.issues) return (0, neverthrow.err)(makeValidationError(name, "input", inputResult.issues));
|
|
660
|
+
try {
|
|
661
|
+
const result = await invoke(name, inputResult.value);
|
|
662
|
+
const outputSchema = validateOutput(def);
|
|
663
|
+
if (!outputSchema) return (0, neverthrow.ok)(result);
|
|
664
|
+
const outputResult = await outputSchema["~standard"].validate(result);
|
|
665
|
+
if (outputResult.issues) return (0, neverthrow.err)(makeValidationError(name, "output", outputResult.issues));
|
|
666
|
+
return (0, neverthrow.ok)(outputResult.value);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
return (0, neverthrow.err)(classifyHandleError(operation, error, workflowId));
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
return makeResultAsync(work);
|
|
672
|
+
};
|
|
673
|
+
return proxy;
|
|
674
|
+
}
|
|
323
675
|
//#endregion
|
|
324
676
|
exports.QueryValidationError = QueryValidationError;
|
|
677
|
+
exports.RuntimeClientError = RuntimeClientError;
|
|
325
678
|
exports.SignalValidationError = SignalValidationError;
|
|
326
679
|
exports.TypedClient = TypedClient;
|
|
680
|
+
exports.TypedScheduleClient = TypedScheduleClient;
|
|
327
681
|
exports.UpdateValidationError = UpdateValidationError;
|
|
682
|
+
exports.WorkflowAlreadyStartedError = WorkflowAlreadyStartedError;
|
|
683
|
+
exports.WorkflowExecutionNotFoundError = WorkflowExecutionNotFoundError;
|
|
684
|
+
exports.WorkflowFailedError = WorkflowFailedError;
|
|
328
685
|
exports.WorkflowNotFoundError = WorkflowNotFoundError;
|
|
329
|
-
exports.WorkflowValidationError = WorkflowValidationError;
|
|
686
|
+
exports.WorkflowValidationError = WorkflowValidationError;
|