@temporal-contract/worker 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/handler.ts ADDED
@@ -0,0 +1,621 @@
1
+ import {
2
+ ActivityOptions,
3
+ WorkflowInfo,
4
+ defineQuery,
5
+ defineSignal,
6
+ defineUpdate,
7
+ proxyActivities,
8
+ setHandler,
9
+ workflowInfo,
10
+ } from "@temporalio/workflow";
11
+ import { ZodError } from "zod";
12
+ import type {
13
+ ActivityDefinition,
14
+ ContractDefinition,
15
+ QueryDefinition,
16
+ SignalDefinition,
17
+ UpdateDefinition,
18
+ WorkerInferInput,
19
+ WorkerInferOutput,
20
+ WorkerInferWorkflowContextActivities,
21
+ WorkflowDefinition,
22
+ } from "@temporal-contract/contract";
23
+ import {
24
+ ActivityImplementationNotFoundError,
25
+ ActivityDefinitionNotFoundError,
26
+ ActivityInputValidationError,
27
+ ActivityOutputValidationError,
28
+ WorkflowInputValidationError,
29
+ WorkflowOutputValidationError,
30
+ SignalInputValidationError,
31
+ QueryInputValidationError,
32
+ QueryOutputValidationError,
33
+ UpdateInputValidationError,
34
+ UpdateOutputValidationError,
35
+ } from "./errors.js";
36
+
37
+ /**
38
+ * Workflow context with typed activities (workflow + global) and workflow info
39
+ * Note: activities is typed as 'any' to work around TypeScript generic type inference limitations with Zod tuples
40
+ */
41
+ export interface WorkflowContext<
42
+ TContract extends ContractDefinition,
43
+ TWorkflowName extends keyof TContract["workflows"],
44
+ > {
45
+ activities: WorkerInferWorkflowContextActivities<TContract, TWorkflowName>;
46
+ info: WorkflowInfo;
47
+ }
48
+
49
+ /**
50
+ * Workflow implementation function (receives context + typed args as tuple)
51
+ * Note: We use 'any' for args to work around TypeScript limitations with generic Zod tuple inference
52
+ * The actual type will be enforced at runtime by Zod validation
53
+ */
54
+ export type WorkflowImplementation<
55
+ TContract extends ContractDefinition,
56
+ TWorkflowName extends keyof TContract["workflows"],
57
+ > = (
58
+ context: WorkflowContext<TContract, TWorkflowName>,
59
+ args: WorkerInferInput<TContract["workflows"][TWorkflowName]>,
60
+ ) => Promise<WorkerInferOutput<TContract["workflows"][TWorkflowName]>>;
61
+
62
+ /**
63
+ * Raw activity implementation function (receives typed args as tuple)
64
+ * Note: We use 'any' for args/return to work around TypeScript limitations with generic Zod tuple inference
65
+ * The actual types will be enforced at runtime by Zod validation
66
+ */
67
+ export type RawActivityImplementation<TActivity extends ActivityDefinition> = (
68
+ args: WorkerInferInput<TActivity>,
69
+ ) => Promise<WorkerInferOutput<TActivity>>;
70
+
71
+ /**
72
+ * Signal handler implementation
73
+ */
74
+ export type SignalHandlerImplementation<TSignal extends SignalDefinition> = (
75
+ args: WorkerInferInput<TSignal>,
76
+ ) => void | Promise<void>;
77
+
78
+ /**
79
+ * Query handler implementation
80
+ */
81
+ export type QueryHandlerImplementation<TQuery extends QueryDefinition> = (
82
+ args: WorkerInferInput<TQuery>,
83
+ ) => WorkerInferOutput<TQuery>;
84
+
85
+ /**
86
+ * Update handler implementation
87
+ */
88
+ export type UpdateHandlerImplementation<TUpdate extends UpdateDefinition> = (
89
+ args: WorkerInferInput<TUpdate>,
90
+ ) => Promise<WorkerInferOutput<TUpdate>>;
91
+
92
+ /**
93
+ * Map of all activity implementations for a contract (global + all workflow-specific)
94
+ */
95
+ export type ActivityImplementations<T extends ContractDefinition> =
96
+ // Global activities
97
+ (T["activities"] extends Record<string, ActivityDefinition>
98
+ ? {
99
+ [K in keyof T["activities"]]: RawActivityImplementation<T["activities"][K]>;
100
+ }
101
+ : {}) &
102
+ // All workflow-specific activities merged
103
+ UnionToIntersection<
104
+ {
105
+ [K in keyof T["workflows"]]: T["workflows"][K]["activities"] extends Record<
106
+ string,
107
+ ActivityDefinition
108
+ >
109
+ ? {
110
+ [A in keyof T["workflows"][K]["activities"]]: RawActivityImplementation<
111
+ T["workflows"][K]["activities"][A]
112
+ >;
113
+ }
114
+ : {};
115
+ }[keyof T["workflows"]]
116
+ >;
117
+
118
+ /**
119
+ * Utility type to convert union to intersection
120
+ */
121
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
122
+ k: infer I,
123
+ ) => void
124
+ ? I
125
+ : never;
126
+
127
+ /**
128
+ * Options for creating activities handler
129
+ */
130
+ export interface DeclareActivitiesHandlerOptions<T extends ContractDefinition> {
131
+ contract: T;
132
+ activities: ActivityImplementations<T>;
133
+ }
134
+
135
+ /**
136
+ * Activities handler ready for Temporal Worker
137
+ */
138
+ export interface ActivitiesHandler<T extends ContractDefinition> {
139
+ contract: T;
140
+ activities: Record<string, (...args: unknown[]) => Promise<unknown>>;
141
+ }
142
+
143
+ /**
144
+ * Options for declaring a workflow implementation
145
+ */
146
+ export interface DeclareWorkflowOptions<
147
+ TContract extends ContractDefinition,
148
+ TWorkflowName extends keyof TContract["workflows"],
149
+ > {
150
+ workflowName: TWorkflowName;
151
+ contract: TContract;
152
+ implementation: WorkflowImplementation<TContract, TWorkflowName>;
153
+ /**
154
+ * Default activity options applied to all activities in this workflow.
155
+ * These will be merged with the default startToCloseTimeout of 60 seconds.
156
+ * For more control, you can override specific Temporal ActivityOptions like:
157
+ * - startToCloseTimeout: Maximum time for activity execution
158
+ * - scheduleToCloseTimeout: End-to-end timeout including queuing
159
+ * - scheduleToStartTimeout: Maximum time activity can wait in queue
160
+ * - heartbeatTimeout: Time between heartbeats before considering activity dead
161
+ * - retry: Retry policy for failed activities
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * activityOptions: {
166
+ * startToCloseTimeout: '5m',
167
+ * retry: { maximumAttempts: 3 }
168
+ * }
169
+ * ```
170
+ */
171
+ activityOptions?: ActivityOptions;
172
+ /**
173
+ * Signal handlers (if defined in workflow)
174
+ */
175
+ signals?: TContract["workflows"][TWorkflowName]["signals"] extends Record<
176
+ string,
177
+ SignalDefinition
178
+ >
179
+ ? {
180
+ [K in keyof TContract["workflows"][TWorkflowName]["signals"]]: SignalHandlerImplementation<
181
+ TContract["workflows"][TWorkflowName]["signals"][K]
182
+ >;
183
+ }
184
+ : never;
185
+ /**
186
+ * Query handlers (if defined in workflow)
187
+ */
188
+ queries?: TContract["workflows"][TWorkflowName]["queries"] extends Record<string, QueryDefinition>
189
+ ? {
190
+ [K in keyof TContract["workflows"][TWorkflowName]["queries"]]: QueryHandlerImplementation<
191
+ TContract["workflows"][TWorkflowName]["queries"][K]
192
+ >;
193
+ }
194
+ : never;
195
+ /**
196
+ * Update handlers (if defined in workflow)
197
+ */
198
+ updates?: TContract["workflows"][TWorkflowName]["updates"] extends Record<
199
+ string,
200
+ UpdateDefinition
201
+ >
202
+ ? {
203
+ [K in keyof TContract["workflows"][TWorkflowName]["updates"]]: UpdateHandlerImplementation<
204
+ TContract["workflows"][TWorkflowName]["updates"][K]
205
+ >;
206
+ }
207
+ : never;
208
+ }
209
+
210
+ /**
211
+ * Create a validated activities proxy that parses inputs and outputs
212
+ *
213
+ * This wrapper ensures data integrity across the network boundary between
214
+ * workflow and activity execution.
215
+ */
216
+ function createValidatedActivities<
217
+ TContract extends ContractDefinition,
218
+ TWorkflowName extends keyof TContract["workflows"],
219
+ >(
220
+ rawActivities: Record<string, (...args: unknown[]) => Promise<unknown>>,
221
+ workflowActivitiesDefinition: Record<string, ActivityDefinition> | undefined,
222
+ contractActivitiesDefinition: Record<string, ActivityDefinition> | undefined,
223
+ ): WorkerInferWorkflowContextActivities<TContract, TWorkflowName> {
224
+ const validatedActivities = {} as WorkerInferWorkflowContextActivities<TContract, TWorkflowName>;
225
+
226
+ // Merge workflow activities and global contract activities
227
+ const allActivitiesDefinition = {
228
+ ...contractActivitiesDefinition,
229
+ ...workflowActivitiesDefinition, // Workflow activities override global ones
230
+ };
231
+
232
+ for (const [activityName, activityDef] of Object.entries(allActivitiesDefinition)) {
233
+ const rawActivity = rawActivities[activityName];
234
+
235
+ if (!rawActivity) {
236
+ throw new ActivityImplementationNotFoundError(activityName, Object.keys(rawActivities));
237
+ }
238
+
239
+ // @ts-expect-error fixme later
240
+ validatedActivities[activityName] = async (input: unknown) => {
241
+ // Validate input before sending over network
242
+ let validatedInput: unknown;
243
+ try {
244
+ validatedInput = activityDef.input.parse(input);
245
+ } catch (error) {
246
+ if (error instanceof ZodError) {
247
+ throw new ActivityInputValidationError(activityName, error);
248
+ }
249
+ throw error;
250
+ }
251
+
252
+ // Call the actual activity (pass the single parameter directly)
253
+ const result = await rawActivity(validatedInput);
254
+
255
+ // Validate output after receiving from network
256
+ try {
257
+ return activityDef.output.parse(result);
258
+ } catch (error) {
259
+ if (error instanceof ZodError) {
260
+ throw new ActivityOutputValidationError(activityName, error);
261
+ }
262
+ throw error;
263
+ }
264
+ };
265
+ }
266
+
267
+ return validatedActivities;
268
+ }
269
+
270
+ /**
271
+ * Create a typed activities handler with automatic validation
272
+ *
273
+ * This wraps all activity implementations with Zod validation at network boundaries.
274
+ * TypeScript ensures ALL activities (global + workflow-specific) are implemented.
275
+ *
276
+ * Use this to create the activities object for the Temporal Worker.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * import { declareActivitiesHandler } from '@temporal-contract/worker';
281
+ * import myContract from './contract';
282
+ *
283
+ * export const activitiesHandler = declareActivitiesHandler({
284
+ * contract: myContract,
285
+ * activities: {
286
+ * // Global activities
287
+ * sendEmail: async (to, subject, body) => {
288
+ * await emailService.send({ to, subject, body });
289
+ * return { sent: true };
290
+ * },
291
+ * // Workflow-specific activities
292
+ * validateInventory: async (orderId) => {
293
+ * const available = await inventory.check(orderId);
294
+ * return { available };
295
+ * },
296
+ * },
297
+ * });
298
+ *
299
+ * // Use with Temporal Worker
300
+ * import { Worker } from '@temporalio/worker';
301
+ *
302
+ * const worker = await Worker.create({
303
+ * workflowsPath: require.resolve('./workflows'),
304
+ * activities: activitiesHandler.activities,
305
+ * taskQueue: activitiesHandler.contract.taskQueue,
306
+ * });
307
+ * ```
308
+ */
309
+ export function declareActivitiesHandler<T extends ContractDefinition>(
310
+ options: DeclareActivitiesHandlerOptions<T>,
311
+ ): ActivitiesHandler<T> {
312
+ const { contract, activities } = options;
313
+
314
+ // Wrap activities with validation
315
+ const wrappedActivities: Record<string, (...args: unknown[]) => Promise<unknown>> = {};
316
+
317
+ // Collect all available activity definitions
318
+ const allDefinitions: string[] = [];
319
+ if (contract.activities) {
320
+ allDefinitions.push(...Object.keys(contract.activities));
321
+ }
322
+ for (const workflow of Object.values(contract.workflows) as WorkflowDefinition[]) {
323
+ if (workflow.activities) {
324
+ allDefinitions.push(...Object.keys(workflow.activities));
325
+ }
326
+ }
327
+
328
+ for (const [activityName, activityImpl] of Object.entries(activities)) {
329
+ // Find activity definition (global or workflow-specific)
330
+ let activityDef: ActivityDefinition | undefined;
331
+
332
+ // Check global activities
333
+ if (contract.activities?.[activityName]) {
334
+ activityDef = contract.activities[activityName];
335
+ } else {
336
+ // Check workflow-specific activities
337
+ for (const workflow of Object.values(contract.workflows) as WorkflowDefinition[]) {
338
+ if (workflow.activities?.[activityName]) {
339
+ activityDef = workflow.activities[activityName];
340
+ break;
341
+ }
342
+ }
343
+ }
344
+
345
+ if (!activityDef) {
346
+ throw new ActivityDefinitionNotFoundError(activityName, allDefinitions);
347
+ }
348
+
349
+ wrappedActivities[activityName] = async (input: unknown) => {
350
+ // Validate input
351
+ let validatedInput: unknown;
352
+ try {
353
+ validatedInput = activityDef.input.parse(input);
354
+ } catch (error) {
355
+ if (error instanceof ZodError) {
356
+ throw new ActivityInputValidationError(activityName, error);
357
+ }
358
+ throw error;
359
+ }
360
+
361
+ // Execute activity
362
+ const result = await activityImpl(validatedInput);
363
+
364
+ // Validate output
365
+ try {
366
+ return activityDef.output.parse(result);
367
+ } catch (error) {
368
+ if (error instanceof ZodError) {
369
+ throw new ActivityOutputValidationError(activityName, error);
370
+ }
371
+ throw error;
372
+ }
373
+ };
374
+ }
375
+
376
+ return {
377
+ contract,
378
+ activities: wrappedActivities,
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Create a typed workflow implementation with automatic validation
384
+ *
385
+ * This wraps a workflow implementation with:
386
+ * - Input/output validation
387
+ * - Typed workflow context with activities
388
+ * - Workflow info access
389
+ *
390
+ * Workflows must be defined in separate files and imported by the Temporal Worker
391
+ * via workflowsPath.
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * // workflows/processOrder.ts
396
+ * import { declareWorkflow } from '@temporal-contract/worker';
397
+ * import myContract from '../contract';
398
+ *
399
+ * export const processOrder = declareWorkflow({
400
+ * workflowName: 'processOrder',
401
+ * contract: myContract,
402
+ * implementation: async (context, orderId, customerId) => {
403
+ * // context.activities: typed activities (workflow + global)
404
+ * // context.info: WorkflowInfo
405
+ *
406
+ * const inventory = await context.activities.validateInventory(orderId);
407
+ *
408
+ * if (!inventory.available) {
409
+ * throw new Error('Out of stock');
410
+ * }
411
+ *
412
+ * const payment = await context.activities.chargePayment(customerId, 100);
413
+ *
414
+ * // Global activity
415
+ * await context.activities.sendEmail(
416
+ * customerId,
417
+ * 'Order processed',
418
+ * 'Your order has been processed'
419
+ * );
420
+ *
421
+ * return {
422
+ * orderId,
423
+ * status: payment.success ? 'success' : 'failed',
424
+ * transactionId: payment.transactionId,
425
+ * };
426
+ * },
427
+ * activityOptions: {
428
+ * startToCloseTimeout: '1 minute',
429
+ * },
430
+ * });
431
+ * ```
432
+ *
433
+ * Then in your worker setup:
434
+ * ```ts
435
+ * // worker.ts
436
+ * import { Worker } from '@temporalio/worker';
437
+ * import { activitiesHandler } from './activities';
438
+ *
439
+ * const worker = await Worker.create({
440
+ * workflowsPath: require.resolve('./workflows'), // Imports processOrder
441
+ * activities: activitiesHandler.activities,
442
+ * taskQueue: activitiesHandler.contract.taskQueue,
443
+ * });
444
+ * ```
445
+ */
446
+ export function declareWorkflow<
447
+ TContract extends ContractDefinition,
448
+ TWorkflowName extends keyof TContract["workflows"],
449
+ >(
450
+ options: DeclareWorkflowOptions<TContract, TWorkflowName>,
451
+ ): (
452
+ args: WorkerInferInput<TContract["workflows"][TWorkflowName]>,
453
+ ) => Promise<WorkerInferOutput<TContract["workflows"][TWorkflowName]>> {
454
+ const { workflowName, contract, implementation, activityOptions, signals, queries, updates } =
455
+ options;
456
+
457
+ // Get the workflow definition from the contract
458
+ const definition = contract.workflows[
459
+ workflowName as string
460
+ ] as TContract["workflows"][TWorkflowName];
461
+
462
+ return async (args) => {
463
+ // Temporal passes args as array, extract first element which is our single parameter
464
+ const singleArg = Array.isArray(args) ? args[0] : args;
465
+
466
+ // Validate workflow input
467
+ let validatedInput: WorkerInferInput<TContract["workflows"][TWorkflowName]>;
468
+ try {
469
+ validatedInput = definition.input.parse(singleArg) as WorkerInferInput<
470
+ TContract["workflows"][TWorkflowName]
471
+ >;
472
+ } catch (error) {
473
+ if (error instanceof ZodError) {
474
+ throw new WorkflowInputValidationError(String(workflowName), error);
475
+ }
476
+ throw error;
477
+ }
478
+
479
+ // Register signal handlers
480
+ if (definition.signals && signals) {
481
+ const signalDefs = definition.signals as Record<string, SignalDefinition>;
482
+ const signalHandlers = signals as Record<string, unknown>;
483
+
484
+ for (const [signalName, signalDef] of Object.entries(signalDefs)) {
485
+ const handler = signalHandlers[signalName];
486
+ if (handler) {
487
+ const signal = defineSignal(signalName);
488
+ setHandler(signal, async (...args: unknown[]) => {
489
+ // Extract single parameter (Temporal passes as args array)
490
+ const input = args.length === 1 ? args[0] : args;
491
+ let validatedInput: unknown;
492
+ try {
493
+ validatedInput = signalDef.input.parse(input);
494
+ } catch (error) {
495
+ if (error instanceof ZodError) {
496
+ throw new SignalInputValidationError(signalName, error);
497
+ }
498
+ throw error;
499
+ }
500
+ await (handler as SignalHandlerImplementation<SignalDefinition>)(validatedInput);
501
+ });
502
+ }
503
+ }
504
+ }
505
+
506
+ // Register query handlers
507
+ if (definition.queries && queries) {
508
+ const queryDefs = definition.queries as Record<string, QueryDefinition>;
509
+ const queryHandlers = queries as Record<string, unknown>;
510
+
511
+ for (const [queryName, queryDef] of Object.entries(queryDefs)) {
512
+ const handler = queryHandlers[queryName];
513
+ if (handler) {
514
+ const query = defineQuery(queryName);
515
+ setHandler(query, (...args: unknown[]) => {
516
+ // Extract single parameter (Temporal passes as args array)
517
+ const input = args.length === 1 ? args[0] : args;
518
+ let validatedInput: unknown;
519
+ try {
520
+ validatedInput = queryDef.input.parse(input);
521
+ } catch (error) {
522
+ if (error instanceof ZodError) {
523
+ throw new QueryInputValidationError(queryName, error);
524
+ }
525
+ throw error;
526
+ }
527
+ const result = (handler as QueryHandlerImplementation<QueryDefinition>)(validatedInput);
528
+ try {
529
+ return queryDef.output.parse(result);
530
+ } catch (error) {
531
+ if (error instanceof ZodError) {
532
+ throw new QueryOutputValidationError(queryName, error);
533
+ }
534
+ throw error;
535
+ }
536
+ });
537
+ }
538
+ }
539
+ }
540
+
541
+ // Register update handlers
542
+ if (definition.updates && updates) {
543
+ const updateDefs = definition.updates as Record<string, UpdateDefinition>;
544
+ const updateHandlers = updates as Record<string, unknown>;
545
+
546
+ for (const [updateName, updateDef] of Object.entries(updateDefs)) {
547
+ const handler = updateHandlers[updateName];
548
+ if (handler) {
549
+ const update = defineUpdate(updateName);
550
+ setHandler(update, async (...args: unknown[]) => {
551
+ // Extract single parameter (Temporal passes as args array)
552
+ const input = args.length === 1 ? args[0] : args;
553
+ let validatedInput: unknown;
554
+ try {
555
+ validatedInput = updateDef.input.parse(input);
556
+ } catch (error) {
557
+ if (error instanceof ZodError) {
558
+ throw new UpdateInputValidationError(updateName, error);
559
+ }
560
+ throw error;
561
+ }
562
+ const result = await (handler as UpdateHandlerImplementation<UpdateDefinition>)(
563
+ validatedInput,
564
+ );
565
+ try {
566
+ return updateDef.output.parse(result);
567
+ } catch (error) {
568
+ if (error instanceof ZodError) {
569
+ throw new UpdateOutputValidationError(updateName, error);
570
+ }
571
+ throw error;
572
+ }
573
+ });
574
+ }
575
+ }
576
+ }
577
+
578
+ // Create activities proxy if activities are defined
579
+ let contextActivities: unknown = {};
580
+
581
+ if (definition.activities || contract.activities) {
582
+ const rawActivities = proxyActivities<
583
+ Record<string, (...args: unknown[]) => Promise<unknown>>
584
+ >({
585
+ // Default to 1 minute if no timeout specified
586
+ startToCloseTimeout: activityOptions?.startToCloseTimeout ?? 60_000,
587
+ ...activityOptions,
588
+ });
589
+
590
+ contextActivities = createValidatedActivities(
591
+ rawActivities,
592
+ definition.activities,
593
+ contract.activities,
594
+ );
595
+ }
596
+
597
+ // Create workflow context
598
+ const context: WorkflowContext<TContract, TWorkflowName> = {
599
+ activities: contextActivities as WorkerInferWorkflowContextActivities<
600
+ TContract,
601
+ TWorkflowName
602
+ >,
603
+ info: workflowInfo(),
604
+ };
605
+
606
+ // Execute workflow (pass validated input as tuple)
607
+ const result = await implementation(context, validatedInput);
608
+
609
+ // Validate workflow output
610
+ try {
611
+ return definition.output.parse(result) as WorkerInferOutput<
612
+ TContract["workflows"][TWorkflowName]
613
+ >;
614
+ } catch (error) {
615
+ if (error instanceof ZodError) {
616
+ throw new WorkflowOutputValidationError(String(workflowName), error);
617
+ }
618
+ throw error;
619
+ }
620
+ };
621
+ }
@@ -0,0 +1,20 @@
1
+ // Entry point for workflows
2
+ export { declareWorkflow } from "./handler.js";
3
+ export type {
4
+ WorkflowContext,
5
+ WorkflowImplementation,
6
+ SignalHandlerImplementation,
7
+ QueryHandlerImplementation,
8
+ UpdateHandlerImplementation,
9
+ DeclareWorkflowOptions,
10
+ } from "./handler.js";
11
+ export {
12
+ WorkerError,
13
+ WorkflowInputValidationError,
14
+ WorkflowOutputValidationError,
15
+ SignalInputValidationError,
16
+ QueryInputValidationError,
17
+ QueryOutputValidationError,
18
+ UpdateInputValidationError,
19
+ UpdateOutputValidationError,
20
+ } 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
+ }