@temporalio/workflow 1.8.6 → 1.9.0
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/lib/alea.js.map +1 -1
- package/lib/cancellation-scope.js.map +1 -1
- package/lib/errors.d.ts +8 -0
- package/lib/errors.js +18 -5
- package/lib/errors.js.map +1 -1
- package/lib/global-attributes.d.ts +4 -0
- package/lib/global-attributes.js +21 -1
- package/lib/global-attributes.js.map +1 -1
- package/lib/index.d.ts +19 -8
- package/lib/index.js +15 -8
- package/lib/index.js.map +1 -1
- package/lib/interceptors.d.ts +15 -0
- package/lib/interfaces.d.ts +47 -32
- package/lib/interfaces.js +4 -4
- package/lib/interfaces.js.map +1 -1
- package/lib/internals.d.ts +24 -13
- package/lib/internals.js +145 -41
- package/lib/internals.js.map +1 -1
- package/lib/logs.d.ts +50 -0
- package/lib/logs.js +84 -0
- package/lib/logs.js.map +1 -0
- package/lib/sinks.d.ts +29 -10
- package/lib/sinks.js +57 -0
- package/lib/sinks.js.map +1 -1
- package/lib/trigger.js.map +1 -1
- package/lib/worker-interface.d.ts +2 -2
- package/lib/worker-interface.js +34 -22
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow.d.ts +23 -54
- package/lib/workflow.js +156 -141
- package/lib/workflow.js.map +1 -1
- package/package.json +4 -4
- package/src/errors.ts +11 -0
- package/src/global-attributes.ts +21 -0
- package/src/index.ts +24 -8
- package/src/interceptors.ts +18 -0
- package/src/interfaces.ts +56 -36
- package/src/internals.ts +175 -37
- package/src/logs.ts +118 -0
- package/src/sinks.ts +60 -9
- package/src/worker-interface.ts +29 -16
- package/src/workflow.ts +150 -128
package/src/internals.ts
CHANGED
|
@@ -12,15 +12,18 @@ import {
|
|
|
12
12
|
WorkflowExecutionAlreadyStartedError,
|
|
13
13
|
WorkflowQueryType,
|
|
14
14
|
WorkflowSignalType,
|
|
15
|
+
WorkflowUpdateType,
|
|
15
16
|
ProtoFailure,
|
|
17
|
+
WorkflowUpdateValidatorType,
|
|
18
|
+
ApplicationFailure,
|
|
16
19
|
} from '@temporalio/common';
|
|
17
20
|
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
18
|
-
import { checkExtends
|
|
21
|
+
import { checkExtends } from '@temporalio/common/lib/type-helpers';
|
|
19
22
|
import type { coresdk } from '@temporalio/proto';
|
|
20
23
|
import { alea, RNG } from './alea';
|
|
21
24
|
import { RootCancellationScope } from './cancellation-scope';
|
|
22
|
-
import { DeterminismViolationError, isCancellation } from './errors';
|
|
23
|
-
import { QueryInput, SignalInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
|
|
25
|
+
import { DeterminismViolationError, LocalActivityDoBackoff, isCancellation } from './errors';
|
|
26
|
+
import { QueryInput, SignalInput, UpdateInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
|
|
24
27
|
import {
|
|
25
28
|
ContinueAsNew,
|
|
26
29
|
DefaultSignalHandler,
|
|
@@ -31,10 +34,10 @@ import {
|
|
|
31
34
|
WorkflowInfo,
|
|
32
35
|
WorkflowCreateOptionsInternal,
|
|
33
36
|
} from './interfaces';
|
|
34
|
-
import { SinkCall } from './sinks';
|
|
37
|
+
import { type SinkCall } from './sinks';
|
|
35
38
|
import { untrackPromise } from './stack-helpers';
|
|
36
39
|
import pkg from './pkg';
|
|
37
|
-
import {
|
|
40
|
+
import { executeWithLifecycleLogging } from './logs';
|
|
38
41
|
|
|
39
42
|
enum StartChildWorkflowExecutionFailedCause {
|
|
40
43
|
START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
|
|
@@ -67,16 +70,6 @@ export interface Condition {
|
|
|
67
70
|
resolve(): void;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
/**
|
|
71
|
-
* A class that acts as a marker for this special result type
|
|
72
|
-
*/
|
|
73
|
-
@SymbolBasedInstanceOfError('LocalActivityDoBackoff')
|
|
74
|
-
export class LocalActivityDoBackoff extends Error {
|
|
75
|
-
constructor(public readonly backoff: coresdk.activity_result.IDoBackoff) {
|
|
76
|
-
super();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
73
|
export type ActivationHandlerFunction<K extends keyof coresdk.workflow_activation.IWorkflowActivationJob> = (
|
|
81
74
|
activation: NonNullable<coresdk.workflow_activation.IWorkflowActivationJob[K]>
|
|
82
75
|
) => void;
|
|
@@ -110,6 +103,11 @@ export class Activator implements ActivationHandler {
|
|
|
110
103
|
cancelWorkflow: new Map<number, Completion>(),
|
|
111
104
|
};
|
|
112
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Holds buffered Update calls until a handler is registered
|
|
108
|
+
*/
|
|
109
|
+
readonly bufferedUpdates = Array<coresdk.workflow_activation.IDoUpdate>();
|
|
110
|
+
|
|
113
111
|
/**
|
|
114
112
|
* Holds buffered signal calls until a handler is registered
|
|
115
113
|
*/
|
|
@@ -124,6 +122,11 @@ export class Activator implements ActivationHandler {
|
|
|
124
122
|
*/
|
|
125
123
|
protected readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
|
|
126
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Mapping of update name to handler and validator
|
|
127
|
+
*/
|
|
128
|
+
readonly updateHandlers = new Map<string, { handler: WorkflowUpdateType; validator?: WorkflowUpdateValidatorType }>();
|
|
129
|
+
|
|
127
130
|
/**
|
|
128
131
|
* Mapping of signal name to handler
|
|
129
132
|
*/
|
|
@@ -253,7 +256,7 @@ export class Activator implements ActivationHandler {
|
|
|
253
256
|
/**
|
|
254
257
|
* Information about the current Workflow
|
|
255
258
|
*/
|
|
256
|
-
public
|
|
259
|
+
public info: WorkflowInfo;
|
|
257
260
|
|
|
258
261
|
/**
|
|
259
262
|
* A deterministic RNG, used by the isolate's overridden Math.random
|
|
@@ -310,6 +313,10 @@ export class Activator implements ActivationHandler {
|
|
|
310
313
|
}
|
|
311
314
|
}
|
|
312
315
|
|
|
316
|
+
mutateWorkflowInfo(fn: (info: WorkflowInfo) => WorkflowInfo): void {
|
|
317
|
+
this.info = fn(this.info);
|
|
318
|
+
}
|
|
319
|
+
|
|
313
320
|
protected getStackTraces(): Stack[] {
|
|
314
321
|
const { childToParent, promiseToStack } = this.promiseStackStore;
|
|
315
322
|
const internalNodes = [...childToParent.values()].reduce((acc, curr) => {
|
|
@@ -367,8 +374,7 @@ export class Activator implements ActivationHandler {
|
|
|
367
374
|
try {
|
|
368
375
|
promise = workflow(...args);
|
|
369
376
|
} finally {
|
|
370
|
-
//
|
|
371
|
-
// Otherwise this Workflow will now be queryable.
|
|
377
|
+
// Queries must be handled even if there was an exception when invoking the Workflow function.
|
|
372
378
|
this.workflowFunctionWasCalled = true;
|
|
373
379
|
// Empty the buffer
|
|
374
380
|
const buffer = this.bufferedQueries.splice(0);
|
|
@@ -381,11 +387,14 @@ export class Activator implements ActivationHandler {
|
|
|
381
387
|
|
|
382
388
|
public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
|
|
383
389
|
const execute = composeInterceptors(this.interceptors.inbound, 'execute', this.startWorkflowNextHandler.bind(this));
|
|
390
|
+
|
|
384
391
|
untrackPromise(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
392
|
+
executeWithLifecycleLogging(() =>
|
|
393
|
+
execute({
|
|
394
|
+
headers: activation.headers ?? {},
|
|
395
|
+
args: arrayFromPayloads(this.payloadConverter, activation.arguments),
|
|
396
|
+
})
|
|
397
|
+
).then(this.completeWorkflow.bind(this), this.handleWorkflowFailure.bind(this))
|
|
389
398
|
);
|
|
390
399
|
}
|
|
391
400
|
|
|
@@ -480,7 +489,7 @@ export class Activator implements ActivationHandler {
|
|
|
480
489
|
}
|
|
481
490
|
}
|
|
482
491
|
|
|
483
|
-
// Intentionally
|
|
492
|
+
// Intentionally non-async function so this handler doesn't show up in the stack trace
|
|
484
493
|
protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
|
|
485
494
|
const fn = this.queryHandlers.get(queryName);
|
|
486
495
|
if (fn === undefined) {
|
|
@@ -530,6 +539,128 @@ export class Activator implements ActivationHandler {
|
|
|
530
539
|
);
|
|
531
540
|
}
|
|
532
541
|
|
|
542
|
+
public doUpdate(activation: coresdk.workflow_activation.IDoUpdate): void {
|
|
543
|
+
const { id: updateId, protocolInstanceId, name, headers, runValidator } = activation;
|
|
544
|
+
if (!updateId) {
|
|
545
|
+
throw new TypeError('Missing activation update id');
|
|
546
|
+
}
|
|
547
|
+
if (!name) {
|
|
548
|
+
throw new TypeError('Missing activation update name');
|
|
549
|
+
}
|
|
550
|
+
if (!protocolInstanceId) {
|
|
551
|
+
throw new TypeError('Missing activation update protocolInstanceId');
|
|
552
|
+
}
|
|
553
|
+
if (!this.updateHandlers.has(name)) {
|
|
554
|
+
this.bufferedUpdates.push(activation);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const makeInput = (): UpdateInput => ({
|
|
559
|
+
updateId,
|
|
560
|
+
args: arrayFromPayloads(this.payloadConverter, activation.input),
|
|
561
|
+
name,
|
|
562
|
+
headers: headers ?? {},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// The implementation below is responsible for upholding, and constrained
|
|
566
|
+
// by, the following contract:
|
|
567
|
+
//
|
|
568
|
+
// 1. If no validator is present then validation interceptors will not be run.
|
|
569
|
+
//
|
|
570
|
+
// 2. During validation, any error must fail the Update; during the Update
|
|
571
|
+
// itself, Temporal errors fail the Update whereas other errors fail the
|
|
572
|
+
// activation.
|
|
573
|
+
//
|
|
574
|
+
// 3. The handler must not see any mutations of the arguments made by the
|
|
575
|
+
// validator.
|
|
576
|
+
//
|
|
577
|
+
// 4. Any error when decoding/deserializing input must be caught and result
|
|
578
|
+
// in rejection of the Update before it is accepted, even if there is no
|
|
579
|
+
// validator.
|
|
580
|
+
//
|
|
581
|
+
// 5. The initial synchronous portion of the (async) Update handler should
|
|
582
|
+
// be executed after the (sync) validator completes such that there is
|
|
583
|
+
// minimal opportunity for a different concurrent task to be scheduled
|
|
584
|
+
// between them.
|
|
585
|
+
//
|
|
586
|
+
// 6. The stack trace view provided in the Temporal UI must not be polluted
|
|
587
|
+
// by promises that do not derive from user code. This implies that
|
|
588
|
+
// async/await syntax may not be used.
|
|
589
|
+
//
|
|
590
|
+
// Note that there is a deliberately unhandled promise rejection below.
|
|
591
|
+
// These are caught elsewhere and fail the corresponding activation.
|
|
592
|
+
let input: UpdateInput;
|
|
593
|
+
try {
|
|
594
|
+
if (runValidator && this.updateHandlers.get(name)?.validator) {
|
|
595
|
+
const validate = composeInterceptors(
|
|
596
|
+
this.interceptors.inbound,
|
|
597
|
+
'validateUpdate',
|
|
598
|
+
this.validateUpdateNextHandler.bind(this)
|
|
599
|
+
);
|
|
600
|
+
validate(makeInput());
|
|
601
|
+
}
|
|
602
|
+
input = makeInput();
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.rejectUpdate(protocolInstanceId, error);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const execute = composeInterceptors(this.interceptors.inbound, 'handleUpdate', this.updateNextHandler.bind(this));
|
|
608
|
+
this.acceptUpdate(protocolInstanceId);
|
|
609
|
+
untrackPromise(
|
|
610
|
+
execute(input)
|
|
611
|
+
.then((result) => this.completeUpdate(protocolInstanceId, result))
|
|
612
|
+
.catch((error) => {
|
|
613
|
+
if (error instanceof TemporalFailure) {
|
|
614
|
+
this.rejectUpdate(protocolInstanceId, error);
|
|
615
|
+
} else {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
protected async updateNextHandler({ name, args }: UpdateInput): Promise<unknown> {
|
|
623
|
+
const entry = this.updateHandlers.get(name);
|
|
624
|
+
if (!entry) {
|
|
625
|
+
return Promise.reject(new IllegalStateError(`No registered update handler for update: ${name}`));
|
|
626
|
+
}
|
|
627
|
+
const { handler } = entry;
|
|
628
|
+
return await handler(...args);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
protected validateUpdateNextHandler({ name, args }: UpdateInput): void {
|
|
632
|
+
const { validator } = this.updateHandlers.get(name) ?? {};
|
|
633
|
+
if (validator) {
|
|
634
|
+
validator(...args);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
public dispatchBufferedUpdates(): void {
|
|
639
|
+
const bufferedUpdates = this.bufferedUpdates;
|
|
640
|
+
while (bufferedUpdates.length) {
|
|
641
|
+
const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name as string));
|
|
642
|
+
if (foundIndex === -1) {
|
|
643
|
+
// No buffered Updates have a handler yet.
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
const [update] = bufferedUpdates.splice(foundIndex, 1);
|
|
647
|
+
this.doUpdate(update);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
public rejectBufferedUpdates(): void {
|
|
652
|
+
while (this.bufferedUpdates.length) {
|
|
653
|
+
const update = this.bufferedUpdates.shift();
|
|
654
|
+
if (update) {
|
|
655
|
+
this.rejectUpdate(
|
|
656
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
657
|
+
update.protocolInstanceId!,
|
|
658
|
+
ApplicationFailure.nonRetryable(`No registered handler for update: ${update.name}`)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
533
664
|
public async signalWorkflowNextHandler({ signalName, args }: SignalInput): Promise<void> {
|
|
534
665
|
const fn = this.signalHandlers.get(signalName);
|
|
535
666
|
if (fn) {
|
|
@@ -537,7 +668,7 @@ export class Activator implements ActivationHandler {
|
|
|
537
668
|
} else if (this.defaultSignalHandler) {
|
|
538
669
|
return await this.defaultSignalHandler(signalName, ...args);
|
|
539
670
|
} else {
|
|
540
|
-
throw new IllegalStateError(`No registered signal handler for signal ${signalName}`);
|
|
671
|
+
throw new IllegalStateError(`No registered signal handler for signal: ${signalName}`);
|
|
541
672
|
}
|
|
542
673
|
}
|
|
543
674
|
|
|
@@ -572,7 +703,7 @@ export class Activator implements ActivationHandler {
|
|
|
572
703
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
573
704
|
this.signalWorkflow(bufferedSignals.shift()!);
|
|
574
705
|
} else {
|
|
575
|
-
const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName
|
|
706
|
+
const foundIndex = bufferedSignals.findIndex((signal) => this.signalHandlers.has(signal.signalName as string));
|
|
576
707
|
if (foundIndex === -1) break;
|
|
577
708
|
const [signal] = bufferedSignals.splice(foundIndex, 1);
|
|
578
709
|
this.signalWorkflow(signal);
|
|
@@ -660,6 +791,25 @@ export class Activator implements ActivationHandler {
|
|
|
660
791
|
});
|
|
661
792
|
}
|
|
662
793
|
|
|
794
|
+
private acceptUpdate(protocolInstanceId: string): void {
|
|
795
|
+
this.pushCommand({ updateResponse: { protocolInstanceId, accepted: {} } });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private completeUpdate(protocolInstanceId: string, result: unknown): void {
|
|
799
|
+
this.pushCommand({
|
|
800
|
+
updateResponse: { protocolInstanceId, completed: this.payloadConverter.toPayload(result) },
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private rejectUpdate(protocolInstanceId: string, error: unknown): void {
|
|
805
|
+
this.pushCommand({
|
|
806
|
+
updateResponse: {
|
|
807
|
+
protocolInstanceId,
|
|
808
|
+
rejected: this.errorToFailure(ensureTemporalFailure(error)),
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
663
813
|
/** Consume a completion if it exists in Workflow state */
|
|
664
814
|
private maybeConsumeCompletion(type: keyof Activator['completions'], taskSeq: number): Completion | undefined {
|
|
665
815
|
const completion = this.completions[type].get(taskSeq);
|
|
@@ -698,18 +848,6 @@ export class Activator implements ActivationHandler {
|
|
|
698
848
|
}
|
|
699
849
|
}
|
|
700
850
|
|
|
701
|
-
export function maybeGetActivator(): Activator | undefined {
|
|
702
|
-
return maybeGetActivatorUntyped() as Activator | undefined;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
export function getActivator(): Activator {
|
|
706
|
-
const activator = maybeGetActivator();
|
|
707
|
-
if (activator === undefined) {
|
|
708
|
-
throw new IllegalStateError('Workflow uninitialized');
|
|
709
|
-
}
|
|
710
|
-
return activator;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
851
|
function getSeq<T extends { seq?: number | null }>(activation: T): number {
|
|
714
852
|
const seq = activation.seq;
|
|
715
853
|
if (seq === undefined || seq === null) {
|
package/src/logs.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
2
|
+
import { untrackPromise } from './stack-helpers';
|
|
3
|
+
import { type Sink, type Sinks, proxySinks } from './sinks';
|
|
4
|
+
import { isCancellation } from './errors';
|
|
5
|
+
import { WorkflowInfo, ContinueAsNew } from './interfaces';
|
|
6
|
+
import { assertInWorkflowContext } from './global-attributes';
|
|
7
|
+
|
|
8
|
+
export interface WorkflowLogger extends Sink {
|
|
9
|
+
trace(message: string, attrs?: Record<string, unknown>): void;
|
|
10
|
+
debug(message: string, attrs?: Record<string, unknown>): void;
|
|
11
|
+
info(message: string, attrs?: Record<string, unknown>): void;
|
|
12
|
+
warn(message: string, attrs?: Record<string, unknown>): void;
|
|
13
|
+
error(message: string, attrs?: Record<string, unknown>): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sink interface for forwarding logs from the Workflow sandbox to the Worker
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Do not use LoggerSinks directly. To log from Workflow code, use the `log` object
|
|
20
|
+
* exported by the `@temporalio/workflow` package. To capture log messages emitted
|
|
21
|
+
* by Workflow code, set the {@link Runtime.logger} property.
|
|
22
|
+
*/
|
|
23
|
+
export interface LoggerSinksDeprecated extends Sinks {
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Do not use LoggerSinks directly. To log from Workflow code, use the `log` object
|
|
26
|
+
* exported by the `@temporalio/workflow` package. To capture log messages emitted
|
|
27
|
+
* by Workflow code, set the {@link Runtime.logger} property.
|
|
28
|
+
*/
|
|
29
|
+
defaultWorkerLogger: WorkflowLogger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sink interface for forwarding logs from the Workflow sandbox to the Worker
|
|
34
|
+
*/
|
|
35
|
+
export interface LoggerSinksInternal extends Sinks {
|
|
36
|
+
__temporal_logger: WorkflowLogger;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const loggerSink = proxySinks<LoggerSinksInternal>().__temporal_logger;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Symbol used by the SDK logger to extract a timestamp from log attributes.
|
|
43
|
+
* Also defined in `worker/logger.ts` - intentionally not shared.
|
|
44
|
+
*/
|
|
45
|
+
const LogTimestamp = Symbol.for('log_timestamp');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default workflow logger.
|
|
49
|
+
*
|
|
50
|
+
* This logger is replay-aware and will omit log messages on workflow replay. Messages emitted by this logger are
|
|
51
|
+
* funnelled through a sink that forwards them to the logger registered on {@link Runtime.logger}.
|
|
52
|
+
*
|
|
53
|
+
* Notice that since sinks are used to power this logger, any log attributes must be transferable via the
|
|
54
|
+
* {@link https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist | postMessage}
|
|
55
|
+
* API.
|
|
56
|
+
*
|
|
57
|
+
* NOTE: Specifying a custom logger through {@link defaultSink} or by manually registering a sink named
|
|
58
|
+
* `defaultWorkerLogger` has been deprecated. Please use {@link Runtime.logger} instead.
|
|
59
|
+
*/
|
|
60
|
+
export const log: WorkflowLogger = Object.fromEntries(
|
|
61
|
+
(['trace', 'debug', 'info', 'warn', 'error'] as Array<keyof WorkflowLogger>).map((level) => {
|
|
62
|
+
return [
|
|
63
|
+
level,
|
|
64
|
+
(message: string, attrs?: Record<string, unknown>) => {
|
|
65
|
+
const activator = assertInWorkflowContext('Workflow.log(...) may only be used from workflow context.');
|
|
66
|
+
const getLogAttributes = composeInterceptors(activator.interceptors.outbound, 'getLogAttributes', (a) => a);
|
|
67
|
+
return loggerSink[level](message, {
|
|
68
|
+
// Inject the call time in nanosecond resolution as expected by the worker logger.
|
|
69
|
+
[LogTimestamp]: activator.getTimeOfDay(),
|
|
70
|
+
...getLogAttributes(workflowLogAttributes(activator.info)),
|
|
71
|
+
...attrs,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
})
|
|
76
|
+
) as any;
|
|
77
|
+
|
|
78
|
+
export function executeWithLifecycleLogging(fn: () => Promise<unknown>): Promise<unknown> {
|
|
79
|
+
log.debug('Workflow started');
|
|
80
|
+
const p = fn().then(
|
|
81
|
+
(res) => {
|
|
82
|
+
log.debug('Workflow completed');
|
|
83
|
+
return res;
|
|
84
|
+
},
|
|
85
|
+
(error) => {
|
|
86
|
+
// Avoid using instanceof checks in case the modules they're defined in loaded more than once,
|
|
87
|
+
// e.g. by jest or when multiple versions are installed.
|
|
88
|
+
if (typeof error === 'object' && error != null) {
|
|
89
|
+
if (isCancellation(error)) {
|
|
90
|
+
log.debug('Workflow completed as cancelled');
|
|
91
|
+
throw error;
|
|
92
|
+
} else if (error instanceof ContinueAsNew) {
|
|
93
|
+
log.debug('Workflow continued as new');
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
log.warn('Workflow failed', { error });
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
// Avoid showing this interceptor in stack trace query
|
|
102
|
+
untrackPromise(p);
|
|
103
|
+
return p;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns a map of attributes to be set _by default_ on log messages for a given Workflow.
|
|
108
|
+
* Note that this function may be called from outside of the Workflow context (eg. by the worker itself).
|
|
109
|
+
*/
|
|
110
|
+
export function workflowLogAttributes(info: WorkflowInfo): Record<string, unknown> {
|
|
111
|
+
return {
|
|
112
|
+
namespace: info.namespace,
|
|
113
|
+
taskQueue: info.taskQueue,
|
|
114
|
+
workflowId: info.workflowId,
|
|
115
|
+
runId: info.runId,
|
|
116
|
+
workflowType: info.workflowType,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/src/sinks.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { WorkflowInfo } from './interfaces';
|
|
18
|
+
import { assertInWorkflowContext } from './global-attributes';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Any function signature can be used for Sink functions as long as the return type is `void`.
|
|
@@ -44,14 +45,64 @@ export interface SinkCall {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
+
* Get a reference to Sinks for exporting data out of the Workflow.
|
|
49
|
+
*
|
|
50
|
+
* These Sinks **must** be registered with the Worker in order for this
|
|
51
|
+
* mechanism to work.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { proxySinks, Sinks } from '@temporalio/workflow';
|
|
56
|
+
*
|
|
57
|
+
* interface MySinks extends Sinks {
|
|
58
|
+
* logger: {
|
|
59
|
+
* info(message: string): void;
|
|
60
|
+
* error(message: string): void;
|
|
61
|
+
* };
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* const { logger } = proxySinks<MyDependencies>();
|
|
65
|
+
* logger.info('setting up');
|
|
66
|
+
*
|
|
67
|
+
* export function myWorkflow() {
|
|
68
|
+
* return {
|
|
69
|
+
* async execute() {
|
|
70
|
+
* logger.info("hey ho");
|
|
71
|
+
* logger.error("lets go");
|
|
72
|
+
* }
|
|
73
|
+
* };
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
48
76
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
export function proxySinks<T extends Sinks>(): T {
|
|
78
|
+
return new Proxy(
|
|
79
|
+
{},
|
|
80
|
+
{
|
|
81
|
+
get(_, ifaceName) {
|
|
82
|
+
return new Proxy(
|
|
83
|
+
{},
|
|
84
|
+
{
|
|
85
|
+
get(_, fnName) {
|
|
86
|
+
return (...args: any[]) => {
|
|
87
|
+
const activator = assertInWorkflowContext(
|
|
88
|
+
'Proxied sinks functions may only be used from a Workflow Execution.'
|
|
89
|
+
);
|
|
90
|
+
activator.sinkCalls.push({
|
|
91
|
+
ifaceName: ifaceName as string,
|
|
92
|
+
fnName: fnName as string,
|
|
93
|
+
// Sink function doesn't get called immediately. Make a clone of the sink's args, so that further mutations
|
|
94
|
+
// to these objects don't corrupt the args that the sink function will receive. Only available from node 17.
|
|
95
|
+
args: (globalThis as any).structuredClone ? (globalThis as any).structuredClone(args) : args,
|
|
96
|
+
// activator.info is internally copy-on-write. This ensure that any further mutations
|
|
97
|
+
// to the workflow state in the context of the present activation will not corrupt the
|
|
98
|
+
// workflowInfo state that gets passed when the sink function actually gets called.
|
|
99
|
+
workflowInfo: activator.info,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
) as any;
|
|
57
108
|
}
|
package/src/worker-interface.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
import { IllegalStateError } from '@temporalio/common';
|
|
7
7
|
import { msToTs, tsToMs } from '@temporalio/common/lib/time';
|
|
8
8
|
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
9
|
-
import
|
|
9
|
+
import { coresdk } from '@temporalio/proto';
|
|
10
10
|
import { disableStorage } from './cancellation-scope';
|
|
11
11
|
import { DeterminismViolationError } from './errors';
|
|
12
12
|
import { WorkflowInterceptorsFactory } from './interceptors';
|
|
13
|
-
import { WorkflowCreateOptionsInternal
|
|
14
|
-
import { Activator
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { WorkflowCreateOptionsInternal } from './interfaces';
|
|
14
|
+
import { Activator } from './internals';
|
|
15
|
+
import { setActivatorUntyped, getActivator } from './global-attributes';
|
|
16
|
+
import { type SinkCall } from './sinks';
|
|
17
17
|
|
|
18
18
|
// Export the type for use on the "worker" side
|
|
19
19
|
export { PromiseStackStore } from './internals';
|
|
@@ -93,9 +93,13 @@ export function overrideGlobals(): void {
|
|
|
93
93
|
* Sets required internal state and instantiates the workflow and interceptors.
|
|
94
94
|
*/
|
|
95
95
|
export function initRuntime(options: WorkflowCreateOptionsInternal): void {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
const activator = new Activator({
|
|
97
|
+
...options,
|
|
98
|
+
info: fixPrototypes({
|
|
99
|
+
...options.info,
|
|
100
|
+
unsafe: { ...options.info.unsafe, now: OriginalDate.now },
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
99
103
|
// There's on activator per workflow instance, set it globally on the context.
|
|
100
104
|
// We do this before importing any user code so user code can statically reference @temporalio/workflow functions
|
|
101
105
|
// as well as Date and Math.random.
|
|
@@ -136,7 +140,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
const mod = importWorkflows();
|
|
139
|
-
const workflowFn = mod[info.workflowType];
|
|
143
|
+
const workflowFn = mod[activator.info.workflowType];
|
|
140
144
|
const defaultWorkflowFn = mod['default'];
|
|
141
145
|
|
|
142
146
|
if (typeof workflowFn === 'function') {
|
|
@@ -148,7 +152,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
|
|
|
148
152
|
workflowFn === undefined
|
|
149
153
|
? 'no such function is exported by the workflow bundle'
|
|
150
154
|
: `expected a function, but got: '${typeof workflowFn}'`;
|
|
151
|
-
throw new TypeError(`Failed to initialize workflow of type '${info.workflowType}': ${details}`);
|
|
155
|
+
throw new TypeError(`Failed to initialize workflow of type '${activator.info.workflowType}': ${details}`);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
|
|
@@ -188,12 +192,19 @@ export function activate(activation: coresdk.workflow_activation.WorkflowActivat
|
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
// The Rust Core ensures that these activation fields are not null
|
|
191
|
-
activator.info
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
activator.mutateWorkflowInfo((info) => ({
|
|
196
|
+
...info,
|
|
197
|
+
historyLength: activation.historyLength as number,
|
|
198
|
+
// Exact truncation for multi-petabyte histories
|
|
199
|
+
// historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
|
|
200
|
+
historySize: activation.historySizeBytes?.toNumber() || 0,
|
|
201
|
+
continueAsNewSuggested: activation.continueAsNewSuggested ?? false,
|
|
202
|
+
currentBuildId: activation.buildIdForCurrentTask ?? undefined,
|
|
203
|
+
unsafe: {
|
|
204
|
+
...info.unsafe,
|
|
205
|
+
isReplaying: activation.isReplaying ?? false,
|
|
206
|
+
},
|
|
207
|
+
}));
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
// Cast from the interface to the class which has the `variant` attribute.
|
|
@@ -235,9 +246,11 @@ export function activate(activation: coresdk.workflow_activation.WorkflowActivat
|
|
|
235
246
|
*/
|
|
236
247
|
export function concludeActivation(): coresdk.workflow_completion.IWorkflowActivationCompletion {
|
|
237
248
|
const activator = getActivator();
|
|
249
|
+
activator.rejectBufferedUpdates();
|
|
238
250
|
const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
|
|
239
251
|
const { info } = activator;
|
|
240
252
|
const { commands } = intercept({ commands: activator.getAndResetCommands() });
|
|
253
|
+
|
|
241
254
|
return {
|
|
242
255
|
runId: info.runId,
|
|
243
256
|
successful: { commands },
|