@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 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
@@ -96,7 +96,6 @@ function operationInfo() {
96
96
  return {
97
97
  namespace: ctx.namespace,
98
98
  taskQueue: ctx.taskQueue,
99
- endpoint: ctx.endpoint,
100
99
  };
101
100
  }
102
101
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":";;;AAaA,8CAMC;AAkGD,8BAEC;AASD,sCAMC;AAtID,uDAAqD;AAIrD,oGAAoG;AAEpG,0EAA0E;AAC1E,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACjF,IAAI,CAAE,UAAkB,CAAC,uBAAuB,CAAC,EAAE,CAAC;IACjD,UAAkB,CAAC,uBAAuB,CAAC,GAAG,IAAI,oCAAiB,EAAkB,CAAC;AACzF,CAAC;AACY,QAAA,iBAAiB,GAAuC,UAAkB,CAAC,uBAAuB,CAAC,CAAC;AAEjH,SAAgB,iBAAiB;IAC/B,MAAM,GAAG,GAAG,yBAAiB,CAAC,QAAQ,EAAE,CAAC;IACzC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,cAAc,CAAC,gCAAgC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAiCD,oGAAoG;AAEpG;;;;;;;;;;;;GAYG;AACU,QAAA,GAAG,GAAW;IACzB,GAAG,CAAC,KAAe,EAAE,OAAe,EAAE,IAAkB;QACtD,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,CAAC,OAAe,EAAE,IAAkB;QACvC,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,KAAK,CAAC,OAAe,EAAE,IAAkB;QACvC,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,OAAe,EAAE,IAAkB;QACtC,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,CAAC,OAAe,EAAE,IAAkB;QACtC,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACrD,CAAC;IACD,KAAK,CAAC,OAAe,EAAE,IAAkB;QACvC,OAAO,iBAAiB,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACtD,CAAC;CACF,CAAC;AAEF;;;;;;;GAOG;AACU,QAAA,WAAW,GAAgB;IACtC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW;QACnC,OAAO,iBAAiB,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAC5E,CAAC;IACD,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,EAAE,IAAI,EAAE,WAAW;QACxD,OAAO,iBAAiB,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IACzF,CAAC;IACD,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,EAAE,IAAI,EAAE,WAAW;QACpD,OAAO,iBAAiB,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IACrF,CAAC;IACD,QAAQ,CAAC,IAAI;QACX,OAAO,iBAAiB,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC;CACF,CAAC;AAEF;;;;;GAKG;AACH,SAAgB,SAAS;IACvB,OAAO,iBAAiB,EAAE,CAAC,MAAM,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,aAAa;IAC3B,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,OAAO;QACL,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/nexus",
3
- "version": "1.17.2",
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/client": "1.17.2",
27
- "@temporalio/common": "1.17.2",
28
- "@temporalio/proto": "1.17.2"
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,
@@ -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 convertNexusLinkToWorkflowEventLink(link: NexusLink): WorkflowEventLink {
44
- if (link.url.protocol !== 'temporal:') {
45
- throw new TypeError(`Invalid URL scheme: ${link.url}, expected 'temporal:', got '${link.url.protocol}'`);
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
- * OperationTokenType is used to identify the type of Operation token.
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 WorkflowRunOperationToken {
7
+ export interface OperationToken {
9
8
  /**
10
- * Version of the token, by default we assume we're on version 1, this field is not emitted as part of the output,
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. Must be OPERATION_TOKEN_TYPE_WORKFLOW_RUN.
15
+ * Type of the Operation.
17
16
  */
18
17
  t: OperationTokenType;
19
18
 
20
19
  /**
21
- * Namespace of the workflow.
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
- type OperationTokenType = (typeof OperationTokenType)[keyof typeof OperationTokenType];
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 a workflow run Operation token.
71
+ * Load and validate the common fields of an Operation token.
54
72
  */
55
- export function loadWorkflowRunOperationToken(data: string): WorkflowRunOperationToken {
73
+ export function loadOperationToken(data: string): OperationToken {
56
74
  if (!data) {
57
- throw new TypeError('invalid workflow run token: token is empty');
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: WorkflowRunOperationToken;
85
+ let token: OperationToken;
68
86
  try {
69
87
  token = JSON.parse(decoded);
70
88
  } catch (err) {
71
- throw new TypeError('failed to unmarshal workflow run Operation token', { cause: err });
89
+ throw new TypeError('failed to unmarshal Operation token', { cause: err });
72
90
  }
73
91
 
74
- if (token.t !== OperationTokenType.WORKFLOW_RUN) {
75
- throw new TypeError(`invalid workflow token type: ${token.t}, expected: ${OperationTokenType.WORKFLOW_RUN}`);
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 workflow run token: "v" field should not be present');
96
+ throw new TypeError('invalid operation token: "v" field should not be present');
79
97
  }
80
- if (!token.wid) {
81
- throw new TypeError('invalid workflow run token: missing workflow ID (wid)');
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');
@@ -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 { generateWorkflowRunOperationToken, loadWorkflowRunOperationToken } from './token';
9
- import { convertNexusLinkToWorkflowEventLink, convertWorkflowEventLinkToNexusLink } from './link-converter';
10
- import { getClient, getHandlerContext, log } from './context';
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: ctx.callbackHeaders },
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?.workflowEvent != null) {
133
+ if (internalOptions.backLink != null) {
117
134
  try {
118
- ctx.outboundLinks.push(convertWorkflowEventLinkToNexusLink(internalOptions.backLink.workflowEvent));
135
+ ctx.outboundLinks.push(convertTemporalLinkToNexusLink(internalOptions.backLink));
119
136
  } catch (error) {
120
- log.warn('failed to convert Workflow event link to Nexus link', { error });
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
+ }