@temporal-contract/client 0.0.1

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/src/client.ts ADDED
@@ -0,0 +1,390 @@
1
+ import { Client, WorkflowHandle } from "@temporalio/client";
2
+ import type { ClientOptions, WorkflowStartOptions, WorkflowOptions } from "@temporalio/client";
3
+ import { ZodError } from "zod";
4
+ import type {
5
+ ClientInferInput,
6
+ ClientInferOutput,
7
+ ClientInferWorkflowQueries,
8
+ ClientInferWorkflowSignals,
9
+ ClientInferWorkflowUpdates,
10
+ ContractDefinition,
11
+ WorkflowDefinition,
12
+ QueryDefinition,
13
+ SignalDefinition,
14
+ UpdateDefinition,
15
+ } from "@temporal-contract/contract";
16
+ import {
17
+ WorkflowNotFoundError,
18
+ WorkflowValidationError,
19
+ QueryValidationError,
20
+ SignalValidationError,
21
+ UpdateValidationError,
22
+ } from "./errors.js";
23
+
24
+ /**
25
+ * Extended options for starting workflows with Temporal-specific features
26
+ * Combines required workflowId with optional Temporal workflow options
27
+ */
28
+ export type TypedWorkflowStartOptions = Pick<
29
+ WorkflowStartOptions,
30
+ | "workflowId"
31
+ | "workflowIdReusePolicy"
32
+ | "workflowExecutionTimeout"
33
+ | "workflowRunTimeout"
34
+ | "workflowTaskTimeout"
35
+ | "retry"
36
+ | "memo"
37
+ | "searchAttributes"
38
+ | "cronSchedule"
39
+ > &
40
+ Pick<WorkflowOptions, "workflowId">;
41
+
42
+ /**
43
+ * Typed workflow handle with validated results, queries, signals and updates
44
+ */
45
+ export interface TypedWorkflowHandle<TWorkflow extends WorkflowDefinition> {
46
+ workflowId: string;
47
+
48
+ /**
49
+ * Type-safe queries based on workflow definition
50
+ */
51
+ queries: ClientInferWorkflowQueries<TWorkflow>;
52
+
53
+ /**
54
+ * Type-safe signals based on workflow definition
55
+ */
56
+ signals: ClientInferWorkflowSignals<TWorkflow>;
57
+
58
+ /**
59
+ * Type-safe updates based on workflow definition
60
+ */
61
+ updates: ClientInferWorkflowUpdates<TWorkflow>;
62
+
63
+ result: () => Promise<ClientInferOutput<TWorkflow>>;
64
+ terminate: (reason?: string) => Promise<void>;
65
+ cancel: () => Promise<void>;
66
+
67
+ /**
68
+ * Get workflow execution description including status and metadata
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const handle = await client.getHandle('processOrder', 'order-123');
73
+ * const description = await handle.describe();
74
+ * console.log(description.workflowExecutionInfo.status); // RUNNING, COMPLETED, etc.
75
+ * ```
76
+ */
77
+ describe: () => ReturnType<WorkflowHandle["describe"]>;
78
+
79
+ /**
80
+ * Fetch the workflow execution history
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const handle = await client.getHandle('processOrder', 'order-123');
85
+ * const history = handle.fetchHistory();
86
+ * for await (const event of history) {
87
+ * console.log(event);
88
+ * }
89
+ * ```
90
+ */
91
+ fetchHistory: () => ReturnType<WorkflowHandle["fetchHistory"]>;
92
+ }
93
+
94
+ /**
95
+ * Typed Temporal client based on a contract
96
+ *
97
+ * Provides type-safe methods to start and execute workflows
98
+ * defined in the contract.
99
+ */
100
+ export class TypedClient<TContract extends ContractDefinition> {
101
+ private constructor(
102
+ private readonly contract: TContract,
103
+ private readonly client: Client,
104
+ ) {}
105
+
106
+ /**
107
+ * Create a typed Temporal client from a contract
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const connection = await Connection.connect();
112
+ * const client = TypedClient.create(myContract, {
113
+ * connection,
114
+ * namespace: 'default',
115
+ * });
116
+ *
117
+ * const result = await client.executeWorkflow('processOrder', {
118
+ * workflowId: 'order-123',
119
+ * args: [...],
120
+ * });
121
+ * ```
122
+ */
123
+ static create<TContract extends ContractDefinition>(
124
+ contract: TContract,
125
+ options: ClientOptions,
126
+ ): TypedClient<TContract> {
127
+ const client = new Client(options);
128
+ return new TypedClient(contract, client);
129
+ }
130
+
131
+ /**
132
+ * Start a workflow and return a typed handle
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const handle = await client.startWorkflow('processOrder', {
137
+ * workflowId: 'order-123',
138
+ * args: ['ORD-123', 'CUST-456', [{ productId: 'PROD-1', quantity: 2 }]],
139
+ * workflowExecutionTimeout: '1 day',
140
+ * retry: { maximumAttempts: 3 },
141
+ * });
142
+ *
143
+ * const result = await handle.result();
144
+ * ```
145
+ */
146
+ async startWorkflow<TWorkflowName extends keyof TContract["workflows"]>(
147
+ workflowName: TWorkflowName,
148
+ {
149
+ args,
150
+ ...temporalOptions
151
+ }: TypedWorkflowStartOptions & {
152
+ args: ClientInferInput<TContract["workflows"][TWorkflowName]>;
153
+ },
154
+ ): Promise<TypedWorkflowHandle<TContract["workflows"][TWorkflowName]>> {
155
+ const definition = this.contract.workflows[workflowName as string];
156
+
157
+ if (!definition) {
158
+ throw new WorkflowNotFoundError(String(workflowName));
159
+ }
160
+
161
+ // Validate input with Zod schema (tuple)
162
+ let validatedInput: ClientInferInput<TContract["workflows"][TWorkflowName]>;
163
+ try {
164
+ validatedInput = definition.input.parse(args) as ClientInferInput<
165
+ TContract["workflows"][TWorkflowName]
166
+ >;
167
+ } catch (error) {
168
+ if (error instanceof ZodError) {
169
+ throw new WorkflowValidationError(String(workflowName), "input", error);
170
+ }
171
+ throw error;
172
+ }
173
+
174
+ // Start workflow (Temporal expects args as array, so wrap single parameter)
175
+ const handle = await this.client.workflow.start(workflowName as string, {
176
+ ...temporalOptions,
177
+ taskQueue: this.contract.taskQueue,
178
+ args: [validatedInput],
179
+ });
180
+
181
+ return this.createTypedHandle(handle, definition) as TypedWorkflowHandle<
182
+ TContract["workflows"][TWorkflowName]
183
+ >;
184
+ }
185
+
186
+ /**
187
+ * Execute a workflow (start and wait for result)
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const result = await client.executeWorkflow('processOrder', {
192
+ * workflowId: 'order-123',
193
+ * args: ['ORD-123', 'CUST-456', [{ productId: 'PROD-1', quantity: 2 }]],
194
+ * workflowExecutionTimeout: '1 day',
195
+ * retry: { maximumAttempts: 3 },
196
+ * });
197
+ *
198
+ * console.log(result.status); // fully typed!
199
+ * ```
200
+ */
201
+ async executeWorkflow<TWorkflowName extends keyof TContract["workflows"]>(
202
+ workflowName: TWorkflowName,
203
+ {
204
+ args,
205
+ ...temporalOptions
206
+ }: TypedWorkflowStartOptions & {
207
+ args: ClientInferInput<TContract["workflows"][TWorkflowName]>;
208
+ },
209
+ ): Promise<ClientInferOutput<TContract["workflows"][TWorkflowName]>> {
210
+ const definition = this.contract.workflows[workflowName as string];
211
+
212
+ if (!definition) {
213
+ throw new WorkflowNotFoundError(String(workflowName));
214
+ }
215
+
216
+ // Validate input with Zod schema
217
+ let validatedInput: ClientInferInput<TContract["workflows"][TWorkflowName]>;
218
+ try {
219
+ validatedInput = definition.input.parse(args) as ClientInferInput<
220
+ TContract["workflows"][TWorkflowName]
221
+ >;
222
+ } catch (error) {
223
+ if (error instanceof ZodError) {
224
+ throw new WorkflowValidationError(String(workflowName), "input", error);
225
+ }
226
+ throw error;
227
+ }
228
+
229
+ // Execute workflow (Temporal expects args as array, so wrap single parameter)
230
+ const result = await this.client.workflow.execute(workflowName as string, {
231
+ ...temporalOptions,
232
+ taskQueue: this.contract.taskQueue,
233
+ args: [validatedInput],
234
+ });
235
+
236
+ // Validate output with Zod schema
237
+ try {
238
+ return definition.output.parse(result) as ClientInferOutput<
239
+ TContract["workflows"][TWorkflowName]
240
+ >;
241
+ } catch (error) {
242
+ if (error instanceof ZodError) {
243
+ throw new WorkflowValidationError(String(workflowName), "output", error);
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get a handle to an existing workflow
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * const handle = await client.getHandle('processOrder', 'order-123');
255
+ * const result = await handle.result();
256
+ * ```
257
+ */
258
+ async getHandle<TWorkflowName extends keyof TContract["workflows"]>(
259
+ workflowName: TWorkflowName,
260
+ workflowId: string,
261
+ ): Promise<TypedWorkflowHandle<TContract["workflows"][TWorkflowName]>> {
262
+ const definition = this.contract.workflows[workflowName as string];
263
+
264
+ if (!definition) {
265
+ throw new WorkflowNotFoundError(String(workflowName));
266
+ }
267
+
268
+ const handle = this.client.workflow.getHandle(workflowId);
269
+ return this.createTypedHandle(handle, definition) as TypedWorkflowHandle<
270
+ TContract["workflows"][TWorkflowName]
271
+ >;
272
+ }
273
+
274
+ private createTypedHandle<TWorkflow extends WorkflowDefinition>(
275
+ handle: WorkflowHandle,
276
+ definition: TWorkflow,
277
+ ): TypedWorkflowHandle<TWorkflow> {
278
+ // Create typed queries proxy
279
+ const queries = {} as ClientInferWorkflowQueries<TWorkflow>;
280
+ for (const [queryName, queryDef] of Object.entries(definition.queries ?? {}) as Array<
281
+ [string, QueryDefinition]
282
+ >) {
283
+ (queries as Record<string, unknown>)[queryName] = async (
284
+ args: ClientInferInput<typeof queryDef>,
285
+ ) => {
286
+ let validatedInput: ClientInferInput<typeof queryDef>;
287
+ try {
288
+ validatedInput = queryDef.input.parse(args) as ClientInferInput<typeof queryDef>;
289
+ } catch (error) {
290
+ if (error instanceof ZodError) {
291
+ throw new QueryValidationError(queryName, "input", error);
292
+ }
293
+ throw error;
294
+ }
295
+
296
+ const result = await handle.query(queryName as string, validatedInput);
297
+
298
+ try {
299
+ return queryDef.output.parse(result);
300
+ } catch (error) {
301
+ if (error instanceof ZodError) {
302
+ throw new QueryValidationError(queryName, "output", error);
303
+ }
304
+ throw error;
305
+ }
306
+ };
307
+ }
308
+
309
+ // Create typed signals proxy
310
+ const signals = {} as ClientInferWorkflowSignals<TWorkflow>;
311
+ for (const [signalName, signalDef] of Object.entries(definition.signals ?? {}) as Array<
312
+ [string, SignalDefinition]
313
+ >) {
314
+ (signals as Record<string, unknown>)[signalName] = async (
315
+ args: ClientInferInput<typeof signalDef>,
316
+ ) => {
317
+ let validatedInput: ClientInferInput<typeof signalDef>;
318
+ try {
319
+ validatedInput = signalDef.input.parse(args) as ClientInferInput<typeof signalDef>;
320
+ } catch (error) {
321
+ if (error instanceof ZodError) {
322
+ throw new SignalValidationError(signalName, error);
323
+ }
324
+ throw error;
325
+ }
326
+ await handle.signal(signalName as string, validatedInput);
327
+ };
328
+ }
329
+
330
+ // Create typed updates proxy
331
+ const updates = {} as ClientInferWorkflowUpdates<TWorkflow>;
332
+ for (const [updateName, updateDef] of Object.entries(definition.updates ?? {}) as Array<
333
+ [string, UpdateDefinition]
334
+ >) {
335
+ (updates as Record<string, unknown>)[updateName] = async (
336
+ args: ClientInferInput<typeof updateDef>,
337
+ ) => {
338
+ let validatedInput: ClientInferInput<typeof updateDef>;
339
+ try {
340
+ validatedInput = updateDef.input.parse(args) as ClientInferInput<typeof updateDef>;
341
+ } catch (error) {
342
+ if (error instanceof ZodError) {
343
+ throw new UpdateValidationError(updateName, "input", error);
344
+ }
345
+ throw error;
346
+ }
347
+
348
+ const result = await handle.executeUpdate(updateName as string, { args: [validatedInput] });
349
+
350
+ try {
351
+ return updateDef.output.parse(result);
352
+ } catch (error) {
353
+ if (error instanceof ZodError) {
354
+ throw new UpdateValidationError(updateName, "output", error);
355
+ }
356
+ throw error;
357
+ }
358
+ };
359
+ }
360
+
361
+ const typedHandle: TypedWorkflowHandle<TWorkflow> = {
362
+ workflowId: handle.workflowId,
363
+ queries,
364
+ signals,
365
+ updates,
366
+ result: async () => {
367
+ const result = await handle.result();
368
+ // Validate output with Zod schema
369
+ try {
370
+ return definition.output.parse(result) as ClientInferOutput<TWorkflow>;
371
+ } catch (error) {
372
+ if (error instanceof ZodError) {
373
+ throw new WorkflowValidationError(handle.workflowId, "output", error);
374
+ }
375
+ throw error;
376
+ }
377
+ },
378
+ terminate: async (reason?: string) => {
379
+ await handle.terminate(reason);
380
+ },
381
+ cancel: async () => {
382
+ await handle.cancel();
383
+ },
384
+ describe: () => handle.describe(),
385
+ fetchHistory: () => handle.fetchHistory(),
386
+ };
387
+
388
+ return typedHandle;
389
+ }
390
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * Base error class for typed client errors
5
+ */
6
+ export class TypedClientError extends Error {
7
+ constructor(message: string) {
8
+ super(message);
9
+ this.name = "TypedClientError";
10
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
11
+ if (Error.captureStackTrace) {
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Error thrown when a workflow is not found in the contract
19
+ */
20
+ export class WorkflowNotFoundError extends TypedClientError {
21
+ constructor(public readonly workflowName: string) {
22
+ super(`Workflow "${workflowName}" not found in contract`);
23
+ this.name = "WorkflowNotFoundError";
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Error thrown when workflow input or output validation fails
29
+ */
30
+ export class WorkflowValidationError extends TypedClientError {
31
+ constructor(
32
+ public readonly workflowName: string,
33
+ public readonly phase: "input" | "output",
34
+ public readonly zodError: z.ZodError,
35
+ ) {
36
+ super(`Validation failed for workflow "${workflowName}" ${phase}: ${zodError.message}`);
37
+ this.name = "WorkflowValidationError";
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Error thrown when query input or output validation fails
43
+ */
44
+ export class QueryValidationError extends TypedClientError {
45
+ constructor(
46
+ public readonly queryName: string,
47
+ public readonly phase: "input" | "output",
48
+ public readonly zodError: z.ZodError,
49
+ ) {
50
+ super(`Validation failed for query "${queryName}" ${phase}: ${zodError.message}`);
51
+ this.name = "QueryValidationError";
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Error thrown when signal input validation fails
57
+ */
58
+ export class SignalValidationError extends TypedClientError {
59
+ constructor(
60
+ public readonly signalName: string,
61
+ public readonly zodError: z.ZodError,
62
+ ) {
63
+ super(`Validation failed for signal "${signalName}" input: ${zodError.message}`);
64
+ this.name = "SignalValidationError";
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Error thrown when update input or output validation fails
70
+ */
71
+ export class UpdateValidationError extends TypedClientError {
72
+ constructor(
73
+ public readonly updateName: string,
74
+ public readonly phase: "input" | "output",
75
+ public readonly zodError: z.ZodError,
76
+ ) {
77
+ super(`Validation failed for update "${updateName}" ${phase}: ${zodError.message}`);
78
+ this.name = "UpdateValidationError";
79
+ }
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { TypedClient, type TypedWorkflowHandle, type TypedWorkflowStartOptions } from "./client.js";
2
+ export {
3
+ TypedClientError,
4
+ WorkflowNotFoundError,
5
+ WorkflowValidationError,
6
+ QueryValidationError,
7
+ SignalValidationError,
8
+ UpdateValidationError,
9
+ } from "./errors.js";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@temporal-contract/tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"],
9
+ }