@temporalio/nexus 1.17.2 → 1.17.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/lib/context.d.ts +0 -6
- package/lib/context.js +0 -1
- package/lib/context.js.map +1 -1
- package/package.json +7 -7
- package/src/context.ts +19 -0
- package/src/index.ts +8 -0
- package/src/link-converter.ts +77 -3
- package/src/token.ts +67 -18
- package/src/workflow-helpers.ts +213 -11
package/lib/context.d.ts
CHANGED
|
@@ -15,7 +15,6 @@ export interface HandlerContext {
|
|
|
15
15
|
client: Client;
|
|
16
16
|
namespace: string;
|
|
17
17
|
taskQueue: string;
|
|
18
|
-
endpoint: string;
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
20
|
* Holds information about the current Nexus Operation Execution.
|
|
@@ -31,11 +30,6 @@ export interface OperationInfo {
|
|
|
31
30
|
* Task Queue this Nexus Operation is executing on
|
|
32
31
|
*/
|
|
33
32
|
readonly taskQueue: string;
|
|
34
|
-
/**
|
|
35
|
-
* Nexus Endpoint this Operation was routed through.
|
|
36
|
-
* Only available with server version 1.30.0 or later.
|
|
37
|
-
*/
|
|
38
|
-
readonly endpoint: string;
|
|
39
33
|
}
|
|
40
34
|
/**
|
|
41
35
|
* A logger for use in Nexus Handler scope.
|
package/lib/context.js
CHANGED
package/lib/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":";;;AAaA,8CAMC;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@temporalio/nexus",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.3",
|
|
4
4
|
"description": "Temporal.io SDK Nexus sub-package",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"concurrency": 1,
|
|
18
18
|
"workerThreads": false
|
|
19
19
|
},
|
|
20
|
-
"devDependencies": {
|
|
21
|
-
"ava": "^5.3.1"
|
|
22
|
-
},
|
|
23
20
|
"dependencies": {
|
|
24
21
|
"long": "^5.2.3",
|
|
25
22
|
"nexus-rpc": "^0.0.2",
|
|
26
|
-
"@temporalio/
|
|
27
|
-
"@temporalio/common": "1.17.
|
|
28
|
-
"@temporalio/
|
|
23
|
+
"@temporalio/proto": "1.17.3",
|
|
24
|
+
"@temporalio/common": "1.17.3",
|
|
25
|
+
"@temporalio/client": "1.17.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"ava": "^5.3.1"
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
31
31
|
"url": "https://github.com/temporalio/sdk-typescript/issues"
|
package/src/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type * as nexus from 'nexus-rpc';
|
|
2
3
|
import type { Logger, LogLevel, LogMetadata, MetricMeter } from '@temporalio/common';
|
|
3
4
|
import type { Client } from '@temporalio/client';
|
|
4
5
|
|
|
@@ -57,6 +58,24 @@ export interface OperationInfo {
|
|
|
57
58
|
readonly endpoint: string;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Context received by a {@link TemporalOperationHandler}'s start handler when a Nexus Operation is
|
|
63
|
+
* started.
|
|
64
|
+
*
|
|
65
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
66
|
+
*/
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
68
|
+
export interface TemporalStartOperationContext extends nexus.StartOperationContext {}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Context received by a {@link TemporalOperationHandler}'s cancel handler when a Nexus Operation is
|
|
72
|
+
* canceled.
|
|
73
|
+
*
|
|
74
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
75
|
+
*/
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
77
|
+
export interface TemporalCancelOperationContext extends nexus.CancelOperationContext {}
|
|
78
|
+
|
|
60
79
|
// Basic APIs //////////////////////////////////////////////////////////////////////////////////////
|
|
61
80
|
|
|
62
81
|
/**
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,18 @@ export {
|
|
|
11
11
|
metricMeter,
|
|
12
12
|
operationInfo,
|
|
13
13
|
type OperationInfo,
|
|
14
|
+
type TemporalCancelOperationContext,
|
|
15
|
+
type TemporalStartOperationContext,
|
|
14
16
|
} from './context';
|
|
15
17
|
|
|
16
18
|
export {
|
|
17
19
|
startWorkflow,
|
|
20
|
+
type CancelWorkflowRunOptions,
|
|
21
|
+
type TemporalOperationHandlerOptions,
|
|
22
|
+
TemporalOperationHandler,
|
|
23
|
+
TemporalOperationResult,
|
|
24
|
+
type TemporalNexusClient,
|
|
25
|
+
type TemporalOperationStartHandler,
|
|
18
26
|
WorkflowHandle,
|
|
19
27
|
WorkflowRunOperationHandler,
|
|
20
28
|
WorkflowRunOperationStartHandler,
|
package/src/link-converter.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { Link as NexusLink } from 'nexus-rpc';
|
|
|
3
3
|
import { temporal } from '@temporalio/proto';
|
|
4
4
|
|
|
5
5
|
const { EventType } = temporal.api.enums.v1;
|
|
6
|
+
type TemporalLink = temporal.api.common.v1.ILink;
|
|
6
7
|
type WorkflowEventLink = temporal.api.common.v1.Link.IWorkflowEvent;
|
|
8
|
+
type NexusOperationLink = temporal.api.common.v1.Link.INexusOperation;
|
|
7
9
|
type EventReference = temporal.api.common.v1.Link.WorkflowEvent.IEventReference;
|
|
8
10
|
type RequestIdReference = temporal.api.common.v1.Link.WorkflowEvent.IRequestIdReference;
|
|
9
11
|
|
|
@@ -11,12 +13,46 @@ const LINK_EVENT_ID_PARAM = 'eventID';
|
|
|
11
13
|
const LINK_EVENT_TYPE_PARAM = 'eventType';
|
|
12
14
|
const LINK_REQUEST_ID_PARAM = 'requestID';
|
|
13
15
|
const LINK_REFERENCE_TYPE_KEY = 'referenceType';
|
|
16
|
+
const LINK_RUN_ID_KEY = 'runID';
|
|
14
17
|
|
|
15
18
|
const EVENT_REFERENCE_TYPE = 'EventReference';
|
|
16
19
|
const REQUEST_ID_REFERENCE_TYPE = 'RequestIdReference';
|
|
17
20
|
|
|
18
21
|
// fullName isn't part of the generated typed unfortunately.
|
|
19
22
|
const WORKFLOW_EVENT_TYPE: string = (temporal.api.common.v1.Link.WorkflowEvent as any).fullName.slice(1);
|
|
23
|
+
const NEXUS_OPERATION_TYPE: string = (temporal.api.common.v1.Link.NexusOperation as any).fullName.slice(1);
|
|
24
|
+
|
|
25
|
+
export function convertTemporalLinkToNexusLink(link: TemporalLink): NexusLink {
|
|
26
|
+
if (link.workflowEvent != null) {
|
|
27
|
+
return convertWorkflowEventLinkToNexusLink(link.workflowEvent);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (link.nexusOperation != null) {
|
|
31
|
+
return convertNexusOperationLinkToNexusLink(link.nexusOperation);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new TypeError('Invalid Temporal link: unknown variant');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function convertNexusLinkToTemporalLink(link: NexusLink): TemporalLink {
|
|
38
|
+
if (link.url.protocol !== 'temporal:') {
|
|
39
|
+
throw new TypeError(`Invalid URL scheme: ${link.url}, expected 'temporal:', got '${link.url.protocol}'`);
|
|
40
|
+
}
|
|
41
|
+
switch (link.type) {
|
|
42
|
+
case WORKFLOW_EVENT_TYPE:
|
|
43
|
+
return {
|
|
44
|
+
workflowEvent: convertNexusLinkToWorkflowEventLink(link),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
case NEXUS_OPERATION_TYPE:
|
|
48
|
+
return {
|
|
49
|
+
nexusOperation: convertNexusLinkToNexusOperationLink(link),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
throw new TypeError(`Unknown link type: ${link.type}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
20
56
|
|
|
21
57
|
export function convertWorkflowEventLinkToNexusLink(we: WorkflowEventLink): NexusLink {
|
|
22
58
|
if (!we.namespace || !we.workflowId || !we.runId) {
|
|
@@ -40,11 +76,30 @@ export function convertWorkflowEventLinkToNexusLink(we: WorkflowEventLink): Nexu
|
|
|
40
76
|
};
|
|
41
77
|
}
|
|
42
78
|
|
|
43
|
-
export function
|
|
44
|
-
if (
|
|
45
|
-
throw new TypeError(
|
|
79
|
+
export function convertNexusOperationLinkToNexusLink(opLink: NexusOperationLink): NexusLink {
|
|
80
|
+
if (!opLink.namespace || !opLink.operationId) {
|
|
81
|
+
throw new TypeError('Missing required fields: namespace, or operationId');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const url = new URL(
|
|
85
|
+
`temporal:///namespaces/${encodeURIComponent(opLink.namespace)}/nexus-operations/${encodeURIComponent(
|
|
86
|
+
opLink.operationId
|
|
87
|
+
)}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (opLink.runId != null) {
|
|
91
|
+
const searchParams = new URLSearchParams();
|
|
92
|
+
searchParams.set(LINK_RUN_ID_KEY, opLink.runId);
|
|
93
|
+
url.search = searchParams.toString();
|
|
46
94
|
}
|
|
47
95
|
|
|
96
|
+
return {
|
|
97
|
+
url,
|
|
98
|
+
type: NEXUS_OPERATION_TYPE,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function convertNexusLinkToWorkflowEventLink(link: NexusLink): WorkflowEventLink {
|
|
48
103
|
// /namespaces/:namespace/workflows/:workflowId/:runId/history
|
|
49
104
|
const parts = link.url.pathname.split('/');
|
|
50
105
|
if (parts.length !== 7 || parts[1] !== 'namespaces' || parts[3] !== 'workflows' || parts[6] !== 'history') {
|
|
@@ -76,6 +131,25 @@ export function convertNexusLinkToWorkflowEventLink(link: NexusLink): WorkflowEv
|
|
|
76
131
|
return workflowEventLink;
|
|
77
132
|
}
|
|
78
133
|
|
|
134
|
+
function convertNexusLinkToNexusOperationLink(link: NexusLink): NexusOperationLink {
|
|
135
|
+
// /namespaces/:namespace/nexus-operations/:operationId?runId=:runId
|
|
136
|
+
const parts = link.url.pathname.split('/');
|
|
137
|
+
if (parts.length !== 5 || parts[1] !== 'namespaces' || parts[3] !== 'nexus-operations') {
|
|
138
|
+
throw new TypeError(`Invalid URL path: ${link.url}`);
|
|
139
|
+
}
|
|
140
|
+
const namespace = decodeURIComponent(parts[2]!);
|
|
141
|
+
const operationId = decodeURIComponent(parts[4]!);
|
|
142
|
+
|
|
143
|
+
const query = link.url.searchParams;
|
|
144
|
+
const runId = query.get(LINK_RUN_ID_KEY);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
namespace,
|
|
148
|
+
operationId,
|
|
149
|
+
runId,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
79
153
|
function convertLinkWorkflowEventEventReferenceToURLQuery(eventRef: EventReference): string {
|
|
80
154
|
const params = new URLSearchParams();
|
|
81
155
|
params.set(LINK_REFERENCE_TYPE_KEY, EVENT_REFERENCE_TYPE);
|
package/src/token.ts
CHANGED
|
@@ -1,39 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Currently, we only have one type of Operation token: WorkflowRun.
|
|
2
|
+
* Serializable token identifying a Nexus operation target.
|
|
4
3
|
*
|
|
5
4
|
* @internal
|
|
6
5
|
* @hidden
|
|
7
6
|
*/
|
|
8
|
-
export interface
|
|
7
|
+
export interface OperationToken {
|
|
9
8
|
/**
|
|
10
|
-
* Version of the token, by default we assume we're on version
|
|
9
|
+
* Version of the token, by default we assume we're on version 0, this field is not emitted as part of the output,
|
|
11
10
|
* it's only used to reject newer token versions on load.
|
|
12
11
|
*/
|
|
13
12
|
v?: number;
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
|
-
* Type of the Operation.
|
|
15
|
+
* Type of the Operation.
|
|
17
16
|
*/
|
|
18
17
|
t: OperationTokenType;
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
* Namespace of the
|
|
20
|
+
* Namespace of the operation.
|
|
22
21
|
*/
|
|
23
22
|
ns: string;
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* ID of the workflow.
|
|
27
26
|
*/
|
|
27
|
+
wid?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An OperationToken that identifies a WorkflowRun operation.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
* @hidden
|
|
35
|
+
*/
|
|
36
|
+
export interface WorkflowRunOperationToken extends OperationToken {
|
|
37
|
+
t: typeof OperationTokenType.WORKFLOW_RUN;
|
|
28
38
|
wid: string;
|
|
29
39
|
}
|
|
30
|
-
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* OperationTokenType is used to identify the type of Operation token.
|
|
43
|
+
* Currently, we only have one type of Operation token: WorkflowRun.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
* @hidden
|
|
47
|
+
*/
|
|
48
|
+
export type OperationTokenType = (typeof OperationTokenType)[keyof typeof OperationTokenType];
|
|
31
49
|
|
|
32
50
|
/**
|
|
33
51
|
* @internal
|
|
34
52
|
* @hidden
|
|
35
53
|
*/
|
|
36
|
-
const OperationTokenType = {
|
|
54
|
+
export const OperationTokenType = {
|
|
37
55
|
WORKFLOW_RUN: 1,
|
|
38
56
|
} as const;
|
|
39
57
|
|
|
@@ -50,11 +68,11 @@ export function generateWorkflowRunOperationToken(namespace: string, workflowId:
|
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
/**
|
|
53
|
-
* Load and validate
|
|
71
|
+
* Load and validate the common fields of an Operation token.
|
|
54
72
|
*/
|
|
55
|
-
export function
|
|
73
|
+
export function loadOperationToken(data: string): OperationToken {
|
|
56
74
|
if (!data) {
|
|
57
|
-
throw new TypeError('invalid
|
|
75
|
+
throw new TypeError('invalid operation token: token is empty');
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
let decoded: string;
|
|
@@ -64,26 +82,57 @@ export function loadWorkflowRunOperationToken(data: string): WorkflowRunOperatio
|
|
|
64
82
|
throw new TypeError('failed to decode token', { cause: err });
|
|
65
83
|
}
|
|
66
84
|
|
|
67
|
-
let token:
|
|
85
|
+
let token: OperationToken;
|
|
68
86
|
try {
|
|
69
87
|
token = JSON.parse(decoded);
|
|
70
88
|
} catch (err) {
|
|
71
|
-
throw new TypeError('failed to unmarshal
|
|
89
|
+
throw new TypeError('failed to unmarshal Operation token', { cause: err });
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
if (token
|
|
75
|
-
throw new TypeError(`invalid
|
|
92
|
+
if (typeof token !== 'object' || token == null) {
|
|
93
|
+
throw new TypeError(`invalid operation token: expected object, got ${typeof token}`);
|
|
76
94
|
}
|
|
77
95
|
if (token.v !== undefined && token.v !== 0) {
|
|
78
|
-
throw new TypeError('invalid
|
|
96
|
+
throw new TypeError('invalid operation token: "v" field should not be present');
|
|
79
97
|
}
|
|
80
|
-
if (
|
|
81
|
-
throw new TypeError(
|
|
98
|
+
if (typeof token.t !== 'number') {
|
|
99
|
+
throw new TypeError(`invalid operation token: expected token type to be a number, got ${typeof token.t}`);
|
|
100
|
+
}
|
|
101
|
+
if (!isOperationTokenType(token.t)) {
|
|
102
|
+
throw new TypeError(`invalid operation token: unknown token type: ${token.t}`);
|
|
103
|
+
}
|
|
104
|
+
if (typeof token.ns !== 'string') {
|
|
105
|
+
throw new TypeError(`invalid operation token: expected namespace to be a string, got ${typeof token.ns}`);
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
return token;
|
|
85
109
|
}
|
|
86
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Load and validate a workflow run Operation token.
|
|
113
|
+
*/
|
|
114
|
+
export function loadWorkflowRunOperationToken(data: string): WorkflowRunOperationToken {
|
|
115
|
+
const token = loadOperationToken(data);
|
|
116
|
+
assertWorkflowRunOperationToken(token);
|
|
117
|
+
return token;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Assert that an OperationToken identifies a workflow run.
|
|
122
|
+
*/
|
|
123
|
+
export function assertWorkflowRunOperationToken(token: OperationToken): asserts token is WorkflowRunOperationToken {
|
|
124
|
+
if (token.t !== OperationTokenType.WORKFLOW_RUN) {
|
|
125
|
+
throw new TypeError(`invalid workflow token type: ${token.t}, expected: ${OperationTokenType.WORKFLOW_RUN}`);
|
|
126
|
+
}
|
|
127
|
+
if (!token.wid || typeof token.wid !== 'string') {
|
|
128
|
+
throw new TypeError('invalid workflow run token: missing workflow ID (wid)');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isOperationTokenType(value: number): value is OperationTokenType {
|
|
133
|
+
return Object.values(OperationTokenType).includes(value as OperationTokenType);
|
|
134
|
+
}
|
|
135
|
+
|
|
87
136
|
// Exported for use in tests.
|
|
88
137
|
export function base64URLEncodeNoPadding(str: string): string {
|
|
89
138
|
const base64 = Buffer.from(str).toString('base64url');
|
package/src/workflow-helpers.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import * as nexus from 'nexus-rpc';
|
|
2
2
|
import type { Workflow, WorkflowResultType } from '@temporalio/common';
|
|
3
3
|
import type { Replace } from '@temporalio/common/lib/type-helpers';
|
|
4
|
-
import type { WorkflowStartOptions as ClientWorkflowStartOptions } from '@temporalio/client';
|
|
4
|
+
import type { Client, WorkflowStartOptions as ClientWorkflowStartOptions } from '@temporalio/client';
|
|
5
5
|
import { type temporal } from '@temporalio/proto';
|
|
6
6
|
import type { InternalWorkflowStartOptions } from '@temporalio/client/lib/internal';
|
|
7
7
|
import { InternalWorkflowStartOptionsSymbol } from '@temporalio/client/lib/internal';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
8
|
+
import { convertNexusLinkToTemporalLink, convertTemporalLinkToNexusLink } from './link-converter';
|
|
9
|
+
import {
|
|
10
|
+
assertWorkflowRunOperationToken,
|
|
11
|
+
generateWorkflowRunOperationToken,
|
|
12
|
+
loadOperationToken,
|
|
13
|
+
loadWorkflowRunOperationToken,
|
|
14
|
+
OperationTokenType,
|
|
15
|
+
} from './token';
|
|
16
|
+
import {
|
|
17
|
+
getClient,
|
|
18
|
+
getHandlerContext,
|
|
19
|
+
log,
|
|
20
|
+
type TemporalCancelOperationContext,
|
|
21
|
+
type TemporalStartOperationContext,
|
|
22
|
+
} from './context';
|
|
11
23
|
|
|
12
24
|
declare const isNexusWorkflowHandle: unique symbol;
|
|
13
25
|
declare const workflowResultType: unique symbol;
|
|
@@ -77,9 +89,7 @@ export async function startWorkflow<T extends Workflow>(
|
|
|
77
89
|
if (ctx.inboundLinks?.length > 0) {
|
|
78
90
|
for (const l of ctx.inboundLinks) {
|
|
79
91
|
try {
|
|
80
|
-
links.push(
|
|
81
|
-
workflowEvent: convertNexusLinkToWorkflowEventLink(l),
|
|
82
|
-
});
|
|
92
|
+
links.push(convertNexusLinkToTemporalLink(l));
|
|
83
93
|
} catch (error) {
|
|
84
94
|
log.warn('failed to convert Nexus link to Workflow event link', { error });
|
|
85
95
|
}
|
|
@@ -96,10 +106,17 @@ export async function startWorkflow<T extends Workflow>(
|
|
|
96
106
|
attachRequestId: true,
|
|
97
107
|
};
|
|
98
108
|
|
|
109
|
+
// Add nexus-operation-token header to solve for race between Workflow completion
|
|
110
|
+
// and Nexus Operation start recording
|
|
111
|
+
const callbackHeaders = {
|
|
112
|
+
...ctx.callbackHeaders,
|
|
113
|
+
'nexus-operation-token': generateWorkflowRunOperationToken(client.options.namespace, workflowOptions.workflowId),
|
|
114
|
+
};
|
|
115
|
+
|
|
99
116
|
if (ctx.callbackUrl) {
|
|
100
117
|
internalOptions.completionCallbacks = [
|
|
101
118
|
{
|
|
102
|
-
nexus: { url: ctx.callbackUrl, header:
|
|
119
|
+
nexus: { url: ctx.callbackUrl, header: callbackHeaders },
|
|
103
120
|
links, // pass in links here as well for older servers, newer servers dedupe them.
|
|
104
121
|
},
|
|
105
122
|
];
|
|
@@ -113,11 +130,11 @@ export async function startWorkflow<T extends Workflow>(
|
|
|
113
130
|
};
|
|
114
131
|
|
|
115
132
|
const handle = await client.workflow.start(workflowTypeOrFunc, startOptions);
|
|
116
|
-
if (internalOptions.backLink
|
|
133
|
+
if (internalOptions.backLink != null) {
|
|
117
134
|
try {
|
|
118
|
-
ctx.outboundLinks.push(
|
|
135
|
+
ctx.outboundLinks.push(convertTemporalLinkToNexusLink(internalOptions.backLink));
|
|
119
136
|
} catch (error) {
|
|
120
|
-
log.warn('failed to convert
|
|
137
|
+
log.warn('failed to convert temporal link to Nexus link', { error });
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
|
|
@@ -156,3 +173,188 @@ export class WorkflowRunOperationHandler<I, O> implements nexus.OperationHandler
|
|
|
156
173
|
await getClient().workflow.getHandle(decoded.wid).cancel();
|
|
157
174
|
}
|
|
158
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Module-private brand and payload key for {@link TemporalOperationResult}.
|
|
179
|
+
*/
|
|
180
|
+
const operationResult: unique symbol = Symbol('temporal_nexus_TemporalOperationResult');
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A result produced by a {@link TemporalOperationHandler}. Construct via
|
|
184
|
+
* {@link TemporalOperationResult.sync} or {@link TemporalOperationResult.async}.
|
|
185
|
+
|
|
186
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
187
|
+
*/
|
|
188
|
+
export interface TemporalOperationResult<T> {
|
|
189
|
+
readonly [operationResult]: nexus.HandlerStartOperationResult<T>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const TemporalOperationResult = {
|
|
193
|
+
sync<T>(value: T): TemporalOperationResult<T> {
|
|
194
|
+
return {
|
|
195
|
+
[operationResult]: nexus.HandlerStartOperationResult.sync(value),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async<T = unknown>(token: string): TemporalOperationResult<T> {
|
|
200
|
+
return {
|
|
201
|
+
[operationResult]: nexus.HandlerStartOperationResult.async(token),
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A Nexus-aware Temporal Client for use inside {@link TemporalOperationHandler} implementations.
|
|
208
|
+
*
|
|
209
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
210
|
+
*/
|
|
211
|
+
export interface TemporalNexusClient {
|
|
212
|
+
/**
|
|
213
|
+
* The Temporal Client for the active Nexus Operation.
|
|
214
|
+
*
|
|
215
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
216
|
+
*/
|
|
217
|
+
readonly client: Client;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Starts a workflow run as the asynchronous backing operation for the current Nexus Operation.
|
|
221
|
+
*
|
|
222
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
223
|
+
*/
|
|
224
|
+
startWorkflow<T extends Workflow>(
|
|
225
|
+
workflowTypeOrFunc: string | T,
|
|
226
|
+
workflowOptions: WorkflowStartOptions<T>
|
|
227
|
+
): Promise<TemporalOperationResult<WorkflowResultType<T>>>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class TemporalNexusClientImpl implements TemporalNexusClient {
|
|
231
|
+
private asyncOperationStarted = false;
|
|
232
|
+
|
|
233
|
+
constructor(private readonly startOperationContext: TemporalStartOperationContext) {}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The Temporal Client for the active Nexus Operation.
|
|
237
|
+
*
|
|
238
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
239
|
+
*/
|
|
240
|
+
public get client(): Client {
|
|
241
|
+
return getClient();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Starts a workflow run as the asynchronous backing operation for the current Nexus Operation.
|
|
246
|
+
*
|
|
247
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
248
|
+
*/
|
|
249
|
+
public async startWorkflow<T extends Workflow>(
|
|
250
|
+
workflowTypeOrFunc: string | T,
|
|
251
|
+
workflowOptions: WorkflowStartOptions<T>
|
|
252
|
+
): Promise<TemporalOperationResult<WorkflowResultType<T>>> {
|
|
253
|
+
return await this.withAsyncOperationStartReservation(async () => {
|
|
254
|
+
const handle = await startWorkflow(this.startOperationContext, workflowTypeOrFunc, workflowOptions);
|
|
255
|
+
const { namespace } = getHandlerContext();
|
|
256
|
+
return TemporalOperationResult.async(generateWorkflowRunOperationToken(namespace, handle.workflowId));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async withAsyncOperationStartReservation<T>(fn: () => Promise<T>): Promise<T> {
|
|
261
|
+
if (this.asyncOperationStarted) {
|
|
262
|
+
throw new nexus.HandlerError(
|
|
263
|
+
'BAD_REQUEST',
|
|
264
|
+
'Only one async operation can be started per operation handler invocation. Use TemporalNexusClient.client for additional workflow interactions'
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.asyncOperationStarted = true;
|
|
269
|
+
try {
|
|
270
|
+
return await fn();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
this.asyncOperationStarted = false;
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A handler function for the {@link TemporalOperationHandler} constructor.
|
|
280
|
+
*
|
|
281
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
282
|
+
*/
|
|
283
|
+
export type TemporalOperationStartHandler<I, O> = (
|
|
284
|
+
ctx: TemporalStartOperationContext,
|
|
285
|
+
client: TemporalNexusClient,
|
|
286
|
+
input: I
|
|
287
|
+
) => Promise<TemporalOperationResult<O>>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Options passed to a {@link TemporalOperationHandlerOptions.cancelWorkflowRun} handler describing
|
|
291
|
+
* the workflow run to cancel.
|
|
292
|
+
*
|
|
293
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
294
|
+
*/
|
|
295
|
+
export interface CancelWorkflowRunOptions {
|
|
296
|
+
/**
|
|
297
|
+
* The ID of the workflow backing the Nexus Operation that is being canceled.
|
|
298
|
+
*/
|
|
299
|
+
readonly workflowId: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Options for customizing a {@link TemporalOperationHandler}.
|
|
304
|
+
*
|
|
305
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
306
|
+
*/
|
|
307
|
+
export interface TemporalOperationHandlerOptions {
|
|
308
|
+
cancelWorkflowRun?: (ctx: TemporalCancelOperationContext, options: CancelWorkflowRunOptions) => Promise<void>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* A Nexus Operation implementation for operations that interact with Temporal.
|
|
313
|
+
*
|
|
314
|
+
* @experimental Nexus support in Temporal SDK is experimental.
|
|
315
|
+
*/
|
|
316
|
+
export class TemporalOperationHandler<I, O> implements nexus.OperationHandler<I, O> {
|
|
317
|
+
private readonly startHandler: TemporalOperationStartHandler<I, O>;
|
|
318
|
+
private readonly cancelWorkflowRunHandler: NonNullable<TemporalOperationHandlerOptions['cancelWorkflowRun']>;
|
|
319
|
+
|
|
320
|
+
constructor(options: { start: TemporalOperationStartHandler<I, O> } & TemporalOperationHandlerOptions) {
|
|
321
|
+
this.startHandler = options.start;
|
|
322
|
+
this.cancelWorkflowRunHandler = options.cancelWorkflowRun ?? defaultCancelWorkflowRun;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async start(ctx: nexus.StartOperationContext, input: I): Promise<nexus.HandlerStartOperationResult<O>> {
|
|
326
|
+
const result = await this.startHandler(ctx, new TemporalNexusClientImpl(ctx), input);
|
|
327
|
+
return result[operationResult];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async cancel(ctx: nexus.CancelOperationContext, token: string): Promise<void> {
|
|
331
|
+
let opToken;
|
|
332
|
+
try {
|
|
333
|
+
opToken = loadOperationToken(token);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
throw new nexus.HandlerError(nexus.HandlerErrorType.BAD_REQUEST, 'invalid operation token', { cause: err });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
switch (opToken.t) {
|
|
339
|
+
case OperationTokenType.WORKFLOW_RUN:
|
|
340
|
+
try {
|
|
341
|
+
assertWorkflowRunOperationToken(opToken);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
throw new nexus.HandlerError(nexus.HandlerErrorType.BAD_REQUEST, 'invalid workflow run operation token', {
|
|
344
|
+
cause: err,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
await this.cancelWorkflowRunHandler(ctx, { workflowId: opToken.wid });
|
|
348
|
+
return;
|
|
349
|
+
default:
|
|
350
|
+
throw new nexus.HandlerError(
|
|
351
|
+
nexus.HandlerErrorType.BAD_REQUEST,
|
|
352
|
+
`Unsupported operation token type: ${opToken.t}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function defaultCancelWorkflowRun(_ctx: TemporalCancelOperationContext, options: CancelWorkflowRunOptions) {
|
|
359
|
+
await getClient().workflow.getHandle(options.workflowId).cancel();
|
|
360
|
+
}
|