@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 CHANGED
@@ -1,5 +1,7 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _temporalio_common = require("@temporalio/common");
1
3
  let _swan_io_boxed = require("@swan-io/boxed");
2
-
4
+ let _temporalio_client = require("@temporalio/client");
3
5
  //#region src/errors.ts
4
6
  /**
5
7
  * Base class for all typed client errors with boxed pattern
@@ -32,11 +34,117 @@ var WorkflowNotFoundError = class extends TypedClientError {
32
34
  }
33
35
  };
34
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
+ /**
35
143
  * Thrown when workflow input or output validation fails
36
144
  */
37
145
  var WorkflowValidationError = class extends TypedClientError {
38
146
  constructor(workflowName, direction, issues) {
39
- super(`Validation failed for workflow "${workflowName}" ${direction}: ${JSON.stringify(issues)}`);
147
+ super(`Validation failed for workflow "${workflowName}" ${direction}: ${summarizeIssues(issues)}`);
40
148
  this.workflowName = workflowName;
41
149
  this.direction = direction;
42
150
  this.issues = issues;
@@ -47,7 +155,7 @@ var WorkflowValidationError = class extends TypedClientError {
47
155
  */
48
156
  var QueryValidationError = class extends TypedClientError {
49
157
  constructor(queryName, direction, issues) {
50
- super(`Validation failed for query "${queryName}" ${direction}: ${JSON.stringify(issues)}`);
158
+ super(`Validation failed for query "${queryName}" ${direction}: ${summarizeIssues(issues)}`);
51
159
  this.queryName = queryName;
52
160
  this.direction = direction;
53
161
  this.issues = issues;
@@ -58,7 +166,7 @@ var QueryValidationError = class extends TypedClientError {
58
166
  */
59
167
  var SignalValidationError = class extends TypedClientError {
60
168
  constructor(signalName, issues) {
61
- super(`Validation failed for signal "${signalName}": ${JSON.stringify(issues)}`);
169
+ super(`Validation failed for signal "${signalName}": ${summarizeIssues(issues)}`);
62
170
  this.signalName = signalName;
63
171
  this.issues = issues;
64
172
  }
@@ -68,25 +176,221 @@ var SignalValidationError = class extends TypedClientError {
68
176
  */
69
177
  var UpdateValidationError = class extends TypedClientError {
70
178
  constructor(updateName, direction, issues) {
71
- super(`Validation failed for update "${updateName}" ${direction}: ${JSON.stringify(issues)}`);
179
+ super(`Validation failed for update "${updateName}" ${direction}: ${summarizeIssues(issues)}`);
72
180
  this.updateName = updateName;
73
181
  this.direction = direction;
74
182
  this.issues = issues;
75
183
  }
76
184
  };
77
-
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 `Future`, 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
+ * a `Result.Error(...)` for them; the catch here is a safety net for
200
+ * thrown 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 makeFuture(work) {
207
+ return _swan_io_boxed.Future.make((resolve) => {
208
+ work().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(new RuntimeClientError("unexpected", e))));
209
+ });
210
+ }
211
+ /**
212
+ * Map a thrown error from `client.workflow.start` / `signalWithStart` into
213
+ * the discriminated union surfaced by the typed client. Specifically
214
+ * recognizes Temporal's `WorkflowExecutionAlreadyStartedError`; everything
215
+ * else falls through to {@link RuntimeClientError}.
216
+ */
217
+ function classifyStartError(operation, error) {
218
+ if (error instanceof _temporalio_client.WorkflowExecutionAlreadyStartedError) return new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error);
219
+ return new RuntimeClientError(operation, error);
220
+ }
221
+ /**
222
+ * Map a thrown error from a workflow handle method (signal, query,
223
+ * executeUpdate, terminate, cancel, describe, fetchHistory) into the
224
+ * discriminated union surfaced by the typed client. Recognizes Temporal's
225
+ * `WorkflowNotFoundError`; everything else falls through to
226
+ * {@link RuntimeClientError}.
227
+ *
228
+ * `fallbackWorkflowId` is used when Temporal's error carries an empty
229
+ * `workflowId` (it normalizes missing IDs to the empty string), so the
230
+ * surfaced error always identifies the targeted execution.
231
+ */
232
+ function classifyHandleError(operation, error, fallbackWorkflowId) {
233
+ if (error instanceof _temporalio_common.WorkflowNotFoundError) return new WorkflowExecutionNotFoundError(error.workflowId || fallbackWorkflowId, error.runId, error);
234
+ return new RuntimeClientError(operation, error);
235
+ }
236
+ /**
237
+ * Map a thrown error from `handle.result()` / `client.workflow.execute()`
238
+ * (the latter when waiting on the result phase). Recognizes Temporal's
239
+ * `WorkflowFailedError` and `WorkflowNotFoundError`; everything else falls
240
+ * through to {@link RuntimeClientError}.
241
+ *
242
+ * Temporal's `WorkflowFailedError` is itself a wrapper — the actionable
243
+ * failure (ApplicationFailure, CancelledFailure, TerminatedFailure, etc.)
244
+ * lives on its `cause` field. We forward that inner cause directly so
245
+ * consumers can match `err.cause` against the underlying failure class
246
+ * without an extra unwrap step. (If Temporal's cause is `undefined`, our
247
+ * `cause` is too — same shape as before.)
248
+ */
249
+ function classifyResultError(operation, error, workflowId) {
250
+ if (error instanceof _temporalio_client.WorkflowFailedError) return new WorkflowFailedError(workflowId, error.cause);
251
+ if (error instanceof _temporalio_common.WorkflowNotFoundError) return new WorkflowExecutionNotFoundError(error.workflowId || workflowId, error.runId, error);
252
+ return new RuntimeClientError(operation, error);
253
+ }
254
+ //#endregion
255
+ //#region src/schedule.ts
256
+ /**
257
+ * Typed wrapper around Temporal's `ScheduleClient`. Exposed as
258
+ * `typedClient.schedule` — keeps the typed-client surface organized the
259
+ * same way Temporal's own `Client.schedule` does.
260
+ */
261
+ var TypedScheduleClient = class {
262
+ constructor(contract, scheduleClient) {
263
+ this.contract = contract;
264
+ this.scheduleClient = scheduleClient;
265
+ }
266
+ /**
267
+ * Create a new schedule that, on each fire, starts the named contract
268
+ * workflow with validated args.
269
+ *
270
+ * Validates `args` against the workflow's input schema before dispatching
271
+ * the create request to Temporal. The workflow's `taskQueue` and
272
+ * `workflowType` are pulled from the contract automatically; the typed
273
+ * options shape omits them so call sites don't have to repeat themselves.
274
+ */
275
+ create(workflowName, options) {
276
+ const work = async () => {
277
+ const definition = this.contract.workflows[workflowName];
278
+ if (!definition) return _swan_io_boxed.Result.Error(new WorkflowNotFoundError(String(workflowName), Object.keys(this.contract.workflows)));
279
+ const inputResult = await definition.input["~standard"].validate(options.args);
280
+ if (inputResult.issues) return _swan_io_boxed.Result.Error(new WorkflowValidationError(String(workflowName), "input", inputResult.issues));
281
+ try {
282
+ const overrides = options.action ?? {};
283
+ const action = {
284
+ type: "startWorkflow",
285
+ workflowType: workflowName,
286
+ taskQueue: this.contract.taskQueue,
287
+ args: [inputResult.value],
288
+ ...overrides.workflowId !== void 0 ? { workflowId: overrides.workflowId } : {},
289
+ ...overrides.workflowExecutionTimeout !== void 0 ? { workflowExecutionTimeout: overrides.workflowExecutionTimeout } : {},
290
+ ...overrides.workflowRunTimeout !== void 0 ? { workflowRunTimeout: overrides.workflowRunTimeout } : {},
291
+ ...overrides.workflowTaskTimeout !== void 0 ? { workflowTaskTimeout: overrides.workflowTaskTimeout } : {},
292
+ ...overrides.retry !== void 0 ? { retry: overrides.retry } : {},
293
+ ...overrides.memo !== void 0 ? { memo: overrides.memo } : {},
294
+ ...overrides.staticDetails !== void 0 ? { staticDetails: overrides.staticDetails } : {},
295
+ ...overrides.staticSummary !== void 0 ? { staticSummary: overrides.staticSummary } : {}
296
+ };
297
+ const handle = await this.scheduleClient.create({
298
+ scheduleId: options.scheduleId,
299
+ spec: options.spec,
300
+ action,
301
+ ...options.policies !== void 0 ? { policies: options.policies } : {},
302
+ ...options.state !== void 0 ? { state: options.state } : {},
303
+ ...options.memo !== void 0 ? { memo: options.memo } : {}
304
+ });
305
+ return _swan_io_boxed.Result.Ok(wrapScheduleHandle(handle));
306
+ } catch (error) {
307
+ return _swan_io_boxed.Result.Error(new RuntimeClientError("schedule.create", error));
308
+ }
309
+ };
310
+ return makeFuture(work);
311
+ }
312
+ /**
313
+ * Get a typed handle to an existing schedule. Does not validate that the
314
+ * schedule exists — handle methods (`describe`, `pause`, etc.) will
315
+ * surface a `RuntimeClientError` if the underlying ID is unknown.
316
+ */
317
+ getHandle(scheduleId) {
318
+ return wrapScheduleHandle(this.scheduleClient.getHandle(scheduleId));
319
+ }
320
+ };
321
+ function wrapScheduleHandle(handle) {
322
+ return {
323
+ scheduleId: handle.scheduleId,
324
+ pause: (note) => _swan_io_boxed.Future.fromPromise(handle.pause(note)).mapError((error) => new RuntimeClientError("schedule.pause", error)).mapOk(() => void 0),
325
+ unpause: (note) => _swan_io_boxed.Future.fromPromise(handle.unpause(note)).mapError((error) => new RuntimeClientError("schedule.unpause", error)).mapOk(() => void 0),
326
+ trigger: (overlap) => _swan_io_boxed.Future.fromPromise(handle.trigger(overlap)).mapError((error) => new RuntimeClientError("schedule.trigger", error)).mapOk(() => void 0),
327
+ delete: () => _swan_io_boxed.Future.fromPromise(handle.delete()).mapError((error) => new RuntimeClientError("schedule.delete", error)).mapOk(() => void 0),
328
+ describe: () => _swan_io_boxed.Future.fromPromise(handle.describe()).mapError((error) => new RuntimeClientError("schedule.describe", error))
329
+ };
330
+ }
78
331
  //#endregion
79
332
  //#region src/client.ts
80
333
  /**
334
+ * Translate the contract's typed `searchAttributes` map (declared
335
+ * name → value) into a Temporal `TypedSearchAttributes` instance, so the
336
+ * Temporal client honours indexing when starting the workflow.
337
+ *
338
+ * Workflows without a `searchAttributes` block (or callers passing no
339
+ * values) skip the conversion entirely and return `undefined`, matching
340
+ * the Temporal SDK's "absent ≠ empty" semantics.
341
+ */
342
+ function toTypedSearchAttributes(workflowDef, values) {
343
+ if (!values || !workflowDef.searchAttributes) return void 0;
344
+ const pairs = [];
345
+ for (const [name, value] of Object.entries(values)) {
346
+ if (value === void 0) continue;
347
+ const def = workflowDef.searchAttributes[name];
348
+ if (!def) continue;
349
+ const key = (0, _temporalio_common.defineSearchAttributeKey)(name, def.kind);
350
+ pairs.push({
351
+ key,
352
+ value
353
+ });
354
+ }
355
+ return pairs.length > 0 ? new _temporalio_common.TypedSearchAttributes(pairs) : void 0;
356
+ }
357
+ /**
81
358
  * Typed Temporal client with Result/Future pattern based on a contract
82
359
  *
83
360
  * Provides type-safe methods to start and execute workflows
84
361
  * defined in the contract, with explicit error handling using Result pattern.
85
362
  */
86
363
  var TypedClient = class TypedClient {
364
+ /**
365
+ * Typed wrapper around Temporal's `client.schedule.create(...)` and
366
+ * related lifecycle methods. Fires the underlying `startWorkflow` action
367
+ * with args validated against the contract's input schema.
368
+ *
369
+ * **Requires `@temporalio/client` 1.16+.** The Schedule API was added in
370
+ * 1.16; on older versions this property is unset and any access throws.
371
+ * The package's peer dep is pinned to `^1.16.0` so the standard install
372
+ * paths surface a peer-dependency warning rather than a runtime crash.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * const result = await client.schedule.create("processOrder", {
377
+ * scheduleId: "daily-sweep",
378
+ * spec: { cronExpressions: ["0 2 * * *"] },
379
+ * args: { orderId: "sweep" },
380
+ * });
381
+ *
382
+ * result.match({
383
+ * Ok: async (handle) => { await handle.pause("maintenance"); },
384
+ * Error: (error) => console.error("schedule create failed", error),
385
+ * });
386
+ * ```
387
+ */
388
+ schedule;
87
389
  constructor(contract, client) {
88
390
  this.contract = contract;
89
391
  this.client = client;
392
+ 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.");
393
+ this.schedule = new TypedScheduleClient(contract, client.schedule);
90
394
  }
91
395
  /**
92
396
  * Create a typed Temporal client with boxed pattern from a contract
@@ -94,10 +398,8 @@ var TypedClient = class TypedClient {
94
398
  * @example
95
399
  * ```ts
96
400
  * const connection = await Connection.connect();
97
- * const client = TypedClient.create(myContract, {
98
- * connection,
99
- * namespace: 'default',
100
- * });
401
+ * const temporalClient = new Client({ connection });
402
+ * const client = TypedClient.create(myContract, temporalClient);
101
403
  *
102
404
  * const result = await client.executeWorkflow('processOrder', {
103
405
  * workflowId: 'order-123',
@@ -134,33 +436,83 @@ var TypedClient = class TypedClient {
134
436
  * });
135
437
  * ```
136
438
  */
137
- startWorkflow(workflowName, { args, ...temporalOptions }) {
138
- return _swan_io_boxed.Future.make((resolve) => {
139
- (async () => {
140
- const definition = this.contract.workflows[workflowName];
141
- if (!definition) {
142
- resolve(_swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract)));
143
- return;
144
- }
145
- const inputResult = await definition.input["~standard"].validate(args);
146
- if (inputResult.issues) {
147
- resolve(_swan_io_boxed.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(_swan_io_boxed.Result.Ok(typedHandle));
159
- } catch (error) {
160
- resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("startWorkflow", error)));
161
- }
162
- })();
163
- });
439
+ startWorkflow(workflowName, { args, searchAttributes, ...temporalOptions }) {
440
+ const work = async () => {
441
+ const definition = this.contract.workflows[workflowName];
442
+ if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
443
+ const inputResult = await definition.input["~standard"].validate(args);
444
+ if (inputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues));
445
+ const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
446
+ try {
447
+ const handle = await this.client.workflow.start(workflowName, {
448
+ ...temporalOptions,
449
+ taskQueue: this.contract.taskQueue,
450
+ args: [inputResult.value],
451
+ ...typedSearchAttributes ? { typedSearchAttributes } : {}
452
+ });
453
+ return _swan_io_boxed.Result.Ok(this.createTypedHandle(handle, definition));
454
+ } catch (error) {
455
+ return _swan_io_boxed.Result.Error(classifyStartError("startWorkflow", error));
456
+ }
457
+ };
458
+ return makeFuture(work);
459
+ }
460
+ /**
461
+ * Send a signal to a workflow, starting it first if it doesn't already exist.
462
+ *
463
+ * Validates both halves of the call against the contract:
464
+ * - `args` against the workflow's input schema
465
+ * - `signalArgs` against the named signal's input schema
466
+ *
467
+ * Returns a `TypedWorkflowHandleWithSignaledRunId` — the same shape as
468
+ * `startWorkflow`'s handle, plus a `signaledRunId` field for correlating
469
+ * the signal with the (possibly pre-existing) workflow execution chain.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * const result = await client.signalWithStart('processOrder', {
474
+ * workflowId: 'order-123',
475
+ * args: { orderId: 'ORD-123', customerId: 'CUST-1' },
476
+ * signalName: 'cancel',
477
+ * signalArgs: { reason: 'duplicate' },
478
+ * });
479
+ *
480
+ * result.match({
481
+ * Ok: (handle) => console.log('signaled run', handle.signaledRunId),
482
+ * Error: (error) => console.error('signalWithStart failed', error),
483
+ * });
484
+ * ```
485
+ */
486
+ signalWithStart(workflowName, { args, signalName, signalArgs, searchAttributes, ...temporalOptions }) {
487
+ const work = async () => {
488
+ const definition = this.contract.workflows[workflowName];
489
+ if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
490
+ const inputResult = await definition.input["~standard"].validate(args);
491
+ if (inputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues));
492
+ const signalDef = definition.signals?.[signalName];
493
+ if (!signalDef) return _swan_io_boxed.Result.Error(new SignalValidationError(signalName, [{ message: `Signal "${signalName}" is not declared on workflow "${String(workflowName)}".` }]));
494
+ const signalInputResult = await signalDef.input["~standard"].validate(signalArgs);
495
+ if (signalInputResult.issues) return _swan_io_boxed.Result.Error(new SignalValidationError(signalName, signalInputResult.issues));
496
+ const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
497
+ try {
498
+ const handle = await this.client.workflow.signalWithStart(workflowName, {
499
+ ...temporalOptions,
500
+ taskQueue: this.contract.taskQueue,
501
+ args: [inputResult.value],
502
+ signal: signalName,
503
+ signalArgs: [signalInputResult.value],
504
+ ...typedSearchAttributes ? { typedSearchAttributes } : {}
505
+ });
506
+ const typed = this.createTypedHandle(handle, definition);
507
+ return _swan_io_boxed.Result.Ok({
508
+ ...typed,
509
+ signaledRunId: handle.signaledRunId
510
+ });
511
+ } catch (error) {
512
+ return _swan_io_boxed.Result.Error(classifyStartError("signalWithStart", error));
513
+ }
514
+ };
515
+ return makeFuture(work);
164
516
  }
165
517
  /**
166
518
  * Execute a workflow (start and wait for result) with Future/Result pattern
@@ -180,37 +532,31 @@ var TypedClient = class TypedClient {
180
532
  * });
181
533
  * ```
182
534
  */
183
- executeWorkflow(workflowName, { args, ...temporalOptions }) {
184
- return _swan_io_boxed.Future.make((resolve) => {
185
- (async () => {
186
- const definition = this.contract.workflows[workflowName];
187
- if (!definition) {
188
- resolve(_swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract)));
189
- return;
190
- }
191
- const inputResult = await definition.input["~standard"].validate(args);
192
- if (inputResult.issues) {
193
- resolve(_swan_io_boxed.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(_swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "output", outputResult.issues)));
206
- return;
207
- }
208
- resolve(_swan_io_boxed.Result.Ok(outputResult.value));
209
- } catch (error) {
210
- resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("executeWorkflow", error)));
211
- }
212
- })();
213
- });
535
+ executeWorkflow(workflowName, { args, searchAttributes, ...temporalOptions }) {
536
+ const work = async () => {
537
+ const definition = this.contract.workflows[workflowName];
538
+ if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
539
+ const inputResult = await definition.input["~standard"].validate(args);
540
+ if (inputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues));
541
+ const typedSearchAttributes = toTypedSearchAttributes(definition, searchAttributes);
542
+ try {
543
+ const result = await this.client.workflow.execute(workflowName, {
544
+ ...temporalOptions,
545
+ taskQueue: this.contract.taskQueue,
546
+ args: [inputResult.value],
547
+ ...typedSearchAttributes ? { typedSearchAttributes } : {}
548
+ });
549
+ const outputResult = await definition.output["~standard"].validate(result);
550
+ if (outputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "output", outputResult.issues));
551
+ return _swan_io_boxed.Result.Ok(outputResult.value);
552
+ } catch (error) {
553
+ if (error instanceof _temporalio_client.WorkflowExecutionAlreadyStartedError) return _swan_io_boxed.Result.Error(new WorkflowAlreadyStartedError(error.workflowType, error.workflowId, error));
554
+ if (error instanceof _temporalio_client.WorkflowFailedError) return _swan_io_boxed.Result.Error(new WorkflowFailedError(temporalOptions.workflowId, error.cause));
555
+ if (error instanceof _temporalio_common.WorkflowNotFoundError) return _swan_io_boxed.Result.Error(new WorkflowExecutionNotFoundError(error.workflowId || temporalOptions.workflowId, error.runId, error));
556
+ return _swan_io_boxed.Result.Error(createRuntimeClientError("executeWorkflow", error));
557
+ }
558
+ };
559
+ return makeFuture(work);
214
560
  }
215
561
  /**
216
562
  * Get a handle to an existing workflow with Future/Result pattern
@@ -228,120 +574,67 @@ var TypedClient = class TypedClient {
228
574
  * ```
229
575
  */
230
576
  getHandle(workflowName, workflowId) {
231
- return _swan_io_boxed.Future.make((resolve) => {
577
+ const work = async () => {
232
578
  const definition = this.contract.workflows[workflowName];
233
- if (!definition) {
234
- resolve(_swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract)));
235
- return;
236
- }
579
+ if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
237
580
  try {
238
581
  const handle = this.client.workflow.getHandle(workflowId);
239
- const typedHandle = this.createTypedHandle(handle, definition);
240
- resolve(_swan_io_boxed.Result.Ok(typedHandle));
582
+ return _swan_io_boxed.Result.Ok(this.createTypedHandle(handle, definition));
241
583
  } catch (error) {
242
- resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("getHandle", error)));
584
+ return _swan_io_boxed.Result.Error(createRuntimeClientError("getHandle", error));
243
585
  }
244
- });
586
+ };
587
+ return makeFuture(work);
245
588
  }
246
589
  createTypedHandle(workflowHandle, definition) {
247
- const queries = {};
248
- for (const [queryName, queryDef] of Object.entries(definition.queries ?? {})) queries[queryName] = (args) => {
249
- return _swan_io_boxed.Future.make((resolve) => {
250
- (async () => {
251
- const inputResult = await queryDef.input["~standard"].validate(args);
252
- if (inputResult.issues) {
253
- resolve(_swan_io_boxed.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(_swan_io_boxed.Result.Error(new QueryValidationError(queryName, "output", outputResult.issues)));
261
- return;
262
- }
263
- resolve(_swan_io_boxed.Result.Ok(outputResult.value));
264
- } catch (error) {
265
- resolve(_swan_io_boxed.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 _swan_io_boxed.Future.make((resolve) => {
273
- (async () => {
274
- const inputResult = await signalDef.input["~standard"].validate(args);
275
- if (inputResult.issues) {
276
- resolve(_swan_io_boxed.Result.Error(new SignalValidationError(signalName, inputResult.issues)));
277
- return;
278
- }
279
- try {
280
- await workflowHandle.signal(signalName, inputResult.value);
281
- resolve(_swan_io_boxed.Result.Ok(void 0));
282
- } catch (error) {
283
- resolve(_swan_io_boxed.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 _swan_io_boxed.Future.make((resolve) => {
291
- (async () => {
292
- const inputResult = await updateDef.input["~standard"].validate(args);
293
- if (inputResult.issues) {
294
- resolve(_swan_io_boxed.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(_swan_io_boxed.Result.Error(new UpdateValidationError(updateName, "output", outputResult.issues)));
302
- return;
303
- }
304
- resolve(_swan_io_boxed.Result.Ok(outputResult.value));
305
- } catch (error) {
306
- resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("update", error)));
307
- }
308
- })();
309
- });
310
- };
590
+ const queries = buildValidatedProxy({
591
+ defs: definition.queries,
592
+ operation: "query",
593
+ workflowId: workflowHandle.workflowId,
594
+ makeValidationError: (name, direction, issues) => new QueryValidationError(name, direction, issues),
595
+ invoke: (name, validated) => workflowHandle.query(name, validated),
596
+ validateOutput: (def) => def.output
597
+ });
598
+ const signals = buildValidatedProxy({
599
+ defs: definition.signals,
600
+ operation: "signal",
601
+ workflowId: workflowHandle.workflowId,
602
+ makeValidationError: (name, _direction, issues) => new SignalValidationError(name, issues),
603
+ invoke: async (name, validated) => {
604
+ await workflowHandle.signal(name, validated);
605
+ },
606
+ validateOutput: () => null
607
+ });
608
+ const updates = buildValidatedProxy({
609
+ defs: definition.updates,
610
+ operation: "update",
611
+ workflowId: workflowHandle.workflowId,
612
+ makeValidationError: (name, direction, issues) => new UpdateValidationError(name, direction, issues),
613
+ invoke: (name, validated) => workflowHandle.executeUpdate(name, { args: [validated] }),
614
+ validateOutput: (def) => def.output
615
+ });
311
616
  return {
312
617
  workflowId: workflowHandle.workflowId,
313
618
  queries,
314
619
  signals,
315
620
  updates,
316
621
  result: () => {
317
- return _swan_io_boxed.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(_swan_io_boxed.Result.Error(new WorkflowValidationError(workflowHandle.workflowId, "output", outputResult.issues)));
324
- return;
325
- }
326
- resolve(_swan_io_boxed.Result.Ok(outputResult.value));
327
- } catch (error) {
328
- resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("result", error)));
329
- }
330
- })();
331
- });
332
- },
333
- terminate: (reason) => {
334
- return _swan_io_boxed.Future.fromPromise(workflowHandle.terminate(reason)).mapError((error) => createRuntimeClientError("terminate", error)).mapOk(() => void 0);
335
- },
336
- cancel: () => {
337
- return _swan_io_boxed.Future.fromPromise(workflowHandle.cancel()).mapError((error) => createRuntimeClientError("cancel", error)).mapOk(() => void 0);
338
- },
339
- describe: () => {
340
- return _swan_io_boxed.Future.fromPromise(workflowHandle.describe()).mapError((error) => createRuntimeClientError("describe", error));
622
+ const work = async () => {
623
+ try {
624
+ const result = await workflowHandle.result();
625
+ const outputResult = await definition.output["~standard"].validate(result);
626
+ if (outputResult.issues) return _swan_io_boxed.Result.Error(new WorkflowValidationError(workflowHandle.workflowId, "output", outputResult.issues));
627
+ return _swan_io_boxed.Result.Ok(outputResult.value);
628
+ } catch (error) {
629
+ return _swan_io_boxed.Result.Error(classifyResultError("result", error, workflowHandle.workflowId));
630
+ }
631
+ };
632
+ return makeFuture(work);
341
633
  },
342
- fetchHistory: () => {
343
- return _swan_io_boxed.Future.fromPromise(workflowHandle.fetchHistory()).mapError((error) => createRuntimeClientError("fetchHistory", error));
344
- }
634
+ terminate: (reason) => _swan_io_boxed.Future.fromPromise(workflowHandle.terminate(reason)).mapError((error) => classifyHandleError("terminate", error, workflowHandle.workflowId)).mapOk(() => void 0),
635
+ cancel: () => _swan_io_boxed.Future.fromPromise(workflowHandle.cancel()).mapError((error) => classifyHandleError("cancel", error, workflowHandle.workflowId)).mapOk(() => void 0),
636
+ describe: () => _swan_io_boxed.Future.fromPromise(workflowHandle.describe()).mapError((error) => classifyHandleError("describe", error, workflowHandle.workflowId)),
637
+ fetchHistory: () => _swan_io_boxed.Future.fromPromise(workflowHandle.fetchHistory()).mapError((error) => classifyHandleError("fetchHistory", error, workflowHandle.workflowId))
345
638
  };
346
639
  }
347
640
  };
@@ -354,11 +647,44 @@ function createWorkflowNotFoundError(workflowName, contract) {
354
647
  function createWorkflowValidationError(workflowName, direction, issues) {
355
648
  return new WorkflowValidationError(String(workflowName), direction, issues);
356
649
  }
357
-
650
+ /**
651
+ * Build a `{ name: (args) => Future<Result<...>> }` proxy for a contract's
652
+ * queries/signals/updates. The three call sites differ only in how they
653
+ * invoke Temporal and whether they validate output, so the shared
654
+ * input-validate → invoke → output-validate → wrap-Result pipeline lives
655
+ * here once.
656
+ */
657
+ function buildValidatedProxy({ defs, operation, workflowId, makeValidationError, invoke, validateOutput }) {
658
+ const proxy = {};
659
+ if (!defs) return proxy;
660
+ for (const [name, def] of Object.entries(defs)) proxy[name] = (args) => {
661
+ const work = async () => {
662
+ const inputResult = await def.input["~standard"].validate(args);
663
+ if (inputResult.issues) return _swan_io_boxed.Result.Error(makeValidationError(name, "input", inputResult.issues));
664
+ try {
665
+ const result = await invoke(name, inputResult.value);
666
+ const outputSchema = validateOutput(def);
667
+ if (!outputSchema) return _swan_io_boxed.Result.Ok(result);
668
+ const outputResult = await outputSchema["~standard"].validate(result);
669
+ if (outputResult.issues) return _swan_io_boxed.Result.Error(makeValidationError(name, "output", outputResult.issues));
670
+ return _swan_io_boxed.Result.Ok(outputResult.value);
671
+ } catch (error) {
672
+ return _swan_io_boxed.Result.Error(classifyHandleError(operation, error, workflowId));
673
+ }
674
+ };
675
+ return makeFuture(work);
676
+ };
677
+ return proxy;
678
+ }
358
679
  //#endregion
359
680
  exports.QueryValidationError = QueryValidationError;
681
+ exports.RuntimeClientError = RuntimeClientError;
360
682
  exports.SignalValidationError = SignalValidationError;
361
683
  exports.TypedClient = TypedClient;
684
+ exports.TypedScheduleClient = TypedScheduleClient;
362
685
  exports.UpdateValidationError = UpdateValidationError;
686
+ exports.WorkflowAlreadyStartedError = WorkflowAlreadyStartedError;
687
+ exports.WorkflowExecutionNotFoundError = WorkflowExecutionNotFoundError;
688
+ exports.WorkflowFailedError = WorkflowFailedError;
363
689
  exports.WorkflowNotFoundError = WorkflowNotFoundError;
364
- exports.WorkflowValidationError = WorkflowValidationError;
690
+ exports.WorkflowValidationError = WorkflowValidationError;