Skip to content

Commit 2f66281

Browse files
authored
fix: Remove most instanceof checks from SDK (#1128)
The number one reported issue stems from instanceof checks and SDK module level variables, which fail when there are multiple versions of the SDK packages installed. - Remove the instanceof checks in favor of coded `is` methods on the critical classes. - Install the activity context local storage globally. Note that the `is` methods do no use the class name, which may be lost when using minifiers.
1 parent 9edc469 commit 2f66281

File tree

15 files changed

+250
-79
lines changed

15 files changed

+250
-79
lines changed

packages/activity/src/index.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export {
8181
UntypedActivities,
8282
} from '@temporalio/common';
8383

84+
const isCompleteAsyncError = Symbol.for('__temporal_isCompleteAsyncError');
85+
8486
/**
8587
* Throw this error from an Activity in order to make the Worker forget about this Activity.
8688
*
@@ -104,10 +106,27 @@ export class CompleteAsyncError extends Error {
104106
constructor() {
105107
super();
106108
}
109+
110+
/**
111+
* Marker to determine whether an error is an instance of CompleteAsyncError.
112+
*/
113+
protected readonly [isCompleteAsyncError] = true;
114+
115+
/**
116+
* Instanceof check that works when multiple versions of @temporalio/activity are installed.
117+
*/
118+
static is(error: unknown): error is CompleteAsyncError {
119+
return error instanceof CompleteAsyncError || (error instanceof Error && (error as any)[isCompleteAsyncError]);
120+
}
121+
}
122+
123+
// Make it safe to use @temporalio/activity with multiple versions installed.
124+
const asyncLocalStorageSymbol = Symbol.for('__temporal_activity_context_storage__');
125+
if (!(globalThis as any)[asyncLocalStorageSymbol]) {
126+
(globalThis as any)[asyncLocalStorageSymbol] = new AsyncLocalStorage<Context>();
107127
}
108128

109-
/** @ignore */
110-
export const asyncLocalStorage = new AsyncLocalStorage<Context>();
129+
export const asyncLocalStorage: AsyncLocalStorage<Context> = (globalThis as any)[asyncLocalStorageSymbol];
111130

112131
/**
113132
* Holds information about the current Activity Execution. Retrieved inside an Activity with `Context.current().info`.

packages/client/src/async-completion-client.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -207,33 +207,36 @@ export class AsyncCompletionClient extends BaseClient {
207207
heartbeat(fullActivityId: FullActivityId, details?: unknown): Promise<void>;
208208

209209
async heartbeat(taskTokenOrFullActivityId: Uint8Array | FullActivityId, details?: unknown): Promise<void> {
210+
let cancelRequested = false;
210211
try {
211-
if (taskTokenOrFullActivityId instanceof Uint8Array) {
212-
const { cancelRequested } = await this.workflowService.recordActivityTaskHeartbeat({
213-
identity: this.options.identity,
214-
namespace: this.options.namespace,
215-
taskToken: taskTokenOrFullActivityId,
216-
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
217-
});
218-
if (cancelRequested) {
219-
throw new ActivityCancelledError('cancelled');
220-
}
221-
} else {
222-
const { cancelRequested } = await this.workflowService.recordActivityTaskHeartbeatById({
223-
identity: this.options.identity,
224-
namespace: this.options.namespace,
225-
...taskTokenOrFullActivityId,
226-
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
227-
});
228-
if (cancelRequested) {
229-
throw new ActivityCancelledError('cancelled');
230-
}
231-
}
212+
const response = await this._sendHeartbeat(taskTokenOrFullActivityId, details);
213+
cancelRequested = response.cancelRequested;
232214
} catch (err) {
233-
if (err instanceof ActivityCancelledError) {
234-
throw err;
235-
}
236215
this.handleError(err);
237216
}
217+
if (cancelRequested) {
218+
throw new ActivityCancelledError('cancelled');
219+
}
220+
}
221+
222+
private async _sendHeartbeat(
223+
taskTokenOrFullActivityId: Uint8Array | FullActivityId,
224+
details?: unknown
225+
): Promise<{ cancelRequested: boolean }> {
226+
if (taskTokenOrFullActivityId instanceof Uint8Array) {
227+
return await this.workflowService.recordActivityTaskHeartbeat({
228+
identity: this.options.identity,
229+
namespace: this.options.namespace,
230+
taskToken: taskTokenOrFullActivityId,
231+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
232+
});
233+
} else {
234+
return await this.workflowService.recordActivityTaskHeartbeatById({
235+
identity: this.options.identity,
236+
namespace: this.options.namespace,
237+
...taskTokenOrFullActivityId,
238+
details: { payloads: await encodeToPayloads(this.dataConverter, details) },
239+
});
240+
}
238241
}
239242
}

packages/common/src/converter/failure-converter.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ export interface FailureConverter {
5757
errorToFailure(err: unknown, payloadConverter: PayloadConverter): ProtoFailure;
5858
/**
5959
* Converts a Failure proto message to a JS Error object.
60+
*
61+
* The returned error must be an instance of `TemporalFailure`.
6062
*/
61-
failureToError(err: ProtoFailure, payloadConverter: PayloadConverter): Error;
63+
failureToError(err: ProtoFailure, payloadConverter: PayloadConverter): TemporalFailure;
6264
}
6365

6466
/**
@@ -183,7 +185,7 @@ export class DefaultFailureConverter implements FailureConverter {
183185
);
184186
}
185187

186-
failureToError(failure: ProtoFailure, payloadConverter: PayloadConverter): Error {
188+
failureToError(failure: ProtoFailure, payloadConverter: PayloadConverter): TemporalFailure {
187189
if (failure.encodedAttributes) {
188190
const attrs = payloadConverter.fromPayload<DefaultEncodedFailureAttributes>(failure.encodedAttributes);
189191
// Don't apply encodedAttributes unless they conform to an expected schema
@@ -217,7 +219,7 @@ export class DefaultFailureConverter implements FailureConverter {
217219
}
218220

219221
errorToFailureInner(err: unknown, payloadConverter: PayloadConverter): ProtoFailure {
220-
if (err instanceof TemporalFailure) {
222+
if (TemporalFailure.is(err)) {
221223
if (err.failure) return err.failure;
222224
const base = {
223225
message: err.message,
@@ -226,7 +228,7 @@ export class DefaultFailureConverter implements FailureConverter {
226228
source: FAILURE_SOURCE,
227229
};
228230

229-
if (err instanceof ActivityFailure) {
231+
if (ActivityFailure.is(err)) {
230232
return {
231233
...base,
232234
activityFailureInfo: {
@@ -235,7 +237,7 @@ export class DefaultFailureConverter implements FailureConverter {
235237
},
236238
};
237239
}
238-
if (err instanceof ChildWorkflowFailure) {
240+
if (ChildWorkflowFailure.is(err)) {
239241
return {
240242
...base,
241243
childWorkflowExecutionFailureInfo: {
@@ -245,7 +247,7 @@ export class DefaultFailureConverter implements FailureConverter {
245247
},
246248
};
247249
}
248-
if (err instanceof ApplicationFailure) {
250+
if (ApplicationFailure.is(err)) {
249251
return {
250252
...base,
251253
applicationFailureInfo: {
@@ -258,7 +260,7 @@ export class DefaultFailureConverter implements FailureConverter {
258260
},
259261
};
260262
}
261-
if (err instanceof CancelledFailure) {
263+
if (CancelledFailure.is(err)) {
262264
return {
263265
...base,
264266
canceledFailureInfo: {
@@ -269,7 +271,7 @@ export class DefaultFailureConverter implements FailureConverter {
269271
},
270272
};
271273
}
272-
if (err instanceof TimeoutFailure) {
274+
if (TimeoutFailure.is(err)) {
273275
return {
274276
...base,
275277
timeoutFailureInfo: {
@@ -280,16 +282,16 @@ export class DefaultFailureConverter implements FailureConverter {
280282
},
281283
};
282284
}
283-
if (err instanceof TerminatedFailure) {
285+
if (ServerFailure.is(err)) {
284286
return {
285287
...base,
286-
terminatedFailureInfo: {},
288+
serverFailureInfo: { nonRetryable: err.nonRetryable },
287289
};
288290
}
289-
if (err instanceof ServerFailure) {
291+
if (TerminatedFailure.is(err)) {
290292
return {
291293
...base,
292-
serverFailureInfo: { nonRetryable: err.nonRetryable },
294+
terminatedFailureInfo: {},
293295
};
294296
}
295297
// Just a TemporalFailure
@@ -333,7 +335,7 @@ export class DefaultFailureConverter implements FailureConverter {
333335
optionalFailureToOptionalError(
334336
failure: ProtoFailure | undefined | null,
335337
payloadConverter: PayloadConverter
336-
): Error | undefined {
338+
): TemporalFailure | undefined {
337339
return failure ? this.failureToError(failure, payloadConverter) : undefined;
338340
}
339341

packages/common/src/errors.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export class IllegalStateError extends Error {
2525
public readonly name: string = 'IllegalStateError';
2626
}
2727

28+
const isWorkflowExecutionAlreadyStartedError = Symbol.for('__temporal_isWorkflowExecutionAlreadyStartedError');
29+
2830
/**
2931
* This exception is thrown in the following cases:
3032
* - Workflow with the same Workflow Id is currently running
@@ -39,6 +41,21 @@ export class WorkflowExecutionAlreadyStartedError extends TemporalFailure {
3941
constructor(message: string, public readonly workflowId: string, public readonly workflowType: string) {
4042
super(message);
4143
}
44+
45+
/**
46+
* Marker to determine whether an error is an instance of WorkflowExecutionAlreadyStartedError.
47+
*/
48+
protected readonly [isWorkflowExecutionAlreadyStartedError] = true;
49+
50+
/**
51+
* Instanceof check that works when multiple versions of @temporalio/common are installed.
52+
*/
53+
static is(error: unknown): error is WorkflowExecutionAlreadyStartedError {
54+
return (
55+
error instanceof WorkflowExecutionAlreadyStartedError ||
56+
(error instanceof Error && (error as any)[isWorkflowExecutionAlreadyStartedError])
57+
);
58+
}
4259
}
4360

4461
/**

0 commit comments

Comments
 (0)