@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.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}: ${JSON.stringify(issues)}`);
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}: ${JSON.stringify(issues)}`);
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}": ${JSON.stringify(issues)}`);
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}: ${JSON.stringify(issues)}`);
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 client = TypedClient.create(myContract, {
98
- * connection,
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
- return Future.make((resolve) => {
139
- (async () => {
140
- const definition = this.contract.workflows[workflowName];
141
- if (!definition) {
142
- resolve(Result.Error(createWorkflowNotFoundError(workflowName, this.contract)));
143
- return;
144
- }
145
- const inputResult = await definition.input["~standard"].validate(args);
146
- if (inputResult.issues) {
147
- resolve(Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues)));
148
- return;
149
- }
150
- const validatedInput = inputResult.value;
151
- try {
152
- const handle = await this.client.workflow.start(workflowName, {
153
- ...temporalOptions,
154
- taskQueue: this.contract.taskQueue,
155
- args: [validatedInput]
156
- });
157
- const typedHandle = this.createTypedHandle(handle, definition);
158
- resolve(Result.Ok(typedHandle));
159
- } catch (error) {
160
- resolve(Result.Error(createRuntimeClientError("startWorkflow", error)));
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
- return Future.make((resolve) => {
185
- (async () => {
186
- const definition = this.contract.workflows[workflowName];
187
- if (!definition) {
188
- resolve(Result.Error(createWorkflowNotFoundError(workflowName, this.contract)));
189
- return;
190
- }
191
- const inputResult = await definition.input["~standard"].validate(args);
192
- if (inputResult.issues) {
193
- resolve(Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues)));
194
- return;
195
- }
196
- const validatedInput = inputResult.value;
197
- try {
198
- const result = await this.client.workflow.execute(workflowName, {
199
- ...temporalOptions,
200
- taskQueue: this.contract.taskQueue,
201
- args: [validatedInput]
202
- });
203
- const outputResult = await definition.output["~standard"].validate(result);
204
- if (outputResult.issues) {
205
- resolve(Result.Error(createWorkflowValidationError(workflowName, "output", outputResult.issues)));
206
- return;
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
- return Future.make((resolve) => {
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
- const typedHandle = this.createTypedHandle(handle, definition);
240
- resolve(Result.Ok(typedHandle));
581
+ return Result.Ok(this.createTypedHandle(handle, definition));
241
582
  } catch (error) {
242
- resolve(Result.Error(createRuntimeClientError("getHandle", error)));
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
- for (const [queryName, queryDef] of Object.entries(definition.queries ?? {})) queries[queryName] = (args) => {
249
- return Future.make((resolve) => {
250
- (async () => {
251
- const inputResult = await queryDef.input["~standard"].validate(args);
252
- if (inputResult.issues) {
253
- resolve(Result.Error(new QueryValidationError(queryName, "input", inputResult.issues)));
254
- return;
255
- }
256
- try {
257
- const result = await workflowHandle.query(queryName, inputResult.value);
258
- const outputResult = await queryDef.output["~standard"].validate(result);
259
- if (outputResult.issues) {
260
- resolve(Result.Error(new QueryValidationError(queryName, "output", outputResult.issues)));
261
- return;
262
- }
263
- resolve(Result.Ok(outputResult.value));
264
- } catch (error) {
265
- resolve(Result.Error(createRuntimeClientError("query", error)));
266
- }
267
- })();
268
- });
269
- };
270
- const signals = {};
271
- for (const [signalName, signalDef] of Object.entries(definition.signals ?? {})) signals[signalName] = (args) => {
272
- return Future.make((resolve) => {
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
- return Future.make((resolve) => {
318
- (async () => {
319
- try {
320
- const result = await workflowHandle.result();
321
- const outputResult = await definition.output["~standard"].validate(result);
322
- if (outputResult.issues) {
323
- resolve(Result.Error(new WorkflowValidationError(workflowHandle.workflowId, "output", outputResult.issues)));
324
- return;
325
- }
326
- resolve(Result.Ok(outputResult.value));
327
- } catch (error) {
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
- fetchHistory: () => {
343
- return Future.fromPromise(workflowHandle.fetchHistory()).mapError((error) => createRuntimeClientError("fetchHistory", error));
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