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