@temporal-contract/worker 0.0.2 → 0.0.3

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