@proompteng/temporal-bun-sdk 0.2.0 → 0.4.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.
Files changed (123) hide show
  1. package/README.md +141 -10
  2. package/dist/src/bin/replay-command.d.ts.map +1 -1
  3. package/dist/src/bin/replay-command.js +6 -2
  4. package/dist/src/bin/replay-command.js.map +1 -1
  5. package/dist/src/bin/temporal-bun.d.ts +1 -1
  6. package/dist/src/bin/temporal-bun.d.ts.map +1 -1
  7. package/dist/src/bin/temporal-bun.js +74 -0
  8. package/dist/src/bin/temporal-bun.js.map +1 -1
  9. package/dist/src/client/layer.d.ts +2 -2
  10. package/dist/src/client/layer.d.ts.map +1 -1
  11. package/dist/src/client/retries.d.ts.map +1 -1
  12. package/dist/src/client/retries.js +27 -3
  13. package/dist/src/client/retries.js.map +1 -1
  14. package/dist/src/client/serialization.d.ts +34 -2
  15. package/dist/src/client/serialization.d.ts.map +1 -1
  16. package/dist/src/client/serialization.js +78 -5
  17. package/dist/src/client/serialization.js.map +1 -1
  18. package/dist/src/client/types.d.ts +26 -0
  19. package/dist/src/client/types.d.ts.map +1 -1
  20. package/dist/src/client.d.ts +22 -6
  21. package/dist/src/client.d.ts.map +1 -1
  22. package/dist/src/client.js +488 -39
  23. package/dist/src/client.js.map +1 -1
  24. package/dist/src/common/payloads/codecs.d.ts +38 -0
  25. package/dist/src/common/payloads/codecs.d.ts.map +1 -0
  26. package/dist/src/common/payloads/codecs.js +174 -0
  27. package/dist/src/common/payloads/codecs.js.map +1 -0
  28. package/dist/src/common/payloads/converter.d.ts +52 -3
  29. package/dist/src/common/payloads/converter.d.ts.map +1 -1
  30. package/dist/src/common/payloads/converter.js +340 -2
  31. package/dist/src/common/payloads/converter.js.map +1 -1
  32. package/dist/src/common/payloads/failure.d.ts +5 -6
  33. package/dist/src/common/payloads/failure.d.ts.map +1 -1
  34. package/dist/src/common/payloads/failure.js +3 -52
  35. package/dist/src/common/payloads/failure.js.map +1 -1
  36. package/dist/src/common/payloads/index.d.ts +1 -0
  37. package/dist/src/common/payloads/index.d.ts.map +1 -1
  38. package/dist/src/common/payloads/index.js +1 -0
  39. package/dist/src/common/payloads/index.js.map +1 -1
  40. package/dist/src/config.d.ts +9 -0
  41. package/dist/src/config.d.ts.map +1 -1
  42. package/dist/src/config.js +62 -1
  43. package/dist/src/config.js.map +1 -1
  44. package/dist/src/index.d.ts +1 -1
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/index.js.map +1 -1
  47. package/dist/src/interceptors/client.d.ts +28 -0
  48. package/dist/src/interceptors/client.d.ts.map +1 -0
  49. package/dist/src/interceptors/client.js +169 -0
  50. package/dist/src/interceptors/client.js.map +1 -0
  51. package/dist/src/interceptors/types.d.ts +33 -0
  52. package/dist/src/interceptors/types.d.ts.map +1 -0
  53. package/dist/src/interceptors/types.js +44 -0
  54. package/dist/src/interceptors/types.js.map +1 -0
  55. package/dist/src/interceptors/worker.d.ts +22 -0
  56. package/dist/src/interceptors/worker.d.ts.map +1 -0
  57. package/dist/src/interceptors/worker.js +156 -0
  58. package/dist/src/interceptors/worker.js.map +1 -0
  59. package/dist/src/runtime/cli-layer.d.ts +4 -3
  60. package/dist/src/runtime/cli-layer.d.ts.map +1 -1
  61. package/dist/src/runtime/cli-layer.js +5 -2
  62. package/dist/src/runtime/cli-layer.js.map +1 -1
  63. package/dist/src/runtime/effect-layers.d.ts +9 -0
  64. package/dist/src/runtime/effect-layers.d.ts.map +1 -1
  65. package/dist/src/runtime/effect-layers.js +15 -0
  66. package/dist/src/runtime/effect-layers.js.map +1 -1
  67. package/dist/src/worker/concurrency.d.ts +8 -0
  68. package/dist/src/worker/concurrency.d.ts.map +1 -1
  69. package/dist/src/worker/concurrency.js +5 -9
  70. package/dist/src/worker/concurrency.js.map +1 -1
  71. package/dist/src/worker/runtime.d.ts +5 -0
  72. package/dist/src/worker/runtime.d.ts.map +1 -1
  73. package/dist/src/worker/runtime.js +509 -40
  74. package/dist/src/worker/runtime.js.map +1 -1
  75. package/dist/src/worker/sticky-cache.d.ts +5 -0
  76. package/dist/src/worker/sticky-cache.d.ts.map +1 -1
  77. package/dist/src/worker/sticky-cache.js +26 -0
  78. package/dist/src/worker/sticky-cache.js.map +1 -1
  79. package/dist/src/worker/update-protocol.d.ts +33 -0
  80. package/dist/src/worker/update-protocol.d.ts.map +1 -0
  81. package/dist/src/worker/update-protocol.js +243 -0
  82. package/dist/src/worker/update-protocol.js.map +1 -0
  83. package/dist/src/worker.js +1 -0
  84. package/dist/src/worker.js.map +1 -1
  85. package/dist/src/workflow/commands.d.ts +38 -2
  86. package/dist/src/workflow/commands.d.ts.map +1 -1
  87. package/dist/src/workflow/commands.js +153 -1
  88. package/dist/src/workflow/commands.js.map +1 -1
  89. package/dist/src/workflow/context.d.ts +111 -3
  90. package/dist/src/workflow/context.d.ts.map +1 -1
  91. package/dist/src/workflow/context.js +526 -6
  92. package/dist/src/workflow/context.js.map +1 -1
  93. package/dist/src/workflow/definition.d.ts +29 -3
  94. package/dist/src/workflow/definition.d.ts.map +1 -1
  95. package/dist/src/workflow/definition.js +30 -4
  96. package/dist/src/workflow/definition.js.map +1 -1
  97. package/dist/src/workflow/determinism.d.ts +64 -0
  98. package/dist/src/workflow/determinism.d.ts.map +1 -1
  99. package/dist/src/workflow/determinism.js +120 -3
  100. package/dist/src/workflow/determinism.js.map +1 -1
  101. package/dist/src/workflow/errors.d.ts +6 -0
  102. package/dist/src/workflow/errors.d.ts.map +1 -1
  103. package/dist/src/workflow/errors.js +12 -0
  104. package/dist/src/workflow/errors.js.map +1 -1
  105. package/dist/src/workflow/executor.d.ts +56 -0
  106. package/dist/src/workflow/executor.d.ts.map +1 -1
  107. package/dist/src/workflow/executor.js +300 -9
  108. package/dist/src/workflow/executor.js.map +1 -1
  109. package/dist/src/workflow/inbound.d.ts +84 -0
  110. package/dist/src/workflow/inbound.d.ts.map +1 -0
  111. package/dist/src/workflow/inbound.js +65 -0
  112. package/dist/src/workflow/inbound.js.map +1 -0
  113. package/dist/src/workflow/index.d.ts +1 -0
  114. package/dist/src/workflow/index.d.ts.map +1 -1
  115. package/dist/src/workflow/index.js +1 -0
  116. package/dist/src/workflow/index.js.map +1 -1
  117. package/dist/src/workflow/replay.d.ts +24 -2
  118. package/dist/src/workflow/replay.d.ts.map +1 -1
  119. package/dist/src/workflow/replay.js +679 -15
  120. package/dist/src/workflow/replay.js.map +1 -1
  121. package/dist/src/workflows/index.d.ts +1 -1
  122. package/dist/src/workflows/index.d.ts.map +1 -1
  123. package/package.json +1 -1
@@ -5,16 +5,19 @@ import { Context, Effect } from 'effect';
5
5
  import * as Option from 'effect/Option';
6
6
  import { createDefaultHeaders, mergeHeaders, normalizeMetadataHeaders } from './client/headers';
7
7
  import { makeDefaultInterceptorBuilder } from './client/interceptors';
8
- import { withTemporalRetry } from './client/retries';
9
- import { buildCancelRequest, buildQueryRequest, buildSignalRequest, buildSignalWithStartRequest, buildStartWorkflowRequest, buildTerminateRequest, computeSignalRequestId, createSignalRequestEntropy, decodeMemoAttributes, decodeSearchAttributes, encodeMemoAttributes, encodeSearchAttributes, } from './client/serialization';
8
+ import { buildCancelRequest, buildPollWorkflowUpdateRequest, buildQueryRequest, buildSignalRequest, buildSignalWithStartRequest, buildStartWorkflowRequest, buildTerminateRequest, buildUpdateWorkflowRequest, computeSignalRequestId, createSignalRequestEntropy, createUpdateRequestId, decodeMemoAttributes, decodeSearchAttributes, decodeUpdateOutcome, encodeMemoAttributes, encodeSearchAttributes, } from './client/serialization';
10
9
  import { buildTransportOptions, normalizeTemporalAddress } from './client/transport';
11
10
  import { createWorkflowHandle, } from './client/types';
12
- import { createDefaultDataConverter, decodePayloadsToValues } from './common/payloads';
11
+ import { buildCodecsFromConfig, createDefaultDataConverter, decodePayloadsToValues, } from './common/payloads';
13
12
  import { loadTemporalConfig } from './config';
13
+ import { makeDefaultClientInterceptors, runClientInterceptors, } from './interceptors/client';
14
14
  import { createObservabilityServices } from './observability';
15
- import { DescribeNamespaceRequestSchema, DescribeNamespaceResponseSchema, } from './proto/temporal/api/workflowservice/v1/request_response_pb';
15
+ import { WorkflowExecutionSchema, } from './proto/temporal/api/common/v1/message_pb';
16
+ import { UpdateWorkflowExecutionLifecycleStage } from './proto/temporal/api/enums/v1/update_pb';
17
+ import { HistoryEventFilterType } from './proto/temporal/api/enums/v1/workflow_pb';
18
+ import { DescribeNamespaceRequestSchema, DescribeNamespaceResponseSchema, GetWorkflowExecutionHistoryRequestSchema, } from './proto/temporal/api/workflowservice/v1/request_response_pb';
16
19
  import { WorkflowService } from './proto/temporal/api/workflowservice/v1/service_pb';
17
- import { LoggerService, MetricsExporterService, MetricsService, TemporalConfigService, WorkflowServiceClientService, } from './runtime/effect-layers';
20
+ import { DataConverterService, LoggerService, MetricsExporterService, MetricsService, TemporalConfigService, WorkflowServiceClientService, } from './runtime/effect-layers';
18
21
  export const temporalCallOptions = (options) => {
19
22
  const copy = { ...options };
20
23
  Object.defineProperty(copy, CALL_OPTIONS_MARKER, {
@@ -53,6 +56,38 @@ const describeError = (error) => {
53
56
  };
54
57
  const TLS_ERROR_CODE_PREFIXES = ['ERR_TLS_', 'ERR_SSL_'];
55
58
  const TLS_ERROR_MESSAGE_HINTS = [/handshake/i, /certificate/i, /secure tls/i, /ssl/i];
59
+ const isAbortLikeError = (error) => (error instanceof Error && error.name === 'AbortError') ||
60
+ (error instanceof ConnectError && error.code === Code.Canceled);
61
+ const normalizeUnknownError = (error) => {
62
+ const unwrap = (value) => {
63
+ if (!value || typeof value !== 'object') {
64
+ return value;
65
+ }
66
+ if (value instanceof TemporalTlsHandshakeError || value instanceof ConnectError) {
67
+ return value;
68
+ }
69
+ const candidate = value;
70
+ if (candidate.cause !== undefined) {
71
+ return unwrap(candidate.cause);
72
+ }
73
+ if (candidate.error !== undefined) {
74
+ return unwrap(candidate.error);
75
+ }
76
+ if (candidate._tag === 'UnknownException') {
77
+ return unwrap(candidate.cause ?? candidate.error ?? value);
78
+ }
79
+ const symbols = Object.getOwnPropertySymbols(value);
80
+ for (const symbol of symbols) {
81
+ const inner = value[symbol];
82
+ const unwrapped = unwrap(inner);
83
+ if (unwrapped !== inner) {
84
+ return unwrapped;
85
+ }
86
+ }
87
+ return value;
88
+ };
89
+ return unwrap(error);
90
+ };
56
91
  const isCallOptionsCandidate = (value) => {
57
92
  if (!value || typeof value !== 'object') {
58
93
  return false;
@@ -131,6 +166,12 @@ export const createTemporalClient = async (options = {}) => {
131
166
  if (!workflowService) {
132
167
  throw new Error('Temporal workflow service is not available');
133
168
  }
169
+ const dataConverter = options.dataConverter ??
170
+ createDefaultDataConverter({
171
+ payloadCodecs: buildCodecsFromConfig(config.payloadCodecs),
172
+ logger,
173
+ metricsRegistry,
174
+ });
134
175
  const effect = makeTemporalClientEffect({
135
176
  ...options,
136
177
  config,
@@ -139,9 +180,10 @@ export const createTemporalClient = async (options = {}) => {
139
180
  metricsExporter,
140
181
  workflowService,
141
182
  transport,
183
+ dataConverter,
142
184
  });
143
185
  try {
144
- return await Effect.runPromise(effect.pipe(Effect.provideService(TemporalConfigService, config), Effect.provideService(LoggerService, logger), Effect.provideService(MetricsService, metricsRegistry), Effect.provideService(MetricsExporterService, metricsExporter), Effect.provideService(WorkflowServiceClientService, workflowService)));
186
+ return await Effect.runPromise(effect.pipe(Effect.provideService(TemporalConfigService, config), Effect.provideService(LoggerService, logger), Effect.provideService(MetricsService, metricsRegistry), Effect.provideService(MetricsExporterService, metricsExporter), Effect.provideService(WorkflowServiceClientService, workflowService), Effect.provideService(DataConverterService, dataConverter)));
145
187
  }
146
188
  catch (error) {
147
189
  await createdTransport?.close?.();
@@ -153,11 +195,33 @@ export const makeTemporalClientEffect = (options = {}) => Effect.gen(function* (
153
195
  const namespace = options.namespace ?? config.namespace;
154
196
  const identity = options.identity ?? config.workerIdentity;
155
197
  const taskQueue = options.taskQueue ?? config.taskQueue;
156
- const dataConverter = options.dataConverter ?? createDefaultDataConverter();
157
- const initialHeaders = createDefaultHeaders(config.apiKey);
158
198
  const logger = options.logger ?? (yield* LoggerService);
159
199
  const metricsRegistry = options.metrics ?? (yield* MetricsService);
160
200
  const metricsExporter = options.metricsExporter ?? (yield* MetricsExporterService);
201
+ const contextualDataConverter = yield* Effect.contextWith((context) => Context.getOption(context, DataConverterService));
202
+ const dataConverter = options.dataConverter ??
203
+ Option.getOrUndefined(contextualDataConverter) ??
204
+ createDefaultDataConverter({
205
+ payloadCodecs: buildCodecsFromConfig(config.payloadCodecs),
206
+ logger,
207
+ metricsRegistry,
208
+ });
209
+ const initialHeaders = createDefaultHeaders(config.apiKey);
210
+ const tracingEnabled = options.tracingEnabled ?? config.tracingInterceptorsEnabled ?? false;
211
+ const clientInterceptorBuilder = options.clientInterceptorBuilder ?? {
212
+ build: (input) => makeDefaultClientInterceptors(input),
213
+ };
214
+ const defaultClientInterceptors = yield* clientInterceptorBuilder.build({
215
+ namespace,
216
+ taskQueue,
217
+ identity,
218
+ logger,
219
+ metricsRegistry,
220
+ metricsExporter,
221
+ retryPolicy: config.rpcRetryPolicy,
222
+ tracingEnabled,
223
+ });
224
+ const clientInterceptors = [...defaultClientInterceptors, ...(options.clientInterceptors ?? [])];
161
225
  const workflowServiceFromContext = yield* Effect.contextWith((context) => Context.getOption(context, WorkflowServiceClientService));
162
226
  let workflowService = options.workflowService ?? Option.getOrUndefined(workflowServiceFromContext);
163
227
  let transport = options.transport;
@@ -198,6 +262,7 @@ export const makeTemporalClientEffect = (options = {}) => Effect.gen(function* (
198
262
  logger,
199
263
  metrics: clientMetrics,
200
264
  metricsExporter,
265
+ clientInterceptors,
201
266
  });
202
267
  return { client, config };
203
268
  });
@@ -215,6 +280,9 @@ class TemporalClientImpl {
215
280
  #logger;
216
281
  #clientMetrics;
217
282
  #metricsExporter;
283
+ #clientInterceptors;
284
+ #pendingUpdateControllers = new Map();
285
+ #abortedUpdates = new Set();
218
286
  closed = false;
219
287
  headers;
220
288
  static async initMetrics(registry) {
@@ -238,6 +306,7 @@ class TemporalClientImpl {
238
306
  this.#logger = handles.logger;
239
307
  this.#clientMetrics = handles.metrics;
240
308
  this.#metricsExporter = handles.metricsExporter;
309
+ this.#clientInterceptors = handles.clientInterceptors;
241
310
  this.memo = {
242
311
  encode: (input) => encodeMemoAttributes(this.dataConverter, input),
243
312
  decode: (memo) => decodeMemoAttributes(this.dataConverter, memo),
@@ -253,12 +322,17 @@ class TemporalClientImpl {
253
322
  terminate: (handle, options, callOptions) => this.terminateWorkflow(handle, options, callOptions),
254
323
  cancel: (handle, callOptions) => this.cancelWorkflow(handle, callOptions),
255
324
  signalWithStart: (options, callOptions) => this.signalWithStart(options, callOptions),
325
+ update: (workflowHandle, updateOptions, callOptions) => this.updateWorkflow(workflowHandle, updateOptions, callOptions),
326
+ awaitUpdate: (updateHandle, options, callOptions) => this.awaitWorkflowUpdate(updateHandle, options, callOptions),
327
+ cancelUpdate: (updateHandle) => this.cancelWorkflowUpdate(updateHandle),
328
+ getUpdateHandle: (workflowHandle, updateId, firstExecutionRunId) => this.getWorkflowUpdateHandle(workflowHandle, updateId, firstExecutionRunId),
329
+ result: (handle, callOptions) => this.getWorkflowResult(handle, callOptions),
256
330
  };
257
331
  }
258
332
  async startWorkflow(options, callOptions) {
259
- return this.#instrumentOperation('startWorkflow', async () => {
333
+ const parsedOptions = sanitizeStartWorkflowOptions(options);
334
+ return this.#instrumentOperation('workflow.start', async () => {
260
335
  this.ensureOpen();
261
- const parsedOptions = sanitizeStartWorkflowOptions(options);
262
336
  const request = await buildStartWorkflowRequest({
263
337
  options: parsedOptions,
264
338
  defaults: {
@@ -273,12 +347,15 @@ class TemporalClientImpl {
273
347
  namespace: request.namespace,
274
348
  firstExecutionRunId: response.started ? response.runId : undefined,
275
349
  });
350
+ }, {
351
+ workflowId: parsedOptions.workflowId,
352
+ taskQueue: options.taskQueue ?? this.defaultTaskQueue,
276
353
  });
277
354
  }
278
355
  async signalWorkflow(handle, signalName, ...rawArgs) {
279
- return this.#instrumentOperation('signalWorkflow', async () => {
356
+ const resolvedHandle = resolveHandle(this.namespace, handle);
357
+ return this.#instrumentOperation('workflow.signal', async () => {
280
358
  this.ensureOpen();
281
- const resolvedHandle = resolveHandle(this.namespace, handle);
282
359
  const normalizedSignalName = ensureNonEmptyString(signalName, 'signalName');
283
360
  const { values, callOptions } = this.#splitArgsAndOptions(rawArgs);
284
361
  const identity = this.defaultIdentity;
@@ -300,39 +377,101 @@ class TemporalClientImpl {
300
377
  requestId,
301
378
  }, this.dataConverter);
302
379
  await this.executeRpc('signalWorkflow', (rpcOptions) => this.workflowService.signalWorkflowExecution(request, rpcOptions), callOptions);
303
- });
380
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId, taskQueue: this.defaultTaskQueue });
304
381
  }
305
382
  async queryWorkflow(handle, queryName, ...rawArgs) {
306
- return this.#instrumentOperation('queryWorkflow', async () => {
383
+ const resolvedHandle = resolveHandle(this.namespace, handle);
384
+ return this.#instrumentOperation('workflow.query', async () => {
307
385
  this.ensureOpen();
308
- const resolvedHandle = resolveHandle(this.namespace, handle);
309
386
  const { values, callOptions } = this.#splitArgsAndOptions(rawArgs);
310
- const request = await buildQueryRequest(resolvedHandle, queryName, values, this.dataConverter);
387
+ const request = await buildQueryRequest(resolvedHandle, queryName, values, this.dataConverter, {
388
+ rejectCondition: callOptions?.queryRejectCondition,
389
+ });
311
390
  const response = await this.executeRpc('queryWorkflow', (rpcOptions) => this.workflowService.queryWorkflow(request, rpcOptions), callOptions);
312
391
  return this.parseQueryResult(response);
313
- });
392
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId });
314
393
  }
315
394
  async terminateWorkflow(handle, options = {}, callOptions) {
316
- return this.#instrumentOperation('terminateWorkflow', async () => {
395
+ return this.#instrumentOperation('workflow.terminate', async () => {
317
396
  this.ensureOpen();
318
397
  const resolvedHandle = resolveHandle(this.namespace, handle);
319
398
  const parsedOptions = sanitizeTerminateWorkflowOptions(options);
320
399
  const request = await buildTerminateRequest(resolvedHandle, parsedOptions, this.dataConverter, this.defaultIdentity);
321
400
  await this.executeRpc('terminateWorkflow', (rpcOptions) => this.workflowService.terminateWorkflowExecution(request, rpcOptions), callOptions);
322
- });
401
+ }, { workflowId: handle.workflowId, runId: handle.runId });
323
402
  }
324
403
  async cancelWorkflow(handle, callOptions) {
325
- return this.#instrumentOperation('cancelWorkflow', async () => {
404
+ const resolvedHandle = resolveHandle(this.namespace, handle);
405
+ return this.#instrumentOperation('workflow.cancel', async () => {
326
406
  this.ensureOpen();
327
- const resolvedHandle = resolveHandle(this.namespace, handle);
328
407
  const request = buildCancelRequest(resolvedHandle, this.defaultIdentity);
329
408
  await this.executeRpc('cancelWorkflow', (rpcOptions) => this.workflowService.requestCancelWorkflowExecution(request, rpcOptions), callOptions);
330
- });
409
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId });
410
+ }
411
+ async getWorkflowResult(handle, callOptions) {
412
+ let resolvedHandle = resolveHandle(this.namespace, handle);
413
+ return this.#instrumentOperation('workflow.result', async () => {
414
+ this.ensureOpen();
415
+ while (true) {
416
+ const execution = create(WorkflowExecutionSchema, {
417
+ workflowId: resolvedHandle.workflowId,
418
+ ...(resolvedHandle.runId ? { runId: resolvedHandle.runId } : {}),
419
+ });
420
+ const request = create(GetWorkflowExecutionHistoryRequestSchema, {
421
+ namespace: resolvedHandle.namespace ?? this.namespace,
422
+ execution,
423
+ maximumPageSize: 1,
424
+ historyEventFilterType: HistoryEventFilterType.CLOSE_EVENT,
425
+ waitNewEvent: true,
426
+ skipArchival: true,
427
+ });
428
+ const response = await this.executeRpc('getWorkflowExecutionHistory', (rpcOptions) => this.workflowService.getWorkflowExecutionHistory(request, rpcOptions), callOptions);
429
+ const closeEvent = this.#extractCloseEvent(response);
430
+ if (!closeEvent || !closeEvent.attributes) {
431
+ throw new Error('Workflow close event not found');
432
+ }
433
+ const attributes = closeEvent.attributes;
434
+ switch (attributes.case) {
435
+ case 'workflowExecutionContinuedAsNewEventAttributes': {
436
+ const nextRunId = attributes.value.newExecutionRunId;
437
+ if (!nextRunId) {
438
+ throw new Error('Continue-as-new event missing newExecutionRunId');
439
+ }
440
+ resolvedHandle = { ...resolvedHandle, runId: nextRunId };
441
+ continue;
442
+ }
443
+ case 'workflowExecutionCompletedEventAttributes': {
444
+ const payloads = attributes.value.result?.payloads ?? [];
445
+ const decoded = await this.dataConverter.fromPayloads(payloads);
446
+ return (decoded.length <= 1 ? decoded[0] : decoded) ?? undefined;
447
+ }
448
+ case 'workflowExecutionFailedEventAttributes': {
449
+ const failure = await this.dataConverter.decodeFailurePayloads(attributes.value.failure);
450
+ const error = await this.dataConverter.failureToError(failure);
451
+ throw error ?? new Error('Workflow failed');
452
+ }
453
+ case 'workflowExecutionTimedOutEventAttributes':
454
+ throw new Error('Workflow timed out');
455
+ case 'workflowExecutionCanceledEventAttributes': {
456
+ const details = attributes.value.details?.payloads ?? [];
457
+ const decoded = await this.dataConverter.fromPayloads(details);
458
+ const detail = decoded.length <= 1 ? decoded[0] : decoded;
459
+ throw new Error(detail ? `Workflow canceled: ${JSON.stringify(detail)}` : 'Workflow canceled without details');
460
+ }
461
+ case 'workflowExecutionTerminatedEventAttributes': {
462
+ const reason = attributes.value.reason;
463
+ throw new Error(reason ? `Workflow terminated: ${reason}` : 'Workflow terminated');
464
+ }
465
+ default:
466
+ throw new Error(`Unsupported workflow close event type: ${attributes.case}`);
467
+ }
468
+ }
469
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId });
331
470
  }
332
471
  async signalWithStart(options, callOptions) {
333
- return this.#instrumentOperation('signalWithStart', async () => {
472
+ const startOptions = sanitizeStartWorkflowOptions(options);
473
+ return this.#instrumentOperation('workflow.signalWithStart', async () => {
334
474
  this.ensureOpen();
335
- const startOptions = sanitizeStartWorkflowOptions(options);
336
475
  const signalName = ensureNonEmptyString(options.signalName, 'signalName');
337
476
  const signalArgs = options.signalArgs ?? [];
338
477
  if (!Array.isArray(signalArgs)) {
@@ -356,17 +495,144 @@ class TemporalClientImpl {
356
495
  namespace: request.namespace,
357
496
  firstExecutionRunId: response.started ? response.runId : undefined,
358
497
  });
498
+ }, { workflowId: startOptions.workflowId });
499
+ }
500
+ async updateWorkflow(handle, options, callOptions) {
501
+ this.ensureOpen();
502
+ const resolvedHandle = resolveHandle(this.namespace, handle);
503
+ const parsedOptions = sanitizeWorkflowUpdateOptions(options);
504
+ const updateId = parsedOptions.updateId ?? createUpdateRequestId();
505
+ const waitStage = this.#stageToProto(parsedOptions.waitForStage);
506
+ return this.#instrumentOperation('workflow.update', async () => {
507
+ const updateKey = this.#makeUpdateKey(resolvedHandle, updateId);
508
+ const { options: mergedCallOptions, controller, cleanup, } = this.#prepareUpdateCallOptions(updateKey, callOptions);
509
+ try {
510
+ const request = await buildUpdateWorkflowRequest({
511
+ handle: resolvedHandle,
512
+ namespace: this.namespace,
513
+ identity: this.defaultIdentity,
514
+ updateId,
515
+ updateName: parsedOptions.updateName,
516
+ args: parsedOptions.args,
517
+ headers: parsedOptions.headers,
518
+ waitStage,
519
+ firstExecutionRunId: parsedOptions.firstExecutionRunId,
520
+ }, this.dataConverter);
521
+ let response = await this.executeRpc('updateWorkflowExecution', (rpcOptions) => this.workflowService.updateWorkflowExecution(request, rpcOptions), mergedCallOptions);
522
+ while ((response.stage ?? UpdateWorkflowExecutionLifecycleStage.UNSPECIFIED) < waitStage) {
523
+ response = await this.executeRpc('updateWorkflowExecution', (rpcOptions) => this.workflowService.updateWorkflowExecution(request, rpcOptions), mergedCallOptions);
524
+ }
525
+ const runId = response.updateRef?.workflowExecution?.runId || resolvedHandle.runId;
526
+ const updateHandle = this.#createWorkflowUpdateHandle(resolvedHandle, {
527
+ updateId,
528
+ runId,
529
+ firstExecutionRunId: parsedOptions.firstExecutionRunId,
530
+ });
531
+ const stage = this.#stageFromProto(response.stage);
532
+ const outcome = await decodeUpdateOutcome(this.dataConverter, response.outcome);
533
+ return {
534
+ handle: updateHandle,
535
+ stage,
536
+ outcome,
537
+ };
538
+ }
539
+ finally {
540
+ this.#releaseUpdateController(updateKey, controller);
541
+ cleanup?.();
542
+ }
543
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId, updateId });
544
+ }
545
+ async awaitWorkflowUpdate(handle, options = {}, callOptions) {
546
+ this.ensureOpen();
547
+ const resolvedHandle = resolveHandle(this.namespace, handle);
548
+ const updateId = ensureNonEmptyString(handle.updateId, 'updateId');
549
+ const waitStage = this.#stageToProto(options.waitForStage ?? 'completed');
550
+ const firstExecutionRunIdOverride = ensureOptionalTrimmedString(options.firstExecutionRunId, 'firstExecutionRunId', 1);
551
+ const pollHandle = {
552
+ workflowId: resolvedHandle.workflowId,
553
+ namespace: resolvedHandle.namespace,
554
+ runId: resolvedHandle.runId,
555
+ firstExecutionRunId: firstExecutionRunIdOverride ?? resolvedHandle.firstExecutionRunId,
556
+ };
557
+ return this.#instrumentOperation('workflow.awaitUpdate', async () => {
558
+ const request = buildPollWorkflowUpdateRequest({
559
+ handle: pollHandle,
560
+ namespace: this.namespace,
561
+ identity: this.defaultIdentity,
562
+ updateId,
563
+ waitStage,
564
+ });
565
+ const updateKey = this.#makeUpdateKey(resolvedHandle, updateId);
566
+ const { options: mergedCallOptions, controller, cleanup, } = this.#prepareUpdateCallOptions(updateKey, callOptions);
567
+ const throwIfAborted = () => {
568
+ if (controller.signal.aborted || this.#abortedUpdates.has(updateKey)) {
569
+ this.#abortedUpdates.delete(updateKey);
570
+ const abortError = new Error('Workflow update polling aborted');
571
+ abortError.name = 'AbortError';
572
+ throw abortError;
573
+ }
574
+ };
575
+ throwIfAborted();
576
+ try {
577
+ const response = await this.executeRpc('pollWorkflowExecutionUpdate', (rpcOptions) => this.workflowService.pollWorkflowExecutionUpdate(request, rpcOptions), mergedCallOptions);
578
+ const stage = this.#stageFromProto(response.stage);
579
+ const runId = response.updateRef?.workflowExecution?.runId || resolvedHandle.runId;
580
+ const updateHandle = this.#createWorkflowUpdateHandle(resolvedHandle, {
581
+ updateId,
582
+ runId,
583
+ firstExecutionRunId: pollHandle.firstExecutionRunId,
584
+ });
585
+ const outcome = await decodeUpdateOutcome(this.dataConverter, response.outcome);
586
+ return {
587
+ handle: updateHandle,
588
+ stage,
589
+ outcome,
590
+ };
591
+ }
592
+ catch (error) {
593
+ throwIfAborted();
594
+ throw error;
595
+ }
596
+ finally {
597
+ this.#abortedUpdates.delete(updateKey);
598
+ this.#releaseUpdateController(updateKey, controller);
599
+ cleanup?.();
600
+ }
601
+ }, { workflowId: resolvedHandle.workflowId, runId: resolvedHandle.runId, updateId });
602
+ }
603
+ getWorkflowUpdateHandle(handle, updateId, firstExecutionRunId) {
604
+ const resolvedHandle = resolveHandle(this.namespace, handle);
605
+ const normalizedUpdateId = ensureNonEmptyString(updateId, 'updateId');
606
+ const normalizedFirstExecution = ensureOptionalTrimmedString(firstExecutionRunId, 'firstExecutionRunId', 1);
607
+ return this.#createWorkflowUpdateHandle(resolvedHandle, {
608
+ updateId: normalizedUpdateId,
609
+ firstExecutionRunId: normalizedFirstExecution,
359
610
  });
360
611
  }
612
+ async cancelWorkflowUpdate(handle) {
613
+ return this.#instrumentOperation('workflow.update', async () => {
614
+ this.ensureOpen();
615
+ const resolvedHandle = resolveHandle(this.namespace, handle);
616
+ const updateId = ensureNonEmptyString(handle.updateId, 'updateId');
617
+ const updateKey = this.#makeUpdateKey(resolvedHandle, updateId);
618
+ const aborted = this.#abortUpdateControllers(updateKey);
619
+ if (!aborted) {
620
+ this.#log('debug', 'no pending workflow update operation to cancel', {
621
+ workflowId: resolvedHandle.workflowId,
622
+ updateId,
623
+ });
624
+ }
625
+ }, { workflowId: handle.workflowId, runId: handle.runId, updateId: handle.updateId });
626
+ }
361
627
  async describeNamespace(targetNamespace, callOptions) {
362
- return this.#instrumentOperation('describeNamespace', async () => {
628
+ return this.#instrumentOperation('workflow.describe', async () => {
363
629
  this.ensureOpen();
364
630
  const request = create(DescribeNamespaceRequestSchema, {
365
631
  namespace: targetNamespace ?? this.namespace,
366
632
  });
367
633
  const response = await this.executeRpc('describeNamespace', (rpcOptions) => this.workflowService.describeNamespace(request, rpcOptions), callOptions);
368
634
  return toBinary(DescribeNamespaceResponseSchema, response);
369
- });
635
+ }, { namespace: targetNamespace ?? this.namespace });
370
636
  }
371
637
  async updateHeaders(headers) {
372
638
  if (this.closed) {
@@ -404,24 +670,66 @@ class TemporalClientImpl {
404
670
  void Effect.runPromise(this.#clientMetrics.operationErrors.inc());
405
671
  }
406
672
  }
407
- async #instrumentOperation(operation, action) {
673
+ async #instrumentOperation(operation, action, metadata = {}) {
674
+ const context = {
675
+ kind: operation,
676
+ namespace: this.namespace,
677
+ taskQueue: metadata.taskQueue ?? this.defaultTaskQueue,
678
+ identity: this.defaultIdentity,
679
+ workflowId: metadata.workflowId,
680
+ runId: metadata.runId,
681
+ updateId: metadata.updateId,
682
+ metadata,
683
+ };
408
684
  const start = Date.now();
685
+ const effect = runClientInterceptors(this.#clientInterceptors, context, () => Effect.tryPromise(action));
409
686
  try {
410
- const result = await action();
687
+ const result = await Effect.runPromise(effect);
411
688
  this.#log('debug', `temporal client ${operation} succeeded`, {
412
689
  operation,
413
690
  namespace: this.namespace,
691
+ ...metadata,
414
692
  });
415
693
  this.#recordMetrics(Date.now() - start, false);
416
694
  return result;
417
695
  }
418
696
  catch (error) {
697
+ const normalized = normalizeUnknownError(error);
698
+ const finalError = normalized instanceof Error &&
699
+ normalized.message.toLowerCase().includes('unknown error occurred in effect.trypromise')
700
+ ? (normalized.cause ??
701
+ normalized.error ??
702
+ normalized)
703
+ : normalized;
704
+ const message = finalError instanceof Error ? finalError.message.toLowerCase() : '';
705
+ const isUnknownPollAbort = operation === 'workflow.awaitUpdate' && message.includes('unknown error occurred in effect.trypromise');
706
+ if (isAbortLikeError(normalized)) {
707
+ this.#log('warn', `temporal client ${operation} aborted`, {
708
+ operation,
709
+ error: describeError(normalized),
710
+ ...metadata,
711
+ });
712
+ this.#recordMetrics(Date.now() - start, true);
713
+ throw finalError;
714
+ }
715
+ if (isUnknownPollAbort) {
716
+ const abortError = new Error('Workflow update polling aborted');
717
+ abortError.name = 'AbortError';
718
+ this.#log('warn', `temporal client ${operation} aborted`, {
719
+ operation,
720
+ error: describeError(abortError),
721
+ ...metadata,
722
+ });
723
+ this.#recordMetrics(Date.now() - start, true);
724
+ throw abortError;
725
+ }
419
726
  this.#log('error', `temporal client ${operation} failed`, {
420
727
  operation,
421
- error: describeError(error),
728
+ error: describeError(finalError),
729
+ ...metadata,
422
730
  });
423
731
  this.#recordMetrics(Date.now() - start, true);
424
- throw error;
732
+ throw finalError;
425
733
  }
426
734
  }
427
735
  #log(level, message, fields) {
@@ -447,6 +755,89 @@ class TemporalClientImpl {
447
755
  }
448
756
  return { values: args };
449
757
  }
758
+ #prepareUpdateCallOptions(key, callOptions) {
759
+ const controller = new AbortController();
760
+ let cleanup;
761
+ if (callOptions?.signal) {
762
+ if (callOptions.signal.aborted) {
763
+ controller.abort();
764
+ }
765
+ else {
766
+ const onAbort = () => controller.abort();
767
+ callOptions.signal.addEventListener('abort', onAbort, { once: true });
768
+ cleanup = () => callOptions.signal?.removeEventListener('abort', onAbort);
769
+ }
770
+ }
771
+ this.#registerUpdateController(key, controller);
772
+ const overrides = {
773
+ ...(callOptions ? { ...callOptions } : {}),
774
+ signal: controller.signal,
775
+ };
776
+ return { options: overrides, controller, cleanup };
777
+ }
778
+ #registerUpdateController(key, controller) {
779
+ this.#abortedUpdates.delete(key);
780
+ const current = this.#pendingUpdateControllers.get(key);
781
+ if (current) {
782
+ current.add(controller);
783
+ return;
784
+ }
785
+ this.#pendingUpdateControllers.set(key, new Set([controller]));
786
+ }
787
+ #releaseUpdateController(key, controller) {
788
+ const current = this.#pendingUpdateControllers.get(key);
789
+ if (!current) {
790
+ return;
791
+ }
792
+ current.delete(controller);
793
+ if (current.size === 0) {
794
+ this.#pendingUpdateControllers.delete(key);
795
+ }
796
+ }
797
+ #abortUpdateControllers(key) {
798
+ const current = this.#pendingUpdateControllers.get(key);
799
+ if (!current || current.size === 0) {
800
+ this.#abortedUpdates.add(key);
801
+ return false;
802
+ }
803
+ for (const controller of current) {
804
+ controller.abort();
805
+ }
806
+ this.#pendingUpdateControllers.delete(key);
807
+ this.#abortedUpdates.add(key);
808
+ return true;
809
+ }
810
+ #makeUpdateKey(handle, updateId) {
811
+ const namespace = handle.namespace ?? this.namespace;
812
+ const runId = handle.runId ?? '';
813
+ return `${namespace}::${handle.workflowId}::${runId}::${updateId}`;
814
+ }
815
+ #createWorkflowUpdateHandle(handle, overrides) {
816
+ return {
817
+ workflowId: handle.workflowId,
818
+ namespace: handle.namespace,
819
+ runId: overrides.runId ?? handle.runId,
820
+ firstExecutionRunId: overrides.firstExecutionRunId ?? handle.firstExecutionRunId,
821
+ updateId: overrides.updateId,
822
+ };
823
+ }
824
+ #stageToProto(stage, minimum = UpdateWorkflowExecutionLifecycleStage.ADMITTED) {
825
+ const normalized = ensureWorkflowUpdateStage(stage);
826
+ const protoStage = WORKFLOW_UPDATE_STAGE_TO_PROTO[normalized];
827
+ return protoStage >= minimum ? protoStage : minimum;
828
+ }
829
+ #stageFromProto(stage) {
830
+ if (stage === UpdateWorkflowExecutionLifecycleStage.ADMITTED) {
831
+ return 'admitted';
832
+ }
833
+ if (stage === UpdateWorkflowExecutionLifecycleStage.ACCEPTED) {
834
+ return 'accepted';
835
+ }
836
+ if (stage === UpdateWorkflowExecutionLifecycleStage.COMPLETED) {
837
+ return 'completed';
838
+ }
839
+ return 'unspecified';
840
+ }
450
841
  #mergeRetryPolicy(overrides) {
451
842
  const base = this.config.rpcRetryPolicy;
452
843
  if (!overrides) {
@@ -490,6 +881,13 @@ class TemporalClientImpl {
490
881
  retryableStatusCodes,
491
882
  };
492
883
  }
884
+ #extractCloseEvent(response) {
885
+ const events = response?.history?.events ?? [];
886
+ if (events.length === 0) {
887
+ return undefined;
888
+ }
889
+ return events[events.length - 1];
890
+ }
493
891
  #buildCallContext(overrides) {
494
892
  const userHeaders = overrides?.headers ? normalizeMetadataHeaders(overrides.headers) : undefined;
495
893
  const mergedHeaders = userHeaders ? mergeHeaders(this.headers, userHeaders) : { ...this.headers };
@@ -502,29 +900,37 @@ class TemporalClientImpl {
502
900
  signal,
503
901
  }),
504
902
  retryPolicy: this.#mergeRetryPolicy(overrides?.retryPolicy),
903
+ headers: mergedHeaders,
505
904
  };
506
905
  }
507
906
  async executeRpc(operation, rpc, overrides) {
508
- const { create, retryPolicy } = this.#buildCallContext(overrides);
509
- let attempt = 0;
510
- const effect = Effect.tryPromise({
907
+ const { create, retryPolicy, headers } = this.#buildCallContext(overrides);
908
+ const interceptorContext = {
909
+ kind: 'rpc',
910
+ namespace: this.namespace,
911
+ taskQueue: this.defaultTaskQueue,
912
+ identity: this.defaultIdentity,
913
+ headers,
914
+ metadata: { retryPolicy },
915
+ };
916
+ const baseEffect = () => Effect.tryPromise({
511
917
  try: () => {
512
- attempt += 1;
918
+ interceptorContext.attempt = (interceptorContext.attempt ?? 0) + 1;
513
919
  return rpc(create());
514
920
  },
515
921
  catch: (error) => wrapRpcError(error),
516
922
  }).pipe(Effect.tapError((error) => Effect.sync(() => {
517
923
  this.#log('warn', `temporal rpc ${operation} attempt failed`, {
518
924
  operation,
519
- attempt,
925
+ attempt: interceptorContext.attempt,
520
926
  error: describeError(error),
521
927
  });
522
928
  })));
523
- const result = await Effect.runPromise(withTemporalRetry(effect, retryPolicy));
524
- if (attempt > 1) {
525
- this.#log('info', `temporal rpc ${operation} succeeded after ${attempt} attempts`, {
929
+ const result = await Effect.runPromise(runClientInterceptors(this.#clientInterceptors, interceptorContext, baseEffect));
930
+ if ((interceptorContext.attempt ?? 1) > 1) {
931
+ this.#log('info', `temporal rpc ${operation} succeeded after ${interceptorContext.attempt} attempts`, {
526
932
  operation,
527
- attempts: attempt,
933
+ attempts: interceptorContext.attempt,
528
934
  });
529
935
  }
530
936
  return result;
@@ -650,6 +1056,49 @@ const sanitizeTerminateWorkflowOptions = (options = {}) => {
650
1056
  firstExecutionRunId: ensureOptionalTrimmedString(options.firstExecutionRunId, 'firstExecutionRunId', 1),
651
1057
  };
652
1058
  };
1059
+ const sanitizeWorkflowUpdateOptions = (options) => {
1060
+ if (!options || typeof options !== 'object') {
1061
+ throw new Error('Workflow update options must be provided');
1062
+ }
1063
+ const updateName = ensureNonEmptyString(options.updateName, 'updateName');
1064
+ const args = options.args ?? [];
1065
+ if (!Array.isArray(args)) {
1066
+ throw new Error('update args must be an array when provided');
1067
+ }
1068
+ const waitForStage = ensureWorkflowUpdateStage(options.waitForStage);
1069
+ const updateId = ensureOptionalTrimmedString(options.updateId, 'updateId', 1);
1070
+ const firstExecutionRunId = ensureOptionalTrimmedString(options.firstExecutionRunId, 'firstExecutionRunId', 1);
1071
+ return {
1072
+ updateName,
1073
+ args,
1074
+ headers: options.headers,
1075
+ updateId,
1076
+ waitForStage,
1077
+ firstExecutionRunId,
1078
+ };
1079
+ };
1080
+ const WORKFLOW_UPDATE_STAGE_VALUES = new Set([
1081
+ 'unspecified',
1082
+ 'admitted',
1083
+ 'accepted',
1084
+ 'completed',
1085
+ ]);
1086
+ const DEFAULT_WORKFLOW_UPDATE_STAGE = 'accepted';
1087
+ const WORKFLOW_UPDATE_STAGE_TO_PROTO = {
1088
+ unspecified: UpdateWorkflowExecutionLifecycleStage.UNSPECIFIED,
1089
+ admitted: UpdateWorkflowExecutionLifecycleStage.ADMITTED,
1090
+ accepted: UpdateWorkflowExecutionLifecycleStage.ACCEPTED,
1091
+ completed: UpdateWorkflowExecutionLifecycleStage.COMPLETED,
1092
+ };
1093
+ const ensureWorkflowUpdateStage = (stage) => {
1094
+ if (!stage) {
1095
+ return DEFAULT_WORKFLOW_UPDATE_STAGE;
1096
+ }
1097
+ if (!WORKFLOW_UPDATE_STAGE_VALUES.has(stage)) {
1098
+ throw new Error('waitForStage must be one of unspecified, admitted, accepted, or completed');
1099
+ }
1100
+ return stage;
1101
+ };
653
1102
  const resolveHandle = (defaultNamespace, handle) => {
654
1103
  const workflowId = ensureNonEmptyString(handle.workflowId, 'workflowId');
655
1104
  const namespace = ensureOptionalTrimmedString(handle.namespace, 'namespace', 1) ?? defaultNamespace;