@superdurable/dex 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Dex SDK for TypeScript
2
2
 
3
- ## Pending Channel messages
3
+ ## Flow state I/O
4
4
 
5
- `Client.getChannelMessages` returns typed `ChannelMessage<T>` envelopes in FIFO
6
- order. `Client.deleteChannelMessage` deletes a still-pending ID and rejects with
7
- `ChannelMessageNotFoundError` after consumption or another deletion.
5
+ Applications read and write Flow state through typed RPCs. The Client does not
6
+ expose direct Attribute reads or writes, Channel publication, or pending-message
7
+ mutation. This keeps each external state transition behind a Flow-owned method.
8
8
 
9
9
  An RPC can stage `channel.delete(context, messageId)`. Set the decorator option
10
10
  `isTransactional: true` when a missing ID must abort all other RPC writes.
@@ -221,6 +221,17 @@ and `getAllInstanceKeys` include buffered sets and deletes. The matching
221
221
  instances. Keys are decoded and sorted. Use
222
222
  `forceCompleteIfChannelsEmpty(...)` for conditional completion.
223
223
 
224
+ Request IDs are optional for both durable waits. When omitted, the server
225
+ derives a namespaced stable ID from the Step execution or Attribute condition,
226
+ such as `wait-for-attribute:myInt>10`. Reuse an override only for the same
227
+ logical wait. The Client automatically reattaches transport long polls. If an
228
+ earlier Update with that ID exhausted its handler budget, the server appends an
229
+ increasing `-N` suffix and starts a new Update. `maximumWaitTimeMs` is optional
230
+ and is the total handler budget across reattachments; omit it or use zero to
231
+ wait indefinitely. A positive budget expiry throws `WaitHandlerTimeoutError`.
232
+ An abandoned infinite wait remains accepted and counts against Temporal's
233
+ in-flight Update limit until it completes or the Flow closes.
234
+
224
235
  ### Async handlers
225
236
 
226
237
  `Step.execute`, `Step.waitFor`, and RPC methods may be `async` and return a
@@ -299,6 +310,14 @@ must be non-empty, may contain `#`, and may repeat; every write appends a new
299
310
  message. `Client.readStream` returns it as `StreamMessage.source`. Step writes
300
311
  use `#<stepExecutionID>` as source metadata.
301
312
 
313
+ `Client.readStream` moves forward one message at a time and can long-poll.
314
+ `Client.listStreamMessages` returns a non-blocking newest-first retained page.
315
+ Pass the registered typed `Stream`, a positive page size, and an empty token for
316
+ the first page. Pass `StreamMessagesPage.nextPageToken` unchanged to read older
317
+ messages until it is empty. The server caps page size at 1000 by default. A
318
+ trimmed anchor returns an empty page, and newer concurrent writes do not enter
319
+ an existing older-page chain.
320
+
302
321
  Step durability defaults to the Flow configuration and then sync. Regular
303
322
  attempts default to two hours, heartbeat timeout defaults to one minute, and
304
323
  retry total duration defaults to four hours. The SDK accepts non-negative
@@ -347,7 +366,7 @@ Flow config, Condition ID, and reuse. Parent completion does not cancel an unfin
347
366
  ## Errors
348
367
 
349
368
  Client calls reject with concrete `DexServiceError` subclasses. Existing-Flow
350
- reads (`getAttribute`, `describeFlow`, `waitForFlow`, and `timeTravel`) use
369
+ reads (`describeFlow`, `waitForFlow`, and `timeTravel`) use
351
370
  `FlowNotFoundError`; operations that require a running Flow use
352
371
  `FlowNotActiveError`. Start conflicts, worker failures, RPC lock contention,
353
372
  and long-poll timeouts use `FlowAlreadyStartedError`,
@@ -355,7 +374,7 @@ and long-poll timeouts use `FlowAlreadyStartedError`,
355
374
 
356
375
  ```typescript
357
376
  try {
358
- await client.publish(flowId, orders.approved, orderId);
377
+ await client.invokeRPC(orders.updateOrder, flowId, update);
359
378
  } catch (error) {
360
379
  if (error instanceof FlowNotActiveError) {
361
380
  // The Flow is missing or already closed.
@@ -3,10 +3,9 @@ import { AttributeMatch } from "./attribute-match.js";
3
3
  import type { Context } from "./context.js";
4
4
  import { type Flow, type Registry } from "./flow.js";
5
5
  import { type FlowResult } from "./flow-result.js";
6
- import { type ClientOptions, type FlowConfig, type FlowInfo, type TimeTravelOptions, type SearchFlowsPage, type StartFlowOptions, type StepExecutionId, type StopFlowOptions, type TimerId } from "./options.js";
6
+ import { type ClientOptions, type FlowConfig, type FlowInfo, type TimeTravelOptions, type SearchFlowsPage, type StartFlowOptions, type StepExecutionId, type StopFlowOptions, type TimerId, type WaitForAttributeOptions, type WaitForStepCompletionOptions } from "./options.js";
7
7
  import { Attribute, AttributeMap } from "./persistence.js";
8
8
  import type { RPCResult } from "./rpc.js";
9
- import { Channel, ChannelMap, type ChannelMessage } from "./wait.js";
10
9
  import type { Stream, StreamMessage, StreamMessagesPage } from "./stream.js";
11
10
  /**
12
11
  * Calls Dex FlowService through registered, typed Flow definitions.
@@ -87,97 +86,6 @@ export declare class Client {
87
86
  * @returns A promise resolved after successful handler completion.
88
87
  */
89
88
  invokeRPC(rpcMethod: (context: Context) => void | Promise<void>, flowId: string, runId?: string): Promise<void>;
90
- /**
91
- * Reads a singleton Attribute.
92
- * @typeParam T - Attribute value type.
93
- * @param flowId - Non-empty existing Flow ID.
94
- * @param attribute - Typed singleton Attribute definition.
95
- * @param runId - Optional exact run; targets the current run when omitted.
96
- * @returns The decoded value, or `undefined` when unset.
97
- */
98
- getAttribute<T>(flowId: string, attribute: Attribute<T>, runId?: string): Promise<T | undefined>;
99
- /**
100
- * Reads one AttributeMap instance.
101
- * @typeParam T - Attribute value type.
102
- * @param flowId - Non-empty existing Flow ID.
103
- * @param attribute - Typed AttributeMap definition.
104
- * @param instance - The map instance. Slash is prohibited because it is a reserved character.
105
- * @param runId - Optional exact run; targets the current run when omitted.
106
- * @returns The decoded value, or `undefined` when unset.
107
- */
108
- getAttribute<T>(flowId: string, attribute: AttributeMap<T>, instance: string, runId?: string): Promise<T | undefined>;
109
- /**
110
- * Writes a singleton Attribute on an active Flow.
111
- * @typeParam T - Attribute value type.
112
- * @param flowId - Non-empty active Flow ID.
113
- * @param attribute - Typed singleton Attribute definition.
114
- * @param value - Value encoded by the Attribute codec.
115
- * @param runId - Optional exact run; targets the active run when omitted.
116
- * @returns A promise resolved after Dex applies the write.
117
- */
118
- setAttribute<T>(flowId: string, attribute: Attribute<T>, value: T, runId?: string): Promise<void>;
119
- /**
120
- * Writes one AttributeMap instance on an active Flow.
121
- * @typeParam T - Attribute value type.
122
- * @param flowId - Non-empty active Flow ID.
123
- * @param attribute - Typed AttributeMap definition.
124
- * @param instance - The map instance. Slash is prohibited because it is a reserved character.
125
- * @param value - Value encoded by the Attribute codec.
126
- * @param runId - Optional exact run; targets the active run when omitted.
127
- * @returns A promise resolved after Dex applies the write.
128
- */
129
- setAttribute<T>(flowId: string, attribute: AttributeMap<T>, instance: string, value: T, runId?: string): Promise<void>;
130
- /**
131
- * Publishes one or more values to a singleton Channel.
132
- * @typeParam T - Channel element type.
133
- * @param flowId - Non-empty active Flow ID.
134
- * @param channel - Typed singleton Channel definition.
135
- * @param values - Values appended in argument order.
136
- * @returns A promise resolved after Dex accepts the batch.
137
- */
138
- publish<T>(flowId: string, channel: Channel<T>, ...values: readonly T[]): Promise<void>;
139
- /**
140
- * Publishes one or more values to a ChannelMap instance.
141
- * @typeParam T - Channel element type.
142
- * @param flowId - Non-empty active Flow ID.
143
- * @param channel - Typed ChannelMap definition.
144
- * @param instance - The map instance. Slash is prohibited because it is a reserved character.
145
- * @param values - Values appended in argument order.
146
- * @returns A promise resolved after Dex accepts the batch.
147
- */
148
- publish<T>(flowId: string, channel: ChannelMap<T>, instance: string, ...values: readonly T[]): Promise<void>;
149
- /**
150
- * Returns every pending singleton Channel message in FIFO order.
151
- * @typeParam T - Channel value type.
152
- * @param flowId - Non-empty existing Flow ID.
153
- * @param channel - Registered singleton Channel.
154
- * @returns Typed pending message envelopes.
155
- */
156
- getChannelMessages<T>(flowId: string, channel: Channel<T>): Promise<readonly ChannelMessage<T>[]>;
157
- /**
158
- * Returns every pending message for one ChannelMap instance in FIFO order.
159
- * @typeParam T - Channel value type.
160
- * @param flowId - Non-empty existing Flow ID.
161
- * @param channel - Registered ChannelMap.
162
- * @param instance - The ChannelMap instance. Slash is prohibited because it is a reserved character.
163
- * @returns Typed pending message envelopes.
164
- */
165
- getChannelMessages<T>(flowId: string, channel: ChannelMap<T>, instance: string): Promise<readonly ChannelMessage<T>[]>;
166
- /**
167
- * Deletes one pending singleton Channel message by its server-assigned ID.
168
- * @param flowId - Non-empty active Flow ID.
169
- * @param channel - Registered singleton Channel.
170
- * @param messageId - Non-empty server-assigned message ID.
171
- */
172
- deleteChannelMessage(flowId: string, channel: Channel<unknown>, messageId: string): Promise<void>;
173
- /**
174
- * Deletes one pending message from a ChannelMap instance by server-assigned ID.
175
- * @param flowId - Non-empty active Flow ID.
176
- * @param channel - Registered ChannelMap.
177
- * @param instance - The ChannelMap instance. Slash is prohibited because it is a reserved character.
178
- * @param messageId - Non-empty server-assigned message ID.
179
- */
180
- deleteChannelMessage(flowId: string, channel: ChannelMap<unknown>, instance: string, messageId: string): Promise<void>;
181
89
  /**
182
90
  * Appends one typed best-effort Stream message with source metadata.
183
91
  * @typeParam T - Stream message type.
@@ -257,36 +165,40 @@ export declare class Client {
257
165
  */
258
166
  skipTimer(flowId: string, stepExecutionId: StepExecutionId, timerId: TimerId): Promise<void>;
259
167
  /**
260
- * Long-polls until one Step execution completes.
168
+ * Waits until one Step execution completes or its total handler budget expires.
169
+ * The server derives a stable Request ID from the Step execution when none is supplied.
170
+ * Transport long polls automatically reattach to the same logical wait.
261
171
  * @param flowId - Non-empty active Flow ID.
262
172
  * @param stepExecutionId - Step type and positive execution number.
263
- * @param timeoutMs - Non-negative server-side wait duration in milliseconds.
264
- * @throws {@link LongPollTimeoutError} when completion is not observed in time.
173
+ * @param options - Optional Request ID override and total handler wait budget.
174
+ * @throws {@link WaitHandlerTimeoutError} when a positive handler budget expires first.
265
175
  */
266
- waitForStepCompletion(flowId: string, stepExecutionId: StepExecutionId, timeoutMs: number): Promise<void>;
176
+ waitForStepCompletion(flowId: string, stepExecutionId: StepExecutionId, options: WaitForStepCompletionOptions): Promise<void>;
267
177
  /**
268
178
  * Waits until a singleton Attribute in the current run satisfies a match.
269
179
  * Returns the current value observed by the successful wait operation.
180
+ * The server derives a stable Request ID from the condition when none is supplied.
181
+ * Transport long polls automatically reattach to the same logical wait.
270
182
  * @typeParam T - Attribute value type.
271
183
  * @param flowId - Non-empty active Flow ID.
272
184
  * @param attribute - Registered singleton Attribute to observe.
273
185
  * @param match - Scalar predicate whose operand has the Attribute value type.
274
- * @param timeoutMs - Non-negative server-side wait duration in milliseconds.
186
+ * @param options - Optional Request ID override and total handler wait budget.
275
187
  * @returns The matched current Attribute value.
276
188
  */
277
- waitForAttributeMatch<T>(flowId: string, attribute: Attribute<T>, match: AttributeMatch<T>, timeoutMs: number): Promise<T>;
189
+ waitForAttributeMatch<T>(flowId: string, attribute: Attribute<T>, match: AttributeMatch<T>, options: WaitForAttributeOptions): Promise<T>;
278
190
  /**
279
191
  * Waits until one AttributeMap instance in the current run satisfies a match.
280
- * Primitive-value restrictions and timeout errors match `waitForAttributeMatch`.
192
+ * Primitive-value restrictions, options, and timeout errors match `waitForAttributeMatch`.
281
193
  * @typeParam T - AttributeMap value type.
282
194
  * @param flowId - Non-empty active Flow ID.
283
195
  * @param attribute - Registered AttributeMap to observe.
284
196
  * @param instance - The map instance to observe. Slash is prohibited because it is a reserved character.
285
197
  * @param match - Scalar predicate whose operand has the AttributeMap value type.
286
- * @param timeoutMs - Non-negative server-side wait duration in milliseconds.
198
+ * @param options - Optional Request ID override and total handler wait budget.
287
199
  * @returns The matched current AttributeMap value.
288
200
  */
289
- waitForAttributeMatch<T>(flowId: string, attribute: AttributeMap<T>, instance: string, match: AttributeMatch<T>, timeoutMs: number): Promise<T>;
201
+ waitForAttributeMatch<T>(flowId: string, attribute: AttributeMap<T>, instance: string, match: AttributeMatch<T>, options: WaitForAttributeOptions): Promise<T>;
290
202
  private waitForAttributeValue;
291
203
  /**
292
204
  * Replaces mutable configuration for an active Flow.
@@ -5,11 +5,11 @@
5
5
  // See the LICENSE file in the repository root.
6
6
  //
7
7
  // SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
- import { credentials } from "@grpc/grpc-js";
8
+ import { credentials, status } from "@grpc/grpc-js";
9
9
  import { AttributeMatch } from "./attribute-match.js";
10
10
  import { mapAttributeStoreNames, mapAttributeStoreSync } from "./attribute-store-sync.js";
11
11
  import { ActiveStepSearchMode as ProtoActiveStepSearchMode, FlowErrorType as ProtoFlowErrorType, FlowTimeoutPolicy as ProtoFlowTimeoutPolicy, FlowResetStepMethod as ProtoFlowResetStepMethod, FlowResetType as ProtoFlowResetType, FlowServiceClient, FlowStatus as ProtoFlowStatus, IdReusePolicy as ProtoIdReusePolicy, IndexType as ProtoIndexType, ExecuteMethodFailurePolicy, StartFlowRequest, StepDurability as ProtoStepDurability, StepOptions as ProtoStepOptions, FlowTimeoutHandlerOptions as ProtoFlowTimeoutHandlerOptions, StopType as ProtoStopType, WaitForMethodFailurePolicy, } from "./gen/dex.js";
12
- import { FlowErrorType, } from "./errors.js";
12
+ import { ErrorSubStatus, FlowErrorType, LongPollTimeoutError, WaitHandlerTimeoutError, } from "./errors.js";
13
13
  import { registeredFlow, registeredRPC, registeredStream, } from "./flow.js";
14
14
  import { createFlowResultFromProto, } from "./flow-result.js";
15
15
  import { translateServiceError } from "./grpc-status.js";
@@ -149,115 +149,6 @@ export class Client {
149
149
  }
150
150
  return decodeValue(codecOrJson(rpc.options.outputCodec), await this.hydrator.hydrate(response.output));
151
151
  }
152
- /**
153
- * Reads and decodes a singleton or map Attribute.
154
- * @param flowId - Non-empty existing Flow ID.
155
- * @param attribute - Typed Attribute definition.
156
- * @param instanceOrRunId - Map instance, or singleton run ID.
157
- * @param runId - Exact run for a map read.
158
- * @returns The decoded value, or `undefined` when unset.
159
- */
160
- async getAttribute(flowId, attribute, instanceOrRunId = "", runId = "") {
161
- const isMap = attribute instanceof AttributeMap;
162
- const response = await unary({ operation: "getAttribute", flowId, requirement: "existing" }, (callback) => this.service.getAttributes({
163
- flowId: requireName(flowId),
164
- runId: isMap ? runId : instanceOrRunId,
165
- keys: [physicalName(attribute.name, isMap ? instanceOrRunId : undefined)],
166
- allKeys: false,
167
- }, callback));
168
- const value = response.attributes[0]?.value;
169
- if (value === undefined) {
170
- return undefined;
171
- }
172
- return decodeValue(attribute.codec, await this.hydrator.hydrate(value));
173
- }
174
- /**
175
- * Encodes and writes a singleton or map Attribute.
176
- * @param flowId - Non-empty active Flow ID.
177
- * @param attribute - Typed Attribute definition.
178
- * @param instanceOrValue - Map instance or singleton value.
179
- * @param valueOrRunId - Map value or singleton run ID.
180
- * @param runId - Exact run for a map write.
181
- * @returns A promise resolved after Dex applies the write.
182
- */
183
- async setAttribute(flowId, attribute, instanceOrValue, valueOrRunId, runId = "") {
184
- const isMap = attribute instanceof AttributeMap;
185
- const instance = isMap ? String(instanceOrValue) : undefined;
186
- const value = isMap ? valueOrRunId : instanceOrValue;
187
- await unary({ operation: "setAttribute", flowId, requirement: "active" }, (callback) => this.service.setAttributes({
188
- flowId: requireName(flowId),
189
- runId: isMap ? runId : typeof valueOrRunId === "string" ? valueOrRunId : "",
190
- attributes: [
191
- {
192
- key: physicalName(attribute.name, instance),
193
- value: encodeValue(attribute.codec, value),
194
- indexConfig: mapIndex(attribute.index),
195
- syncConfig: mapAttributeStoreSync(attribute),
196
- },
197
- ],
198
- requestId: crypto.randomUUID(),
199
- }, callback));
200
- }
201
- /**
202
- * Encodes and publishes a singleton or map Channel batch.
203
- * @param flowId - Non-empty active Flow ID.
204
- * @param channel - Typed Channel definition.
205
- * @param instanceAndValues - Optional map instance followed by values.
206
- * @returns A promise resolved after Dex accepts the batch.
207
- */
208
- async publish(flowId, channel, ...instanceAndValues) {
209
- const isMap = channel instanceof ChannelMap;
210
- const instance = isMap ? String(instanceAndValues[0]) : undefined;
211
- const values = isMap ? instanceAndValues.slice(1) : instanceAndValues;
212
- await unary({ operation: "publish", flowId, requirement: "active" }, (callback) => this.service.publishToChannel({
213
- flowId: requireName(flowId),
214
- runId: "",
215
- messages: values.map((value) => ({
216
- channelName: physicalName(channel.name, instance),
217
- value: encodeValue(channel.codec, value),
218
- messageId: "",
219
- })),
220
- }, callback));
221
- }
222
- /**
223
- * Implements singleton and ChannelMap pending-message reads.
224
- * @typeParam T - Channel value type.
225
- * @param flowId - Non-empty existing Flow ID.
226
- * @param channel - Registered singleton or map definition.
227
- * @param instance - Required ChannelMap instance; omitted for a singleton.
228
- * @returns Typed pending message envelopes in FIFO order.
229
- */
230
- async getChannelMessages(flowId, channel, instance) {
231
- const response = await unary({ operation: "getChannelMessages", flowId, requirement: "existing" }, (callback) => this.service.getChannelMessages({
232
- flowId: requireName(flowId),
233
- runId: "",
234
- channelName: physicalName(channel.name, instance),
235
- }, callback));
236
- return Promise.all(response.messages.map(async (message) => ({
237
- messageId: message.messageId,
238
- value: decodeValue(channel.codec, await this.hydrator.hydrate(message.value)),
239
- })));
240
- }
241
- /**
242
- * Implements singleton and ChannelMap pending-message deletion.
243
- * @param flowId - Non-empty active Flow ID.
244
- * @param channel - Registered singleton or map definition.
245
- * @param instanceOrMessageId - Map instance or singleton message ID.
246
- * @param mapMessageId - Required message ID for a ChannelMap.
247
- * @returns A promise resolved after Dex accepts the deletion.
248
- */
249
- async deleteChannelMessage(flowId, channel, instanceOrMessageId, mapMessageId) {
250
- const isMap = channel instanceof ChannelMap;
251
- const instance = isMap ? instanceOrMessageId : undefined;
252
- const messageId = isMap ? mapMessageId : instanceOrMessageId;
253
- await unary({ operation: "deleteChannelMessage", flowId, requirement: "active" }, (callback) => this.service.deleteChannelMessage({
254
- flowId: requireName(flowId),
255
- runId: "",
256
- channelName: physicalName(channel.name, instance),
257
- messageId: requireName(messageId ?? ""),
258
- requestId: crypto.randomUUID(),
259
- }, callback));
260
- }
261
152
  /**
262
153
  * Appends one typed best-effort Stream message with source metadata.
263
154
  * @typeParam T - Stream message type.
@@ -468,62 +359,85 @@ export class Client {
468
359
  }, callback));
469
360
  }
470
361
  /**
471
- * Long-polls until one Step execution completes.
362
+ * Waits until one Step execution completes or its total handler budget expires.
363
+ * The server derives a stable Request ID from the Step execution when none is supplied.
364
+ * Transport long polls automatically reattach to the same logical wait.
472
365
  * @param flowId - Non-empty active Flow ID.
473
366
  * @param stepExecutionId - Step type and positive execution number.
474
- * @param timeoutMs - Non-negative server-side wait duration in milliseconds.
475
- * @throws {@link LongPollTimeoutError} when completion is not observed in time.
367
+ * @param options - Optional Request ID override and total handler wait budget.
368
+ * @throws {@link WaitHandlerTimeoutError} when a positive handler budget expires first.
476
369
  */
477
- async waitForStepCompletion(flowId, stepExecutionId, timeoutMs) {
478
- await unary({ operation: "waitForStepCompletion", flowId, requirement: "active" }, (callback) => this.service.waitForStepCompletion({
479
- flowId: requireName(flowId),
480
- stepType: stepExecutionId.stepType,
481
- stepExecutionNumber: String(stepExecutionId.number ?? 1),
482
- waitTimeSeconds: seconds(timeoutMs),
483
- requestId: crypto.randomUUID(),
484
- }, callback));
370
+ async waitForStepCompletion(flowId, stepExecutionId, options) {
371
+ const waitBudget = new ClientWaitBudget(options.maximumWaitTimeMs);
372
+ while (true) {
373
+ try {
374
+ await unary({ operation: "waitForStepCompletion", flowId, requirement: "active" }, (callback) => this.service.waitForStepCompletion({
375
+ flowId: requireName(flowId),
376
+ stepType: stepExecutionId.stepType,
377
+ stepExecutionNumber: String(stepExecutionId.number ?? 1),
378
+ waitTimeSeconds: waitBudget.remainingSeconds("waitForStepCompletion", flowId),
379
+ requestId: options.requestId ?? "",
380
+ }, callback));
381
+ return;
382
+ }
383
+ catch (error) {
384
+ if (!(error instanceof LongPollTimeoutError)) {
385
+ throw error;
386
+ }
387
+ }
388
+ }
485
389
  }
486
390
  /**
487
391
  * Waits until a singleton Attribute or AttributeMap instance satisfies a match.
488
392
  * @param flowId - Non-empty active Flow ID.
489
393
  * @param attribute - Registered Attribute or AttributeMap to observe.
490
- * @param args - Match and timeout, optionally preceded by a map instance.
394
+ * @param args - Match and options, optionally preceded by a map instance.
491
395
  * @returns The matched current value.
492
396
  */
493
397
  async waitForAttributeMatch(flowId, attribute, ...args) {
494
398
  if (attribute instanceof Attribute) {
495
- if (args.length !== 2 || typeof args[1] !== "number") {
399
+ if (args.length !== 2 || !isWaitForAttributeOptions(args[1])) {
496
400
  throw new TypeError("waitForAttributeMatch received invalid Attribute arguments");
497
401
  }
498
402
  return this.waitForAttributeValue(flowId, attribute, undefined, args[0], args[1]);
499
403
  }
500
404
  if (attribute instanceof AttributeMap) {
501
- if (args.length !== 3 || typeof args[0] !== "string" || typeof args[2] !== "number") {
405
+ if (args.length !== 3 || typeof args[0] !== "string" || !isWaitForAttributeOptions(args[2])) {
502
406
  throw new TypeError("waitForAttributeMatch received invalid AttributeMap arguments");
503
407
  }
504
408
  return this.waitForAttributeValue(flowId, attribute, args[0], args[1], args[2]);
505
409
  }
506
410
  throw new TypeError("waitForAttributeMatch received an invalid Attribute definition");
507
411
  }
508
- async waitForAttributeValue(flowId, attribute, instance, match, timeoutMs) {
412
+ async waitForAttributeValue(flowId, attribute, instance, match, options) {
509
413
  if (!(match instanceof AttributeMatch)) {
510
414
  throw new TypeError("waitForAttributeMatch requires an AttributeMatch");
511
415
  }
512
416
  const encoded = match.encode(attribute.codec);
513
- const response = await unary({ operation: "waitForAttributeMatch", flowId, requirement: "active" }, (callback) => this.service.waitForAttribute({
514
- flowId: requireName(flowId),
515
- match: {
516
- key: physicalName(attribute.name, instance),
517
- operator: encoded.operator,
518
- operand: encoded.operand,
519
- },
520
- waitTimeSeconds: seconds(timeoutMs),
521
- requestId: crypto.randomUUID(),
522
- }, callback));
523
- if (response.matchedValue === undefined) {
524
- throw new Error("waitForAttributeMatch response is incomplete");
417
+ const waitBudget = new ClientWaitBudget(options.maximumWaitTimeMs);
418
+ while (true) {
419
+ try {
420
+ const response = await unary({ operation: "waitForAttributeMatch", flowId, requirement: "active" }, (callback) => this.service.waitForAttribute({
421
+ flowId: requireName(flowId),
422
+ match: {
423
+ key: physicalName(attribute.name, instance),
424
+ operator: encoded.operator,
425
+ operand: encoded.operand,
426
+ },
427
+ waitTimeSeconds: waitBudget.remainingSeconds("waitForAttributeMatch", flowId),
428
+ requestId: options.requestId ?? "",
429
+ }, callback));
430
+ if (response.matchedValue === undefined) {
431
+ throw new Error("waitForAttributeMatch response is incomplete");
432
+ }
433
+ return decodeValue(attribute.codec, response.matchedValue);
434
+ }
435
+ catch (error) {
436
+ if (!(error instanceof LongPollTimeoutError)) {
437
+ throw error;
438
+ }
439
+ }
525
440
  }
526
- return decodeValue(attribute.codec, response.matchedValue);
527
441
  }
528
442
  /**
529
443
  * Replaces mutable configuration for an active Flow.
@@ -878,6 +792,29 @@ function seconds(milliseconds) {
878
792
  }
879
793
  return milliseconds / 1_000;
880
794
  }
795
+ function isWaitForAttributeOptions(value) {
796
+ return typeof value === "object" && value !== null;
797
+ }
798
+ class ClientWaitBudget {
799
+ deadlineMs;
800
+ constructor(maximumWaitTimeMs) {
801
+ const maximumWaitSeconds = seconds(maximumWaitTimeMs);
802
+ if (maximumWaitSeconds > 2_147_483_647) {
803
+ throw new RangeError("duration exceeds the int32 seconds range");
804
+ }
805
+ this.deadlineMs = maximumWaitSeconds === 0 ? undefined : performance.now() + maximumWaitSeconds * 1_000;
806
+ }
807
+ remainingSeconds(operation, flowId) {
808
+ if (this.deadlineMs === undefined) {
809
+ return 0;
810
+ }
811
+ const remainingMs = this.deadlineMs - performance.now();
812
+ if (remainingMs <= 0) {
813
+ throw new WaitHandlerTimeoutError(status.DEADLINE_EXCEEDED, ErrorSubStatus.WAIT_HANDLER_TIMEOUT, "wait handler timed out", operation, flowId);
814
+ }
815
+ return Math.ceil(remainingMs / 1_000);
816
+ }
817
+ }
881
818
  function heartbeatSeconds(milliseconds) {
882
819
  const value = seconds(milliseconds);
883
820
  if (value > 2_147_483_647) {