@temporal-contract/client 0.2.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,6 +1,7 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _temporalio_common = require("@temporalio/common");
2
3
  let _swan_io_boxed = require("@swan-io/boxed");
3
-
4
+ let _temporalio_client = require("@temporalio/client");
4
5
  //#region src/errors.ts
5
6
  /**
6
7
  * Base class for all typed client errors with boxed pattern
@@ -33,11 +34,117 @@ var WorkflowNotFoundError = class extends TypedClientError {
33
34
  }
34
35
  };
35
36
  /**
37
+ * Discriminated variant of {@link RuntimeClientError} surfaced when starting
38
+ * a workflow collides with an existing execution — Temporal's
39
+ * `WorkflowExecutionAlreadyStartedError`. The most common cause is a
40
+ * workflowId reuse policy that rejects duplicates while a previous run is
41
+ * still in retention.
42
+ *
43
+ * Distinguishing this from `RuntimeClientError` lets idempotent callers
44
+ * branch on it explicitly (e.g. fetch the existing handle and continue)
45
+ * without inspecting `error.cause` against a Temporal SDK class.
46
+ */
47
+ var WorkflowAlreadyStartedError = class extends TypedClientError {
48
+ constructor(workflowType, workflowId, cause) {
49
+ super(`Workflow "${workflowType}" with ID "${workflowId}" is already started or in retention.`);
50
+ this.workflowType = workflowType;
51
+ this.workflowId = workflowId;
52
+ this.cause = cause;
53
+ }
54
+ };
55
+ /**
56
+ * Discriminated variant of {@link RuntimeClientError} surfaced when an
57
+ * operation targets a workflow execution that doesn't exist in the
58
+ * namespace — Temporal's `WorkflowNotFoundError` (distinct from this
59
+ * package's contract-level {@link WorkflowNotFoundError}).
60
+ *
61
+ * Returned from:
62
+ * - handle methods: `signal`, `query`, `executeUpdate`, `result`,
63
+ * `terminate`, `cancel`, `describe`, `fetchHistory`
64
+ * - `executeWorkflow` (when the underlying execute call hits a missing
65
+ * execution mid-flight)
66
+ */
67
+ var WorkflowExecutionNotFoundError = class extends TypedClientError {
68
+ constructor(workflowId, runId, cause) {
69
+ super(`Workflow execution "${workflowId}"${runId ? ` (run "${runId}")` : ""} not found in namespace.`);
70
+ this.workflowId = workflowId;
71
+ this.runId = runId;
72
+ this.cause = cause;
73
+ }
74
+ };
75
+ /**
76
+ * Discriminated variant of {@link RuntimeClientError} surfaced when waiting
77
+ * on a workflow's result and the workflow completes with a failure —
78
+ * Temporal's `WorkflowFailedError`.
79
+ *
80
+ * `cause` is the *unwrapped* underlying failure (typically an
81
+ * `ApplicationFailure`, `CancelledFailure`, `TerminatedFailure`, or
82
+ * `TimeoutFailure`) lifted from Temporal's wrapper, so callers can branch
83
+ * on the failure category in one step (`err.cause instanceof
84
+ * ApplicationFailure`) instead of unwrapping twice via the SDK wrapper.
85
+ *
86
+ * Returned from `executeWorkflow` and `handle.result()`.
87
+ */
88
+ var WorkflowFailedError = class extends TypedClientError {
89
+ constructor(workflowId, cause) {
90
+ const causeMessage = cause instanceof Error ? cause.message : String(cause ?? "unknown failure");
91
+ super(`Workflow "${workflowId}" completed with failure: ${causeMessage}`);
92
+ this.workflowId = workflowId;
93
+ this.cause = cause;
94
+ }
95
+ };
96
+ /**
97
+ * Pattern for string keys safe to render with dot notation. A "safe" key is a
98
+ * JavaScript identifier (letters/digits/underscore/$, not starting with a
99
+ * digit). Anything else — keys containing dots, spaces, leading digits, the
100
+ * empty string, the literal string `"0"` etc. — gets bracket-quoted so the
101
+ * path is unambiguous.
102
+ *
103
+ * This helper is intentionally duplicated with the worker package so each
104
+ * entry point is self-contained; keep the two copies in sync.
105
+ */
106
+ const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
107
+ /**
108
+ * Render a Standard Schema {@link StandardSchemaV1.Issue} into a human-readable
109
+ * string that includes the failing field's path.
110
+ *
111
+ * Example output:
112
+ * - `at items[0].quantity: Expected number, received undefined`
113
+ * - `at customerId: Expected string, received undefined`
114
+ * - `at user["first name"]: Expected string, received undefined`
115
+ * - `Validation error` *(no path)*
116
+ *
117
+ * Path segments come either as bare `PropertyKey` values or as
118
+ * `{ key: PropertyKey }` objects (per the spec); both are normalized.
119
+ * - Numeric keys → `[N]`
120
+ * - String keys that are valid JS identifiers → bare (first) or `.key`
121
+ * - String keys that aren't valid identifiers → `["..."]` with JSON-style
122
+ * escaping (handles dots, spaces, leading digits, the empty string, the
123
+ * literal string `"0"`, embedded quotes, etc.)
124
+ * - Symbol / other `PropertyKey` → `[Symbol(name)]`
125
+ */
126
+ function formatIssue(issue) {
127
+ if (issue.path === void 0 || issue.path.length === 0) return issue.message;
128
+ let path = "";
129
+ for (let i = 0; i < issue.path.length; i++) {
130
+ const segment = issue.path[i];
131
+ const key = segment !== null && typeof segment === "object" && "key" in segment ? segment.key : segment;
132
+ if (typeof key === "number") path += `[${key}]`;
133
+ else if (typeof key === "string" && SAFE_IDENTIFIER.test(key)) path += i === 0 ? key : `.${key}`;
134
+ else if (typeof key === "string") path += `[${JSON.stringify(key)}]`;
135
+ else path += `[${String(key)}]`;
136
+ }
137
+ return `at ${path}: ${issue.message}`;
138
+ }
139
+ function summarizeIssues(issues) {
140
+ return issues.map(formatIssue).join("; ");
141
+ }
142
+ /**
36
143
  * Thrown when workflow input or output validation fails
37
144
  */
38
145
  var WorkflowValidationError = class extends TypedClientError {
39
146
  constructor(workflowName, direction, issues) {
40
- super(`Validation failed for workflow "${workflowName}" ${direction}: ${JSON.stringify(issues)}`);
147
+ super(`Validation failed for workflow "${workflowName}" ${direction}: ${summarizeIssues(issues)}`);
41
148
  this.workflowName = workflowName;
42
149
  this.direction = direction;
43
150
  this.issues = issues;
@@ -48,7 +155,7 @@ var WorkflowValidationError = class extends TypedClientError {
48
155
  */
49
156
  var QueryValidationError = class extends TypedClientError {
50
157
  constructor(queryName, direction, issues) {
51
- super(`Validation failed for query "${queryName}" ${direction}: ${JSON.stringify(issues)}`);
158
+ super(`Validation failed for query "${queryName}" ${direction}: ${summarizeIssues(issues)}`);
52
159
  this.queryName = queryName;
53
160
  this.direction = direction;
54
161
  this.issues = issues;
@@ -59,7 +166,7 @@ var QueryValidationError = class extends TypedClientError {
59
166
  */
60
167
  var SignalValidationError = class extends TypedClientError {
61
168
  constructor(signalName, issues) {
62
- super(`Validation failed for signal "${signalName}": ${JSON.stringify(issues)}`);
169
+ super(`Validation failed for signal "${signalName}": ${summarizeIssues(issues)}`);
63
170
  this.signalName = signalName;
64
171
  this.issues = issues;
65
172
  }
@@ -69,25 +176,221 @@ var SignalValidationError = class extends TypedClientError {
69
176
  */
70
177
  var UpdateValidationError = class extends TypedClientError {
71
178
  constructor(updateName, direction, issues) {
72
- super(`Validation failed for update "${updateName}" ${direction}: ${JSON.stringify(issues)}`);
179
+ super(`Validation failed for update "${updateName}" ${direction}: ${summarizeIssues(issues)}`);
73
180
  this.updateName = updateName;
74
181
  this.direction = direction;
75
182
  this.issues = issues;
76
183
  }
77
184
  };
78
-
185
+ //#endregion
186
+ //#region src/internal.ts
187
+ /**
188
+ * Internal helpers shared across the client package's modules.
189
+ *
190
+ * Not part of the public API — this module is not listed in the package's
191
+ * `exports` map, so consumers can't import from `@temporal-contract/client/internal`.
192
+ * In-package modules and tests import it directly via relative path.
193
+ */
194
+ /**
195
+ * Wrap an async result-producing function in a `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
+ }
79
331
  //#endregion
80
332
  //#region src/client.ts
81
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
+ /**
82
358
  * Typed Temporal client with Result/Future pattern based on a contract
83
359
  *
84
360
  * Provides type-safe methods to start and execute workflows
85
361
  * defined in the contract, with explicit error handling using Result pattern.
86
362
  */
87
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;
88
389
  constructor(contract, client) {
89
390
  this.contract = contract;
90
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);
91
394
  }
92
395
  /**
93
396
  * Create a typed Temporal client with boxed pattern from a contract
@@ -133,27 +436,83 @@ var TypedClient = class TypedClient {
133
436
  * });
134
437
  * ```
135
438
  */
136
- startWorkflow(workflowName, { args, ...temporalOptions }) {
137
- return _swan_io_boxed.Future.make((resolve) => {
138
- (async () => {
139
- const definition = this.contract.workflows[workflowName];
140
- if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
141
- const inputResult = await definition.input["~standard"].validate(args);
142
- if (inputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues));
143
- const validatedInput = inputResult.value;
144
- try {
145
- const handle = await this.client.workflow.start(workflowName, {
146
- ...temporalOptions,
147
- taskQueue: this.contract.taskQueue,
148
- args: [validatedInput]
149
- });
150
- const typedHandle = this.createTypedHandle(handle, definition);
151
- return _swan_io_boxed.Result.Ok(typedHandle);
152
- } catch (error) {
153
- return _swan_io_boxed.Result.Error(createRuntimeClientError("startWorkflow", error));
154
- }
155
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
156
- });
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);
157
516
  }
158
517
  /**
159
518
  * Execute a workflow (start and wait for result) with Future/Result pattern
@@ -173,28 +532,31 @@ var TypedClient = class TypedClient {
173
532
  * });
174
533
  * ```
175
534
  */
176
- executeWorkflow(workflowName, { args, ...temporalOptions }) {
177
- return _swan_io_boxed.Future.make((resolve) => {
178
- (async () => {
179
- const definition = this.contract.workflows[workflowName];
180
- if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
181
- const inputResult = await definition.input["~standard"].validate(args);
182
- if (inputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "input", inputResult.issues));
183
- const validatedInput = inputResult.value;
184
- try {
185
- const result = await this.client.workflow.execute(workflowName, {
186
- ...temporalOptions,
187
- taskQueue: this.contract.taskQueue,
188
- args: [validatedInput]
189
- });
190
- const outputResult = await definition.output["~standard"].validate(result);
191
- if (outputResult.issues) return _swan_io_boxed.Result.Error(createWorkflowValidationError(workflowName, "output", outputResult.issues));
192
- return _swan_io_boxed.Result.Ok(outputResult.value);
193
- } catch (error) {
194
- return _swan_io_boxed.Result.Error(createRuntimeClientError("executeWorkflow", error));
195
- }
196
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
197
- });
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);
198
560
  }
199
561
  /**
200
562
  * Get a handle to an existing workflow with Future/Result pattern
@@ -212,101 +574,67 @@ var TypedClient = class TypedClient {
212
574
  * ```
213
575
  */
214
576
  getHandle(workflowName, workflowId) {
215
- return _swan_io_boxed.Future.make((resolve) => {
216
- (async () => {
217
- const definition = this.contract.workflows[workflowName];
218
- if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
219
- try {
220
- const handle = this.client.workflow.getHandle(workflowId);
221
- const typedHandle = this.createTypedHandle(handle, definition);
222
- return _swan_io_boxed.Result.Ok(typedHandle);
223
- } catch (error) {
224
- return _swan_io_boxed.Result.Error(createRuntimeClientError("getHandle", error));
225
- }
226
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
227
- });
577
+ const work = async () => {
578
+ const definition = this.contract.workflows[workflowName];
579
+ if (!definition) return _swan_io_boxed.Result.Error(createWorkflowNotFoundError(workflowName, this.contract));
580
+ try {
581
+ const handle = this.client.workflow.getHandle(workflowId);
582
+ return _swan_io_boxed.Result.Ok(this.createTypedHandle(handle, definition));
583
+ } catch (error) {
584
+ return _swan_io_boxed.Result.Error(createRuntimeClientError("getHandle", error));
585
+ }
586
+ };
587
+ return makeFuture(work);
228
588
  }
229
589
  createTypedHandle(workflowHandle, definition) {
230
- const queries = {};
231
- for (const [queryName, queryDef] of Object.entries(definition.queries ?? {})) queries[queryName] = (args) => {
232
- return _swan_io_boxed.Future.make((resolve) => {
233
- (async () => {
234
- const inputResult = await queryDef.input["~standard"].validate(args);
235
- if (inputResult.issues) return _swan_io_boxed.Result.Error(new QueryValidationError(queryName, "input", inputResult.issues));
236
- try {
237
- const result = await workflowHandle.query(queryName, inputResult.value);
238
- const outputResult = await queryDef.output["~standard"].validate(result);
239
- if (outputResult.issues) return _swan_io_boxed.Result.Error(new QueryValidationError(queryName, "output", outputResult.issues));
240
- return _swan_io_boxed.Result.Ok(outputResult.value);
241
- } catch (error) {
242
- return _swan_io_boxed.Result.Error(createRuntimeClientError("query", error));
243
- }
244
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
245
- });
246
- };
247
- const signals = {};
248
- for (const [signalName, signalDef] of Object.entries(definition.signals ?? {})) signals[signalName] = (args) => {
249
- return _swan_io_boxed.Future.make((resolve) => {
250
- (async () => {
251
- const inputResult = await signalDef.input["~standard"].validate(args);
252
- if (inputResult.issues) return _swan_io_boxed.Result.Error(new SignalValidationError(signalName, inputResult.issues));
253
- try {
254
- await workflowHandle.signal(signalName, inputResult.value);
255
- return _swan_io_boxed.Result.Ok(void 0);
256
- } catch (error) {
257
- return _swan_io_boxed.Result.Error(createRuntimeClientError("signal", error));
258
- }
259
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
260
- });
261
- };
262
- const updates = {};
263
- for (const [updateName, updateDef] of Object.entries(definition.updates ?? {})) updates[updateName] = (args) => {
264
- return _swan_io_boxed.Future.make((resolve) => {
265
- (async () => {
266
- const inputResult = await updateDef.input["~standard"].validate(args);
267
- if (inputResult.issues) return _swan_io_boxed.Result.Error(new UpdateValidationError(updateName, "input", inputResult.issues));
268
- try {
269
- const result = await workflowHandle.executeUpdate(updateName, { args: [inputResult.value] });
270
- const outputResult = await updateDef.output["~standard"].validate(result);
271
- if (outputResult.issues) return _swan_io_boxed.Result.Error(new UpdateValidationError(updateName, "output", outputResult.issues));
272
- return _swan_io_boxed.Result.Ok(outputResult.value);
273
- } catch (error) {
274
- return _swan_io_boxed.Result.Error(createRuntimeClientError("update", error));
275
- }
276
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
277
- });
278
- };
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
+ });
279
616
  return {
280
617
  workflowId: workflowHandle.workflowId,
281
618
  queries,
282
619
  signals,
283
620
  updates,
284
621
  result: () => {
285
- return _swan_io_boxed.Future.make((resolve) => {
286
- (async () => {
287
- try {
288
- const result = await workflowHandle.result();
289
- const outputResult = await definition.output["~standard"].validate(result);
290
- if (outputResult.issues) return _swan_io_boxed.Result.Error(new WorkflowValidationError(workflowHandle.workflowId, "output", outputResult.issues));
291
- return _swan_io_boxed.Result.Ok(outputResult.value);
292
- } catch (error) {
293
- return _swan_io_boxed.Result.Error(createRuntimeClientError("result", error));
294
- }
295
- })().then(resolve).catch((e) => resolve(_swan_io_boxed.Result.Error(createRuntimeClientError("unexpected", e))));
296
- });
297
- },
298
- terminate: (reason) => {
299
- return _swan_io_boxed.Future.fromPromise(workflowHandle.terminate(reason)).mapError((error) => createRuntimeClientError("terminate", error)).mapOk(() => void 0);
300
- },
301
- cancel: () => {
302
- return _swan_io_boxed.Future.fromPromise(workflowHandle.cancel()).mapError((error) => createRuntimeClientError("cancel", error)).mapOk(() => void 0);
303
- },
304
- describe: () => {
305
- return _swan_io_boxed.Future.fromPromise(workflowHandle.describe()).mapError((error) => createRuntimeClientError("describe", error));
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);
306
633
  },
307
- fetchHistory: () => {
308
- return _swan_io_boxed.Future.fromPromise(workflowHandle.fetchHistory()).mapError((error) => createRuntimeClientError("fetchHistory", error));
309
- }
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))
310
638
  };
311
639
  }
312
640
  };
@@ -319,11 +647,44 @@ function createWorkflowNotFoundError(workflowName, contract) {
319
647
  function createWorkflowValidationError(workflowName, direction, issues) {
320
648
  return new WorkflowValidationError(String(workflowName), direction, issues);
321
649
  }
322
-
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
+ }
323
679
  //#endregion
324
680
  exports.QueryValidationError = QueryValidationError;
681
+ exports.RuntimeClientError = RuntimeClientError;
325
682
  exports.SignalValidationError = SignalValidationError;
326
683
  exports.TypedClient = TypedClient;
684
+ exports.TypedScheduleClient = TypedScheduleClient;
327
685
  exports.UpdateValidationError = UpdateValidationError;
686
+ exports.WorkflowAlreadyStartedError = WorkflowAlreadyStartedError;
687
+ exports.WorkflowExecutionNotFoundError = WorkflowExecutionNotFoundError;
688
+ exports.WorkflowFailedError = WorkflowFailedError;
328
689
  exports.WorkflowNotFoundError = WorkflowNotFoundError;
329
- exports.WorkflowValidationError = WorkflowValidationError;
690
+ exports.WorkflowValidationError = WorkflowValidationError;