@temporalio/common 1.8.0 → 1.8.2

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/failure.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { temporal } from '@temporalio/proto';
2
- import { checkExtends, isRecord } from './type-helpers';
2
+ import { checkExtends, errorMessage, isRecord, SymbolBasedInstanceOfError } from './type-helpers';
3
3
 
4
4
  export const FAILURE_SOURCE = 'TypeScriptSDK';
5
5
  export type ProtoFailure = temporal.api.failure.v1.IFailure;
@@ -35,8 +35,6 @@ checkExtends<RetryState, temporal.api.enums.v1.RetryState>();
35
35
 
36
36
  export type WorkflowExecution = temporal.api.common.v1.IWorkflowExecution;
37
37
 
38
- const isTemporalFailure = Symbol.for('__temporal_isTemporalFailure');
39
-
40
38
  /**
41
39
  * Represents failures that can cross Workflow and Activity boundaries.
42
40
  *
@@ -44,8 +42,8 @@ const isTemporalFailure = Symbol.for('__temporal_isTemporalFailure');
44
42
  *
45
43
  * The only child class you should ever throw from your code is {@link ApplicationFailure}.
46
44
  */
45
+ @SymbolBasedInstanceOfError('TemporalFailure')
47
46
  export class TemporalFailure extends Error {
48
- public readonly name: string = 'TemporalFailure';
49
47
  /**
50
48
  * The original failure that constructed this error.
51
49
  *
@@ -56,45 +54,16 @@ export class TemporalFailure extends Error {
56
54
  constructor(message?: string | undefined | null, public readonly cause?: Error) {
57
55
  super(message ?? undefined);
58
56
  }
59
-
60
- /**
61
- * Marker to determine whether an error is an instance of TemporalFailure.
62
- */
63
- protected readonly [isTemporalFailure] = true;
64
-
65
- /**
66
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
67
- */
68
- static is(error: unknown): error is TemporalFailure {
69
- return error instanceof TemporalFailure || (error instanceof Error && (error as any)[isTemporalFailure]);
70
- }
71
57
  }
72
58
 
73
- const isServerFailure = Symbol.for('__temporal_isServerFailure');
74
-
75
59
  /** Exceptions originated at the Temporal service. */
60
+ @SymbolBasedInstanceOfError('ServerFailure')
76
61
  export class ServerFailure extends TemporalFailure {
77
- public readonly name: string = 'ServerFailure';
78
-
79
62
  constructor(message: string | undefined, public readonly nonRetryable: boolean, cause?: Error) {
80
63
  super(message, cause);
81
64
  }
82
-
83
- /**
84
- * Marker to determine whether an error is an instance of ServerFailure.
85
- */
86
- protected readonly [isServerFailure] = true;
87
-
88
- /**
89
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
90
- */
91
- static is(error: unknown): error is ServerFailure {
92
- return error instanceof ServerFailure || (error instanceof Error && (error as any)[isServerFailure]);
93
- }
94
65
  }
95
66
 
96
- const isApplicationFailure = Symbol.for('__temporal_isApplicationFailure');
97
-
98
67
  /**
99
68
  * `ApplicationFailure`s are used to communicate application-specific failures in Workflows and Activities.
100
69
  *
@@ -117,9 +86,8 @@ const isApplicationFailure = Symbol.for('__temporal_isApplicationFailure');
117
86
  * `ApplicationFailure` from the last Activity Task will be the `cause` of the {@link ActivityFailure} thrown in the
118
87
  * Workflow.
119
88
  */
89
+ @SymbolBasedInstanceOfError('ApplicationFailure')
120
90
  export class ApplicationFailure extends TemporalFailure {
121
- public readonly name: string = 'ApplicationFailure';
122
-
123
91
  /**
124
92
  * Alternatively, use {@link fromError} or {@link create}.
125
93
  */
@@ -133,18 +101,6 @@ export class ApplicationFailure extends TemporalFailure {
133
101
  super(message, cause);
134
102
  }
135
103
 
136
- /**
137
- * Marker to determine whether an error is an instance of ApplicationFailure.
138
- */
139
- protected readonly [isApplicationFailure] = true;
140
-
141
- /**
142
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
143
- */
144
- static is(error: unknown): error is ApplicationFailure {
145
- return error instanceof ApplicationFailure || (error instanceof Error && (error as any)[isApplicationFailure]);
146
- }
147
-
148
104
  /**
149
105
  * Create a new `ApplicationFailure` from an Error object.
150
106
  *
@@ -223,8 +179,6 @@ export interface ApplicationFailureOptions {
223
179
  cause?: Error;
224
180
  }
225
181
 
226
- const isCancelledFailure = Symbol.for('__temporal_isCancelledFailure');
227
-
228
182
  /**
229
183
  * This error is thrown when Cancellation has been requested. To allow Cancellation to happen, let it propagate. To
230
184
  * ignore Cancellation, catch it and continue executing. Note that Cancellation can only be requested a single time, so
@@ -232,59 +186,28 @@ const isCancelledFailure = Symbol.for('__temporal_isCancelledFailure');
232
186
  *
233
187
  * When a Workflow or Activity has been successfully cancelled, a `CancelledFailure` will be the `cause`.
234
188
  */
189
+ @SymbolBasedInstanceOfError('CancelledFailure')
235
190
  export class CancelledFailure extends TemporalFailure {
236
- public readonly name: string = 'CancelledFailure';
237
-
238
191
  constructor(message: string | undefined, public readonly details: unknown[] = [], cause?: Error) {
239
192
  super(message, cause);
240
193
  }
241
-
242
- /**
243
- * Marker to determine whether an error is an instance of CancelledFailure.
244
- */
245
- protected readonly [isCancelledFailure] = true;
246
-
247
- /**
248
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
249
- */
250
- static is(error: unknown): error is CancelledFailure {
251
- return error instanceof CancelledFailure || (error instanceof Error && (error as any)[isCancelledFailure]);
252
- }
253
194
  }
254
195
 
255
- const isTerminatedFailure = Symbol.for('__temporal_isTerminatedFailure');
256
-
257
196
  /**
258
197
  * Used as the `cause` when a Workflow has been terminated
259
198
  */
199
+ @SymbolBasedInstanceOfError('TerminatedFailure')
260
200
  export class TerminatedFailure extends TemporalFailure {
261
- public readonly name: string = 'TerminatedFailure';
262
-
263
201
  constructor(message: string | undefined, cause?: Error) {
264
202
  super(message, cause);
265
203
  }
266
-
267
- /**
268
- * Marker to determine whether an error is an instance of TerminatedFailure.
269
- */
270
- protected readonly [isTerminatedFailure] = true;
271
-
272
- /**
273
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
274
- */
275
- static is(error: unknown): error is TerminatedFailure {
276
- return error instanceof TerminatedFailure || (error instanceof Error && (error as any)[isTerminatedFailure]);
277
- }
278
204
  }
279
205
 
280
- const isTimeoutFailure = Symbol.for('__temporal_isTimeoutFailure');
281
-
282
206
  /**
283
207
  * Used to represent timeouts of Activities and Workflows
284
208
  */
209
+ @SymbolBasedInstanceOfError('TimeoutFailure')
285
210
  export class TimeoutFailure extends TemporalFailure {
286
- public readonly name: string = 'TimeoutFailure';
287
-
288
211
  constructor(
289
212
  message: string | undefined,
290
213
  public readonly lastHeartbeatDetails: unknown,
@@ -292,31 +215,16 @@ export class TimeoutFailure extends TemporalFailure {
292
215
  ) {
293
216
  super(message);
294
217
  }
295
-
296
- /**
297
- * Marker to determine whether an error is an instance of TimeoutFailure.
298
- */
299
- protected readonly [isTimeoutFailure] = true;
300
-
301
- /**
302
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
303
- */
304
- static is(error: unknown): error is TimeoutFailure {
305
- return error instanceof TimeoutFailure || (error instanceof Error && (error as any)[isTimeoutFailure]);
306
- }
307
218
  }
308
219
 
309
- const isActivityFailure = Symbol.for('__temporal_isActivityFailure');
310
-
311
220
  /**
312
221
  * Contains information about an Activity failure. Always contains the original reason for the failure as its `cause`.
313
222
  * For example, if an Activity timed out, the cause will be a {@link TimeoutFailure}.
314
223
  *
315
224
  * This exception is expected to be thrown only by the framework code.
316
225
  */
226
+ @SymbolBasedInstanceOfError('ActivityFailure')
317
227
  export class ActivityFailure extends TemporalFailure {
318
- public readonly name: string = 'ActivityFailure';
319
-
320
228
  public constructor(
321
229
  message: string | undefined,
322
230
  public readonly activityType: string,
@@ -327,31 +235,16 @@ export class ActivityFailure extends TemporalFailure {
327
235
  ) {
328
236
  super(message, cause);
329
237
  }
330
-
331
- /**
332
- * Marker to determine whether an error is an instance of ActivityFailure.
333
- */
334
- protected readonly [isActivityFailure] = true;
335
-
336
- /**
337
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
338
- */
339
- static is(error: unknown): error is ActivityFailure {
340
- return error instanceof ActivityFailure || (error instanceof Error && (error as any)[isActivityFailure]);
341
- }
342
238
  }
343
239
 
344
- const isChildWorkflowFailure = Symbol.for('__temporal_isChildWorkflowFailure');
345
-
346
240
  /**
347
241
  * Contains information about a Child Workflow failure. Always contains the reason for the failure as its {@link cause}.
348
242
  * For example, if the Child was Terminated, the `cause` is a {@link TerminatedFailure}.
349
243
  *
350
244
  * This exception is expected to be thrown only by the framework code.
351
245
  */
246
+ @SymbolBasedInstanceOfError('ChildWorkflowFailure')
352
247
  export class ChildWorkflowFailure extends TemporalFailure {
353
- public readonly name: string = 'ChildWorkflowFailure';
354
-
355
248
  public constructor(
356
249
  public readonly namespace: string | undefined,
357
250
  public readonly execution: WorkflowExecution,
@@ -361,18 +254,6 @@ export class ChildWorkflowFailure extends TemporalFailure {
361
254
  ) {
362
255
  super('Child Workflow execution failed', cause);
363
256
  }
364
-
365
- /**
366
- * Marker to determine whether an error is an instance of ChildWorkflowFailure.
367
- */
368
- protected readonly [isChildWorkflowFailure] = true;
369
-
370
- /**
371
- * Instanceof check that works when multiple versions of @temporalio/common are installed.
372
- */
373
- static is(error: unknown): error is ChildWorkflowFailure {
374
- return error instanceof ChildWorkflowFailure || (error instanceof Error && (error as any)[isChildWorkflowFailure]);
375
- }
376
257
  }
377
258
 
378
259
  /**
@@ -385,7 +266,7 @@ export class ChildWorkflowFailure extends TemporalFailure {
385
266
  * - `stack`: `error.stack` or `''`
386
267
  */
387
268
  export function ensureApplicationFailure(error: unknown): ApplicationFailure {
388
- if (ApplicationFailure.is(error)) {
269
+ if (error instanceof ApplicationFailure) {
389
270
  return error;
390
271
  }
391
272
 
@@ -404,7 +285,7 @@ export function ensureApplicationFailure(error: unknown): ApplicationFailure {
404
285
  * Otherwise returns an `ApplicationFailure` with `String(err)` as the message.
405
286
  */
406
287
  export function ensureTemporalFailure(err: unknown): TemporalFailure {
407
- if (TemporalFailure.is(err)) {
288
+ if (err instanceof TemporalFailure) {
408
289
  return err;
409
290
  }
410
291
  return ensureApplicationFailure(err);
@@ -417,14 +298,8 @@ export function ensureTemporalFailure(err: unknown): TemporalFailure {
417
298
  * Otherwise, return `error.message`.
418
299
  */
419
300
  export function rootCause(error: unknown): string | undefined {
420
- if (TemporalFailure.is(error)) {
301
+ if (error instanceof TemporalFailure) {
421
302
  return error.cause ? rootCause(error.cause) : error.message;
422
303
  }
423
- if (error instanceof Error) {
424
- return error.message;
425
- }
426
- if (typeof error === 'string') {
427
- return error;
428
- }
429
- return undefined;
304
+ return errorMessage(error);
430
305
  }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export * from './retry-policy';
23
23
  export { type Timestamp, Duration, StringValue } from './time';
24
24
  export * from './workflow-handle';
25
25
  export * from './workflow-options';
26
+ export * from './versioning-intent';
26
27
 
27
28
  /**
28
29
  * Encode a UTF-8 string into a Uint8Array
@@ -21,7 +21,6 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
21
21
  return typeof value === 'object' && value !== null;
22
22
  }
23
23
 
24
- // ts-prune-ignore-next
25
24
  export function hasOwnProperty<X extends Record<string, unknown>, Y extends PropertyKey>(
26
25
  record: X,
27
26
  prop: Y
@@ -36,15 +35,27 @@ export function hasOwnProperties<X extends Record<string, unknown>, Y extends Pr
36
35
  return props.every((prop) => prop in record);
37
36
  }
38
37
 
38
+ export function isError(error: unknown): error is Error {
39
+ return (
40
+ isRecord(error) &&
41
+ typeof error.name === 'string' &&
42
+ typeof error.message === 'string' &&
43
+ (error.stack == null || typeof error.stack === 'string')
44
+ );
45
+ }
46
+
47
+ export function isAbortError(error: unknown): error is Error & { name: 'AbortError' } {
48
+ return isError(error) && error.name === 'AbortError';
49
+ }
50
+
39
51
  /**
40
52
  * Get `error.message` (or `undefined` if not present)
41
53
  */
42
54
  export function errorMessage(error: unknown): string | undefined {
43
- if (typeof error === 'string') {
44
- return error;
45
- }
46
- if (error instanceof Error) {
55
+ if (isError(error)) {
47
56
  return error.message;
57
+ } else if (typeof error === 'string') {
58
+ return error;
48
59
  }
49
60
  return undefined;
50
61
  }
@@ -52,16 +63,17 @@ export function errorMessage(error: unknown): string | undefined {
52
63
  interface ErrorWithCode {
53
64
  code: string;
54
65
  }
66
+
67
+ function isErrorWithCode(error: unknown): error is ErrorWithCode {
68
+ return isRecord(error) && typeof error.code === 'string';
69
+ }
70
+
55
71
  /**
56
72
  * Get `error.code` (or `undefined` if not present)
57
73
  */
58
74
  export function errorCode(error: unknown): string | undefined {
59
- if (
60
- typeof error === 'object' &&
61
- (error as ErrorWithCode).code !== undefined &&
62
- typeof (error as ErrorWithCode).code === 'string'
63
- ) {
64
- return (error as ErrorWithCode).code;
75
+ if (isErrorWithCode(error)) {
76
+ return error.code;
65
77
  }
66
78
 
67
79
  return undefined;
@@ -73,3 +85,58 @@ export function errorCode(error: unknown): string | undefined {
73
85
  export function assertNever(msg: string, x: never): never {
74
86
  throw new TypeError(msg + ': ' + x);
75
87
  }
88
+
89
+ export type Class<E extends Error> = {
90
+ new (...args: any[]): E;
91
+ prototype: E;
92
+ };
93
+
94
+ /**
95
+ * A decorator to be used on error classes. It adds the 'name' property AND provides a custom
96
+ * 'instanceof' handler that works correctly across execution contexts.
97
+ *
98
+ * ### Details ###
99
+ *
100
+ * According to the EcmaScript's spec, the default behavior of JavaScript's `x instanceof Y` operator is to walk up the
101
+ * prototype chain of object 'x', checking if any constructor in that hierarchy is _exactly the same object_ as the
102
+ * constructor function 'Y'.
103
+ *
104
+ * Unfortunately, it happens in various situations that different constructor function objects get created for what
105
+ * appears to be the very same class. This leads to surprising behavior where `instanceof` returns false though it is
106
+ * known that the object is indeed an instance of that class. One particular case where this happens is when constructor
107
+ * 'Y' belongs to a different realm than the constuctor with which 'x' was instantiated. Another case is when two copies
108
+ * of the same library gets loaded in the same realm.
109
+ *
110
+ * In practice, this tends to cause issues when crossing the workflow-sandboxing boundary (since Node's vm module
111
+ * really creates new execution realms), as well as when running tests using Jest (see https://github.com/jestjs/jest/issues/2549
112
+ * for some details on that one).
113
+ *
114
+ * This function injects a custom 'instanceof' handler into the prototype of 'clazz', which is both cross-realm safe and
115
+ * cross-copies-of-the-same-lib safe. It works by adding a special symbol property to the prototype of 'clazz', and then
116
+ * checking for the presence of that symbol.
117
+ */
118
+ export function SymbolBasedInstanceOfError<E extends Error>(markerName: string): (clazz: Class<E>) => void {
119
+ return (clazz: Class<E>): void => {
120
+ const marker = Symbol.for(`__temporal_is${markerName}`);
121
+
122
+ Object.defineProperty(clazz.prototype, 'name', { value: markerName, enumerable: true });
123
+ Object.defineProperty(clazz.prototype, marker, { value: true, enumerable: false });
124
+ Object.defineProperty(clazz, Symbol.hasInstance, {
125
+ // eslint-disable-next-line object-shorthand
126
+ value: function (this: any, error: object): boolean {
127
+ if (this === clazz) {
128
+ return isRecord(error) && (error as any)[marker] === true;
129
+ } else {
130
+ // 'this' must be a _subclass_ of clazz that doesn't redefined [Symbol.hasInstance], so that it inherited
131
+ // from clazz's [Symbol.hasInstance]. If we don't handle this particular situation, then
132
+ // `x instanceof SubclassOfParent` would return true for any instance of 'Parent', which is clearly wrong.
133
+ //
134
+ // Ideally, it'd be preferable to avoid this case entirely, by making sure that all subclasses of 'clazz'
135
+ // redefine [Symbol.hasInstance], but we can't enforce that. We therefore fallback to the default instanceof
136
+ // behavior (which is NOT cross-realm safe).
137
+ return this.prototype.isPrototypeOf(error); // eslint-disable-line no-prototype-builtins
138
+ }
139
+ },
140
+ });
141
+ };
142
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Indicates whether the user intends certain commands to be run on a compatible worker Build Id
3
+ * version or not.
4
+ *
5
+ * `COMPATIBLE` indicates that the command should run on a worker with compatible version if
6
+ * possible. It may not be possible if the target task queue does not also have knowledge of the
7
+ * current worker's Build Id.
8
+ *
9
+ * `DEFAULT` indicates that the command should run on the target task queue's current
10
+ * overall-default Build Id.
11
+ *
12
+ * Where this type is accepted optionally, an unset value indicates that the SDK should choose the
13
+ * most sensible default behavior for the type of command, accounting for whether the command will
14
+ * be run on the same task queue as the current worker.
15
+ *
16
+ * @experimental
17
+ */
18
+ export type VersioningIntent = 'COMPATIBLE' | 'DEFAULT';