@vampgg/utils 1.0.0-beta.2 → 1.0.0-beta.4

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
@@ -213,7 +213,7 @@ import { createEventIterator } from "@vampgg/utils/create-event-iterator";
213
213
  import type { Subscriber, Context, CleanupFn } from "@vampgg/utils/create-event-iterator";
214
214
  ```
215
215
 
216
- Converts any push-based event source (EventEmitter, WebSocket messages, browser events) into an `AsyncGenerator`. The subscriber receives `{ emit, cancel }` and returns an optional cleanup function.
216
+ Converts any push-based event source (EventEmitter, WebSocket messages, browser events) into an `AsyncGenerator`. The subscriber receives `{ emit, cancel, error }` (`error(e)` surfaces a failure at the consumer instead of a clean end) and returns an optional cleanup function.
217
217
 
218
218
  ```ts
219
219
  const stream = createEventIterator<MyEvent>(({ emit, cancel }) => {
@@ -292,6 +292,25 @@ await channel.waitForOpen();
292
292
 
293
293
  All four tempo method types (unary, server-stream, client-stream, duplex) are supported with deadline and retry policy propagation.
294
294
 
295
+ **Full client round-trip.** In practice you connect, obtain a tempo-generated client stub from the channel, and call RPC methods directly — the channel handles encoding, correlation, and streaming:
296
+
297
+ ```ts
298
+ import { TempoWSChannel } from "@vampgg/utils/ws-channel";
299
+ import { RpcClient, MutationScope } from "./bebop"; // your tempo-generated client + types
300
+
301
+ const channel = TempoWSChannel.forAddress("wss://host/v1/game?ns=room1", { maxRetries: 3 });
302
+ await channel.waitForOpen();
303
+
304
+ const client = channel.getClient(RpcClient);
305
+
306
+ const spawned = await client.spawn(entity); // unary
307
+ for await (const scope of await client.observe(MutationScope({}))) {
308
+ // server-stream: each frame is a decoded record
309
+ }
310
+
311
+ channel.close();
312
+ ```
313
+
295
314
  ---
296
315
 
297
316
  ### `@vampgg/utils/ws-router` — WebSocket Tempo Server Router
@@ -0,0 +1,139 @@
1
+ import { Message } from "./bebop.mjs";
2
+ import { BebopRecord } from "bebop";
3
+ import { BaseChannel, CallOptions, ClientContext, MethodInfo, RetryPolicy } from "@tempojs/client";
4
+ import { BebopContentType, Credential, Deadline, MethodType, TempoError, TempoLogger } from "@tempojs/common";
5
+ import { EventEmitter } from "tseep";
6
+
7
+ //#region src/channel-core.d.ts
8
+ /**
9
+ * Shared client-channel core for the worker, WebSocket, and browser-extension
10
+ * transports.
11
+ *
12
+ * All three transports speak the same framed protocol: each request is a
13
+ * {@link Message} envelope matched to its response(s) by `messageId`, streamed
14
+ * frames reuse the envelope with fresh `data`, and a `CANCELLED` status frame
15
+ * terminates a stream. This class owns the protocol machinery — request
16
+ * construction, retry/deadline handling, response validation, the four RPC
17
+ * shapes (unary / client-stream / server-stream / duplex) and teardown —
18
+ * while subclasses supply only the genuinely transport-specific pieces via the
19
+ * small protected surface ({@link sendFrame} et al).
20
+ *
21
+ * Internal to `@vampgg/utils`; the public transports are the concrete
22
+ * subclasses (`TempoWorkerChannel`, `TempoWSChannel`, `TempoExtensionChannel`).
23
+ */
24
+ declare abstract class CoreChannel extends BaseChannel {
25
+ /** Per-call response listeners keyed by messageId (response demultiplexing). */
26
+ readonly events: EventEmitter<Record<string, (message: Message) => void>>;
27
+ /**
28
+ * Reject callbacks for in-flight unary / client-stream calls, keyed by
29
+ * messageId. Lets close()/error() settle parked requests instead of hanging.
30
+ */
31
+ readonly pending: Map<string, (e: unknown) => void>;
32
+ protected closed: boolean;
33
+ /** The reason a closed channel settles calls that arrive after teardown. */
34
+ protected closeReason?: TempoError;
35
+ protected readonly userAgent: string;
36
+ protected constructor(target: URL, logger: TempoLogger, contentType: BebopContentType);
37
+ /**
38
+ * Encodes and delivers one {@link Message} frame over the transport. May be
39
+ * synchronous (worker / WebSocket) or asynchronous (extension runtime); the
40
+ * call sites await or attach rejection handlers as each RPC shape requires.
41
+ */
42
+ protected abstract sendFrame(message: Message): void | Promise<unknown>;
43
+ /** Generates the unique id for an outgoing request envelope. */
44
+ protected generateMessageId(): string;
45
+ /**
46
+ * Serializes one streamed record into the bytes placed on `Message.data`.
47
+ * The default copies out of bebop's shared static write buffer so a later
48
+ * encode cannot clobber the payload; a transport whose {@link sendFrame}
49
+ * consumes the bytes synchronously may override to skip the copy.
50
+ */
51
+ protected serializeStreamItem(method: MethodInfo<BebopRecord, BebopRecord>, value: BebopRecord): Uint8Array;
52
+ /** Awaited before every RPC (e.g. the WebSocket transport's open gate). */
53
+ protected beforeCall(): Promise<void>;
54
+ /** Value for the request `authorization` field, if the transport has one. */
55
+ protected getAuthorizationValue(): Promise<string | undefined>;
56
+ /**
57
+ * Persists a `credential` value received on a response envelope. The default
58
+ * ignores it (the worker/extension transports carry no credentials).
59
+ */
60
+ protected storeResponseCredential(_credential: string): Promise<void>;
61
+ /**
62
+ * Whether tearing down a duplex-stream consumer sends a terminal CANCELLED
63
+ * frame to the server (the WebSocket transport does; the others do not).
64
+ */
65
+ protected readonly cancelDuplexOnCleanup: boolean;
66
+ /** Runs inside {@link close} before in-flight calls are rejected. */
67
+ protected onClosing(_reason: TempoError): void;
68
+ /** Runs at the end of {@link close} to tear down the underlying transport. */
69
+ protected closeTransport(): void;
70
+ /**
71
+ * Tear down the channel: reject every in-flight unary/client-stream call,
72
+ * remove all event listeners, and dispose the underlying transport.
73
+ * Idempotent and safe to invoke from transport close/error events or by an
74
+ * explicit caller.
75
+ */
76
+ close(reason?: TempoError): void;
77
+ removeCredential(): Promise<void>;
78
+ getCredential(): Promise<Credential | undefined>;
79
+ /**
80
+ * Executes a function with retries according to the provided retry policy.
81
+ * The function will be retried if it fails with a TempoError and its status code is included in the retryableStatusCodes of the retry policy.
82
+ * If a deadline is provided, the deadline for each attempt will be managed by the provided deadline, but the deadline will not be reset upon each retry.
83
+ *
84
+ * @template T - The type of the result returned by the function.
85
+ * @param {((retryAttempt: number) => Promise<T>)} func - A function that returns a Promise with a result. The function will receive a number indicating the current retry attempt.
86
+ * @param {RetryPolicy} retryPolicy - An object defining the retry policy, including maxAttempts, initialBackoff, maxBackoff, backoffMultiplier, and retryableStatusCodes.
87
+ * @param {Deadline} [deadline] - An optional deadline object that manages the timeout for each attempt.
88
+ * @param {AbortController} [abortController] - An optional AbortController instance to cancel the function execution.
89
+ * @returns {Promise<T>} - A Promise that resolves with the result of the function if it completes within the deadline and retry policy constraints.
90
+ * @throws {Error} - If the function execution fails and the error does not match the retry policy, or if the maximum number of attempts is reached without a successful result.
91
+ */
92
+ executeWithRetry<T>(func: (retryAttempt: number) => Promise<T>, retryPolicy: RetryPolicy, deadline?: Deadline, abortController?: AbortController): Promise<T>;
93
+ protected fetchUnary(init: Message, options?: CallOptions): Promise<Message>;
94
+ protected fetchClientStream(init: Message, method: MethodInfo<BebopRecord, BebopRecord>, generator: () => AsyncGenerator<BebopRecord, void, undefined>, options?: CallOptions): Promise<Message>;
95
+ protected fetchServerStream(init: Message, context: ClientContext, method: MethodInfo<BebopRecord, BebopRecord>, options?: CallOptions): AsyncGenerator<BebopRecord, void, undefined>;
96
+ protected fetchDuplexStream(init: Message, context: ClientContext, method: MethodInfo<BebopRecord, BebopRecord>, generator: () => AsyncGenerator<BebopRecord, void, undefined>, options?: CallOptions): AsyncGenerator<BebopRecord, void, undefined>;
97
+ /**
98
+ * Creates the request {@link Message} envelope for a given payload, context,
99
+ * method and optional call options.
100
+ *
101
+ * @param {Uint8Array} payload - The payload to be sent in the request.
102
+ * @param {ClientContext} context - The context of the client making the request.
103
+ * @param {MethodInfo<BebopRecord, BebopRecord>} method - Information about the method being called.
104
+ * @param {CallOptions | undefined} options - Optional configuration for the call.
105
+ * @returns {Promise<Message>} A Promise resolving to the created request envelope.
106
+ * @throws {TempoError} Throws an error if there's a problem while getting the credential header.
107
+ */
108
+ protected createRequest(payload: Uint8Array, _context: ClientContext, method: MethodInfo<BebopRecord, BebopRecord>, options?: CallOptions): Promise<Message>;
109
+ /**
110
+ * Processes the headers of the response from the server, validating their
111
+ * integrity and correctness, and hands any received credential to the
112
+ * transport via {@link storeResponseCredential}.
113
+ *
114
+ * @param {Message} response - The response received from the server.
115
+ * @param {ClientContext} context - The context of the client making the request.
116
+ * @param {MethodType} methodType - The type of method being called.
117
+ * @throws {TempoError} Throws an error if any validation checks fail or if there's a problem parsing or storing credentials.
118
+ */
119
+ protected processResponseHeaders(response: Message, _context: ClientContext, _methodType: MethodType): Promise<void>;
120
+ private raiseCallError;
121
+ /**
122
+ * {@inheritDoc BaseChannel.startUnary}
123
+ */
124
+ startUnary<TRequest extends BebopRecord, TResponse extends BebopRecord>(request: TRequest, context: ClientContext, method: MethodInfo<TRequest, TResponse>, options?: CallOptions): Promise<TResponse>;
125
+ /**
126
+ * {@inheritDoc BaseChannel.startClientStream}
127
+ */
128
+ startClientStream<TRequest extends BebopRecord, TResponse extends BebopRecord>(generator: () => AsyncGenerator<TRequest, void, undefined>, context: ClientContext, method: MethodInfo<TRequest, TResponse>, options?: CallOptions): Promise<TResponse>;
129
+ /**
130
+ * {@inheritDoc BaseChannel.startServerStream}
131
+ */
132
+ startServerStream<TRequest extends BebopRecord, TResponse extends BebopRecord>(request: TRequest, context: ClientContext, method: MethodInfo<TRequest, TResponse>, options?: CallOptions): Promise<AsyncGenerator<TResponse, void, undefined>>;
133
+ /**
134
+ * {@inheritDoc BaseChannel.startDuplexStream}
135
+ */
136
+ startDuplexStream<TRequest extends BebopRecord, TResponse extends BebopRecord>(generator: () => AsyncGenerator<TRequest, void, undefined>, context: ClientContext, method: MethodInfo<TRequest, TResponse>, options?: CallOptions): Promise<AsyncGenerator<TResponse, void, undefined>>;
137
+ }
138
+ //#endregion
139
+ export { CoreChannel };
@@ -0,0 +1,411 @@
1
+ import { Message } from "./bebop.mjs";
2
+ import { createEventIterator } from "./create-event-iterator.mjs";
3
+ import { createDuplexIterator } from "./create-duplex-iterator.mjs";
4
+ import { BaseChannel } from "@tempojs/client";
5
+ import { TempoError, TempoStatusCode, TempoUtil, TempoVersion } from "@tempojs/common";
6
+ import { EventEmitter } from "tseep";
7
+ //#region src/channel-core.ts
8
+ /**
9
+ * Shared client-channel core for the worker, WebSocket, and browser-extension
10
+ * transports.
11
+ *
12
+ * All three transports speak the same framed protocol: each request is a
13
+ * {@link Message} envelope matched to its response(s) by `messageId`, streamed
14
+ * frames reuse the envelope with fresh `data`, and a `CANCELLED` status frame
15
+ * terminates a stream. This class owns the protocol machinery — request
16
+ * construction, retry/deadline handling, response validation, the four RPC
17
+ * shapes (unary / client-stream / server-stream / duplex) and teardown —
18
+ * while subclasses supply only the genuinely transport-specific pieces via the
19
+ * small protected surface ({@link sendFrame} et al).
20
+ *
21
+ * Internal to `@vampgg/utils`; the public transports are the concrete
22
+ * subclasses (`TempoWorkerChannel`, `TempoWSChannel`, `TempoExtensionChannel`).
23
+ */
24
+ var CoreChannel = class extends BaseChannel {
25
+ /** Per-call response listeners keyed by messageId (response demultiplexing). */
26
+ events = new EventEmitter();
27
+ /**
28
+ * Reject callbacks for in-flight unary / client-stream calls, keyed by
29
+ * messageId. Lets close()/error() settle parked requests instead of hanging.
30
+ */
31
+ pending = /* @__PURE__ */ new Map();
32
+ closed = false;
33
+ /** The reason a closed channel settles calls that arrive after teardown. */
34
+ closeReason;
35
+ userAgent;
36
+ constructor(target, logger, contentType) {
37
+ super(target, logger, contentType);
38
+ this.userAgent = TempoUtil.buildUserAgent("javascript", TempoVersion, void 0, { runtime: TempoUtil.getEnvironmentName() });
39
+ }
40
+ /** Generates the unique id for an outgoing request envelope. */
41
+ generateMessageId() {
42
+ return crypto.randomUUID();
43
+ }
44
+ /**
45
+ * Serializes one streamed record into the bytes placed on `Message.data`.
46
+ * The default copies out of bebop's shared static write buffer so a later
47
+ * encode cannot clobber the payload; a transport whose {@link sendFrame}
48
+ * consumes the bytes synchronously may override to skip the copy.
49
+ */
50
+ serializeStreamItem(method, value) {
51
+ return new Uint8Array(method.serialize(value));
52
+ }
53
+ /** Awaited before every RPC (e.g. the WebSocket transport's open gate). */
54
+ async beforeCall() {}
55
+ /** Value for the request `authorization` field, if the transport has one. */
56
+ async getAuthorizationValue() {}
57
+ /**
58
+ * Persists a `credential` value received on a response envelope. The default
59
+ * ignores it (the worker/extension transports carry no credentials).
60
+ */
61
+ async storeResponseCredential(_credential) {}
62
+ /**
63
+ * Whether tearing down a duplex-stream consumer sends a terminal CANCELLED
64
+ * frame to the server (the WebSocket transport does; the others do not).
65
+ */
66
+ cancelDuplexOnCleanup = false;
67
+ /** Runs inside {@link close} before in-flight calls are rejected. */
68
+ onClosing(_reason) {}
69
+ /** Runs at the end of {@link close} to tear down the underlying transport. */
70
+ closeTransport() {}
71
+ /**
72
+ * Tear down the channel: reject every in-flight unary/client-stream call,
73
+ * remove all event listeners, and dispose the underlying transport.
74
+ * Idempotent and safe to invoke from transport close/error events or by an
75
+ * explicit caller.
76
+ */
77
+ close(reason) {
78
+ if (this.closed) return;
79
+ this.closed = true;
80
+ const err = reason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed");
81
+ this.closeReason = err;
82
+ this.onClosing(err);
83
+ for (const reject of this.pending.values()) reject(err);
84
+ this.pending.clear();
85
+ this.events.removeAllListeners();
86
+ this.closeTransport();
87
+ }
88
+ async removeCredential() {}
89
+ async getCredential() {}
90
+ /**
91
+ * Executes a function with retries according to the provided retry policy.
92
+ * The function will be retried if it fails with a TempoError and its status code is included in the retryableStatusCodes of the retry policy.
93
+ * If a deadline is provided, the deadline for each attempt will be managed by the provided deadline, but the deadline will not be reset upon each retry.
94
+ *
95
+ * @template T - The type of the result returned by the function.
96
+ * @param {((retryAttempt: number) => Promise<T>)} func - A function that returns a Promise with a result. The function will receive a number indicating the current retry attempt.
97
+ * @param {RetryPolicy} retryPolicy - An object defining the retry policy, including maxAttempts, initialBackoff, maxBackoff, backoffMultiplier, and retryableStatusCodes.
98
+ * @param {Deadline} [deadline] - An optional deadline object that manages the timeout for each attempt.
99
+ * @param {AbortController} [abortController] - An optional AbortController instance to cancel the function execution.
100
+ * @returns {Promise<T>} - A Promise that resolves with the result of the function if it completes within the deadline and retry policy constraints.
101
+ * @throws {Error} - If the function execution fails and the error does not match the retry policy, or if the maximum number of attempts is reached without a successful result.
102
+ */
103
+ async executeWithRetry(func, retryPolicy, deadline, abortController) {
104
+ let attempt = 0;
105
+ let lastError;
106
+ const execute = deadline ? (retryAttempt) => deadline.executeWithinDeadline(async () => await func(retryAttempt), abortController) : (retryAttempt) => func(retryAttempt);
107
+ while (attempt < retryPolicy.maxAttempts) try {
108
+ return await execute(attempt);
109
+ } catch (error) {
110
+ if (!(error instanceof Error)) throw new TempoError(TempoStatusCode.UNKNOWN, `unexpected error`, { data: error });
111
+ lastError = error;
112
+ if (!(error instanceof TempoError) || !retryPolicy.retryableStatusCodes.includes(error.status)) throw error;
113
+ const backoffWithJitter = Math.min(retryPolicy.initialBackoff.multiply(Math.pow(retryPolicy.backoffMultiplier, attempt)).totalMilliseconds, retryPolicy.maxBackoff.totalMilliseconds) * (Math.random() * .5 + .75);
114
+ await new Promise((resolve) => setTimeout(resolve, backoffWithJitter));
115
+ attempt++;
116
+ }
117
+ if (abortController && lastError !== void 0 && !(lastError instanceof Error && lastError.name === "AbortError") && !(lastError instanceof TempoError && lastError.status === TempoStatusCode.ABORTED)) abortController.abort();
118
+ return Promise.reject(lastError || new TempoError(TempoStatusCode.DEADLINE_EXCEEDED, "Failed to execute function with retry policy"));
119
+ }
120
+ async fetchUnary(init, options) {
121
+ const messageId = init.messageId;
122
+ let listener;
123
+ let onAbort;
124
+ const cleanup = () => {
125
+ this.events.off(messageId, listener);
126
+ if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
127
+ this.pending.delete(messageId);
128
+ };
129
+ return await new Promise((resolve, reject) => {
130
+ if (this.closed) {
131
+ reject(this.closeReason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed"));
132
+ return;
133
+ }
134
+ listener = (message) => resolve(message);
135
+ this.pending.set(messageId, reject);
136
+ if (options?.controller) {
137
+ onAbort = () => reject(new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", {}));
138
+ options.controller.signal.addEventListener("abort", onAbort, { once: true });
139
+ }
140
+ this.events.on(messageId, listener);
141
+ const sent = this.sendFrame(init);
142
+ if (sent instanceof Promise) sent.catch(reject);
143
+ }).finally(cleanup);
144
+ }
145
+ async fetchClientStream(init, method, generator, options) {
146
+ const messageId = init.messageId;
147
+ let listener;
148
+ let onAbort;
149
+ const cleanup = () => {
150
+ this.events.off(messageId, listener);
151
+ if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
152
+ this.pending.delete(messageId);
153
+ };
154
+ return await new Promise((resolve, reject) => {
155
+ if (this.closed) {
156
+ reject(this.closeReason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed"));
157
+ return;
158
+ }
159
+ listener = (message) => resolve(message);
160
+ this.pending.set(messageId, reject);
161
+ if (options?.controller) {
162
+ onAbort = () => reject(new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", {}));
163
+ options.controller.signal.addEventListener("abort", onAbort, { once: true });
164
+ }
165
+ this.events.on(messageId, listener);
166
+ runGenerator.call(this).catch(reject);
167
+ async function runGenerator() {
168
+ for await (const value of generator()) await this.sendFrame(Message({
169
+ ...init,
170
+ data: this.serializeStreamItem(method, value)
171
+ }));
172
+ await this.sendFrame(Message({
173
+ ...init,
174
+ status: TempoStatusCode.CANCELLED,
175
+ data: new Uint8Array()
176
+ }));
177
+ }
178
+ }).finally(cleanup);
179
+ }
180
+ fetchServerStream(init, context, method, options) {
181
+ const messageId = init.messageId;
182
+ return createEventIterator(({ emit, cancel, error }) => {
183
+ let terminated = false;
184
+ let onAbort;
185
+ const eventHandler = async (message) => {
186
+ try {
187
+ if (message.status === TempoStatusCode.CANCELLED) {
188
+ terminated = true;
189
+ cancel();
190
+ return;
191
+ }
192
+ await this.processResponseHeaders(message, context, method.type);
193
+ const record = method.deserialize(message.data);
194
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
195
+ emit(record);
196
+ } catch (e) {
197
+ error(e);
198
+ }
199
+ };
200
+ if (options?.controller) {
201
+ onAbort = () => error(new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", {}));
202
+ options.controller.signal.addEventListener("abort", onAbort, { once: true });
203
+ }
204
+ this.events.on(messageId, eventHandler);
205
+ this.sendFrame(init);
206
+ return () => {
207
+ this.events.off(messageId, eventHandler);
208
+ if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
209
+ if (terminated || this.closed) return;
210
+ this.sendFrame(Message({
211
+ messageId,
212
+ methodId: init.methodId,
213
+ status: TempoStatusCode.CANCELLED,
214
+ data: new Uint8Array(),
215
+ timestamp: /* @__PURE__ */ new Date()
216
+ }));
217
+ };
218
+ });
219
+ }
220
+ fetchDuplexStream(init, context, method, generator, options) {
221
+ const messageId = init.messageId;
222
+ return createDuplexIterator(generator(), (value) => {
223
+ this.sendFrame(Message({
224
+ ...init,
225
+ data: this.serializeStreamItem(method, value)
226
+ }));
227
+ }, ({ emit, cancel, error }) => {
228
+ let terminated = false;
229
+ let onAbort;
230
+ const eventHandler = async (message) => {
231
+ try {
232
+ if (message.status === TempoStatusCode.CANCELLED) {
233
+ terminated = true;
234
+ cancel();
235
+ return;
236
+ }
237
+ await this.processResponseHeaders(message, context, method.type);
238
+ const record = method.deserialize(message.data);
239
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
240
+ emit(record);
241
+ } catch (e) {
242
+ error(e);
243
+ }
244
+ };
245
+ if (options?.controller) {
246
+ onAbort = () => error(new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", {}));
247
+ options.controller.signal.addEventListener("abort", onAbort, { once: true });
248
+ }
249
+ this.events.on(messageId, eventHandler);
250
+ return () => {
251
+ this.events.off(messageId, eventHandler);
252
+ if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
253
+ if (this.cancelDuplexOnCleanup && !terminated && !this.closed) this.sendFrame(Message({
254
+ messageId,
255
+ methodId: init.methodId,
256
+ status: TempoStatusCode.CANCELLED,
257
+ data: new Uint8Array(),
258
+ timestamp: /* @__PURE__ */ new Date()
259
+ }));
260
+ };
261
+ });
262
+ }
263
+ /**
264
+ * Creates the request {@link Message} envelope for a given payload, context,
265
+ * method and optional call options.
266
+ *
267
+ * @param {Uint8Array} payload - The payload to be sent in the request.
268
+ * @param {ClientContext} context - The context of the client making the request.
269
+ * @param {MethodInfo<BebopRecord, BebopRecord>} method - Information about the method being called.
270
+ * @param {CallOptions | undefined} options - Optional configuration for the call.
271
+ * @returns {Promise<Message>} A Promise resolving to the created request envelope.
272
+ * @throws {TempoError} Throws an error if there's a problem while getting the credential header.
273
+ */
274
+ async createRequest(payload, _context, method, options) {
275
+ const requestInit = {
276
+ messageId: this.generateMessageId(),
277
+ methodId: method.id,
278
+ data: new Uint8Array(payload),
279
+ timestamp: /* @__PURE__ */ new Date()
280
+ };
281
+ if (options?.deadline) requestInit.deadline = new Date(options.deadline.toUnixTimestamp());
282
+ const authorization = await this.getAuthorizationValue();
283
+ if (authorization !== void 0) requestInit.authorization = authorization;
284
+ return Message(requestInit);
285
+ }
286
+ /**
287
+ * Processes the headers of the response from the server, validating their
288
+ * integrity and correctness, and hands any received credential to the
289
+ * transport via {@link storeResponseCredential}.
290
+ *
291
+ * @param {Message} response - The response received from the server.
292
+ * @param {ClientContext} context - The context of the client making the request.
293
+ * @param {MethodType} methodType - The type of method being called.
294
+ * @throws {TempoError} Throws an error if any validation checks fail or if there's a problem parsing or storing credentials.
295
+ */
296
+ async processResponseHeaders(response, _context, _methodType) {
297
+ const statusCode = response.status;
298
+ if (statusCode === void 0) throw new TempoError(TempoStatusCode.UNKNOWN, "tempo-status missing from response.");
299
+ if (statusCode !== TempoStatusCode.OK && statusCode !== TempoStatusCode.CANCELLED) {
300
+ let tempoMessage = response.msg;
301
+ if (!tempoMessage) tempoMessage = "unknown error";
302
+ throw new TempoError(statusCode, tempoMessage);
303
+ }
304
+ const responseCredential = response.credential;
305
+ if (responseCredential) await this.storeResponseCredential(responseCredential);
306
+ }
307
+ /**
308
+ * Runs the shared error-hook + error-mapping tail of every `start*` method.
309
+ * Always throws.
310
+ */
311
+ async raiseCallError(context, e) {
312
+ if (this.hooks !== void 0 && e instanceof Error) await this.hooks.executeErrorHooks(context, e);
313
+ if (e instanceof TempoError) throw e;
314
+ if (e instanceof Error) {
315
+ if (e.name === "AbortError") throw new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", e);
316
+ throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", e);
317
+ }
318
+ throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", { data: e });
319
+ }
320
+ /**
321
+ * {@inheritDoc BaseChannel.startUnary}
322
+ */
323
+ async startUnary(request, context, method, options) {
324
+ try {
325
+ await this.beforeCall();
326
+ const requestData = method.serialize(request);
327
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
328
+ const requestInit = await this.createRequest(requestData, context, method, options);
329
+ let response;
330
+ if (options?.retryPolicy) response = await this.executeWithRetry(async (retryAttempt) => {
331
+ if (retryAttempt > 0) requestInit.previousAttempts = retryAttempt;
332
+ return await this.fetchUnary(requestInit);
333
+ }, options.retryPolicy, options.deadline, options.controller);
334
+ else if (options?.deadline) response = await options.deadline.executeWithinDeadline(async () => {
335
+ return await this.fetchUnary(requestInit, options);
336
+ }, options.controller);
337
+ else response = await this.fetchUnary(requestInit, options);
338
+ await this.processResponseHeaders(response, context, method.type);
339
+ if (this.hooks !== void 0) await this.hooks.executeResponseHooks(context);
340
+ const record = method.deserialize(response.data);
341
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
342
+ return record;
343
+ } catch (e) {
344
+ return await this.raiseCallError(context, e);
345
+ }
346
+ }
347
+ /**
348
+ * {@inheritDoc BaseChannel.startClientStream}
349
+ */
350
+ async startClientStream(generator, context, method, options) {
351
+ try {
352
+ await this.beforeCall();
353
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
354
+ const requestInit = await this.createRequest(new Uint8Array(), context, method, options);
355
+ let response;
356
+ if (options?.deadline) response = await options.deadline.executeWithinDeadline(async () => {
357
+ return await this.fetchClientStream(requestInit, method, generator, options);
358
+ }, options.controller);
359
+ else response = await this.fetchClientStream(requestInit, method, generator, options);
360
+ await this.processResponseHeaders(response, context, method.type);
361
+ const record = this.deserializeResponse(response.data, method);
362
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
363
+ return record;
364
+ } catch (e) {
365
+ return await this.raiseCallError(context, e);
366
+ }
367
+ }
368
+ /**
369
+ * {@inheritDoc BaseChannel.startServerStream}
370
+ */
371
+ async startServerStream(request, context, method, options) {
372
+ try {
373
+ await this.beforeCall();
374
+ const requestData = method.serialize(request);
375
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
376
+ const requestInit = await this.createRequest(requestData, context, method, options);
377
+ let response;
378
+ if (options?.retryPolicy) response = await this.executeWithRetry(async (retryAttempt) => {
379
+ if (retryAttempt > 0) requestInit.previousAttempts = retryAttempt;
380
+ return this.fetchServerStream(requestInit, context, method, options);
381
+ }, options.retryPolicy, options.deadline, options.controller);
382
+ else if (options?.deadline) response = await options.deadline.executeWithinDeadline(async () => {
383
+ return this.fetchServerStream(requestInit, context, method, options);
384
+ }, options.controller);
385
+ else response = this.fetchServerStream(requestInit, context, method, options);
386
+ return response;
387
+ } catch (e) {
388
+ return await this.raiseCallError(context, e);
389
+ }
390
+ }
391
+ /**
392
+ * {@inheritDoc BaseChannel.startDuplexStream}
393
+ */
394
+ async startDuplexStream(generator, context, method, options) {
395
+ try {
396
+ await this.beforeCall();
397
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
398
+ const requestInit = await this.createRequest(new Uint8Array(), context, method, options);
399
+ let response;
400
+ if (options?.deadline) response = await options.deadline.executeWithinDeadline(async () => {
401
+ return this.fetchDuplexStream(requestInit, context, method, generator, options);
402
+ }, options.controller);
403
+ else response = this.fetchDuplexStream(requestInit, context, method, generator, options);
404
+ return response;
405
+ } catch (e) {
406
+ return await this.raiseCallError(context, e);
407
+ }
408
+ }
409
+ };
410
+ //#endregion
411
+ export { CoreChannel };