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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
1
  import { Message } from "./bebop.mjs";
2
- import { createEventIterator } from "./create-event-iterator.mjs";
3
- import { Deadline, Metadata, MethodType, TempoError, TempoStatusCode, TempoUtil, TempoVersion, stringifyCredential } from "@tempojs/common";
4
- import { EventEmitter } from "tseep";
2
+ import { RouterCore } from "./router-core.mjs";
3
+ import { Metadata, TempoError, TempoStatusCode, TempoUtil, TempoVersion } from "@tempojs/common";
5
4
  import { tabs } from "webextension-polyfill";
6
- import { ServerContext, TempoRouterConfiguration } from "@tempojs/server";
5
+ import { TempoRouterConfiguration } from "@tempojs/server";
7
6
  //#region src/extension-router.ts
8
7
  /**
9
8
  * Interface defining the configuration options for a TempoRouter instance.
@@ -39,6 +38,10 @@ var TempoExtensionRouterConfiguration = class {
39
38
  */
40
39
  maxRetryAttempts;
41
40
  /**
41
+ * Optional flag to indicate whether the tempo version, runtime, and variant should be exposed in responses via X-Powered-By (and if the GET endpoint should be enabled). Defaults to false.
42
+ */
43
+ exposeTempo;
44
+ /**
42
45
  * Constructs a new instance of TempoRouterConfiguration with default values.
43
46
  */
44
47
  constructor() {
@@ -146,215 +149,45 @@ var ExtensionBaseRouter = class {
146
149
  }
147
150
  };
148
151
  var TempoExtensionRouter = class extends ExtensionBaseRouter {
149
- events = new EventEmitter();
150
- clientStreams = /* @__PURE__ */ new Map();
152
+ core;
151
153
  constructor(logger, registry, configuration = new TempoRouterConfiguration(), authInterceptor) {
152
154
  super(logger, registry, configuration, authInterceptor);
153
- }
154
- async setAuthContext(request, context) {
155
- const authHeader = request.authorization;
156
- if (authHeader !== void 0 && this.authInterceptor !== void 0) {
157
- const authContext = await this.authInterceptor.intercept(context, authHeader);
158
- if (authContext !== void 0) context.authContext = authContext;
159
- }
160
- }
161
- async invokeUnaryMethod(request, context, method) {
162
- await this.setAuthContext(request, context);
163
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
164
- const record = this.deserializeRequest(request.data, method, "bebop");
165
- if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
166
- return await method.invoke(record, context);
167
- }
168
- async invokeClientStreamMethod(request, context, method) {
169
- await this.setAuthContext(request, context);
170
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
171
- const messageId = request.messageId;
172
- if (this.clientStreams.has(messageId)) {
173
- const invocation = this.clientStreams.get(messageId);
174
- this.events.emit(messageId, request);
175
- return await invocation;
176
- }
177
- const generator = () => {
178
- return createEventIterator(({ emit, cancel }) => {
179
- const eventHandler = async (message) => {
180
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
181
- if (message.status === TempoStatusCode.CANCELLED) {
182
- cancel();
183
- return;
184
- }
185
- const requestData = message.data;
186
- const record = this.deserializeRequest(requestData, method, "bebop");
187
- if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
188
- emit(record);
189
- };
190
- this.events.on(messageId, eventHandler);
191
- return () => {
192
- this.events.off(messageId, eventHandler);
193
- this.clientStreams.delete(messageId);
194
- };
195
- });
196
- };
197
- const invocation = method.invoke(generator, context);
198
- this.clientStreams.set(messageId, invocation);
199
- this.events.emit(messageId, request);
200
- return await invocation.finally(() => {
201
- this.clientStreams.delete(messageId);
155
+ this.core = new RouterCore({
156
+ logger,
157
+ maxRetryAttempts: this.maxRetryAttempts,
158
+ transmitInternalErrors: this.transmitInternalErrors,
159
+ authInterceptor,
160
+ getMethod: (methodId) => this.registry.getMethod(methodId),
161
+ getHooks: () => this.hooks,
162
+ deserializeRequest: (data, method, contentType) => this.deserializeRequest(data, method, contentType),
163
+ serializeResponse: (record, method, contentType) => this.serializeResponse(record, method, contentType)
202
164
  });
203
165
  }
204
- async invokeServerStreamMethod(request, context, method) {
205
- await this.setAuthContext(request, context);
206
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
207
- const requestData = request.data;
208
- const record = this.deserializeRequest(requestData, method, "bebop");
209
- if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
210
- if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
211
- return method.invoke(record, context);
212
- }
213
- async invokeDuplexStreamMethod(request, context, method) {
214
- await this.setAuthContext(request, context);
215
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
216
- const messageId = request.messageId;
217
- if (this.clientStreams.has(messageId)) {
218
- const invocation = this.clientStreams.get(messageId);
219
- this.events.emit(messageId, request);
220
- return invocation;
221
- }
222
- if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
223
- const generator = () => {
224
- return createEventIterator(({ emit, cancel }) => {
225
- const eventHandler = async (message) => {
226
- if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
227
- if (message.status === TempoStatusCode.CANCELLED) {
228
- cancel();
229
- return;
230
- }
231
- const requestData = message.data;
232
- const record = this.deserializeRequest(requestData, method, "bebop");
233
- if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
234
- emit(record);
235
- };
236
- this.events.on(messageId, eventHandler);
237
- this.events.emit(messageId, request);
238
- return () => {
239
- this.events.off(messageId, eventHandler);
240
- this.clientStreams.delete(messageId);
241
- };
242
- });
243
- };
244
- const invocation = method.invoke(generator, context);
245
- this.clientStreams.set(messageId, invocation);
246
- return invocation;
247
- }
248
166
  /**
249
167
  * Tear down all open streams for this connection. Emits a synthetic CANCELLED
250
168
  * to each in-flight client/duplex stream so its generator ends, then clears
251
169
  * all listeners.
252
170
  */
253
171
  async closeConnection() {
254
- for (const id of [...this.clientStreams.keys()]) {
255
- this.events.emit(id, Message({
256
- messageId: id,
257
- status: TempoStatusCode.CANCELLED,
258
- data: new Uint8Array()
259
- }));
260
- this.clientStreams.delete(id);
261
- }
262
- this.events.removeAllListeners();
172
+ this.core.cancelClientStreams();
173
+ this.core.removeAllListeners();
263
174
  }
264
175
  async process(req, response, ctx) {
265
- let request = Message(req);
266
- let context;
267
- try {
268
- const methodId = request.methodId;
269
- const method = this.registry.getMethod(methodId);
270
- if (!method) throw new TempoError(TempoStatusCode.NOT_FOUND, `no service is registered which contains a method of '${methodId}'`);
271
- const metadataHeader = request.customMetadata;
272
- const metadata = metadataHeader ? Metadata.fromHttpHeader(metadataHeader) : new Metadata();
273
- const previousAttempts = request.previousAttempts;
274
- if (previousAttempts !== void 0 && previousAttempts > this.maxRetryAttempts) throw new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, "max retry attempts exceeded");
275
- let deadline;
276
- const deadlineHeader = request.deadline;
277
- if (deadlineHeader !== void 0) deadline = Deadline.fromUnixTimestamp(deadlineHeader.valueOf());
278
- if (deadline !== void 0 && deadline.isExpired()) throw new TempoError(TempoStatusCode.DEADLINE_EXCEEDED, "incoming request has already exceeded its deadline");
279
- const outgoingMetadata = new Metadata();
280
- const incomingContext = {
281
- headers: new Headers(),
282
- metadata
283
- };
284
- if (deadline !== void 0) incomingContext.deadline = deadline;
285
- context = new ServerContext(incomingContext, { metadata: outgoingMetadata }, ctx);
286
- const handleRequest = async () => {
287
- let recordGenerator = void 0;
288
- let record;
289
- switch (method.type) {
290
- case MethodType.Unary:
291
- record = await this.invokeUnaryMethod(request, context, method);
292
- break;
293
- case MethodType.ClientStream:
294
- record = await this.invokeClientStreamMethod(request, context, method);
295
- break;
296
- case MethodType.ServerStream:
297
- recordGenerator = await this.invokeServerStreamMethod(request, context, method);
298
- break;
299
- case MethodType.DuplexStream:
300
- recordGenerator = await this.invokeDuplexStreamMethod(request, context, method);
301
- break;
302
- default: throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: unknown method type");
303
- }
304
- const outgoingCredential = context.outgoingCredential;
305
- if (outgoingCredential) response.credential = stringifyCredential(outgoingCredential);
306
- response.methodId = request.methodId;
307
- response.messageId = request.messageId;
308
- response.status = TempoStatusCode.OK;
309
- response.timestamp = /* @__PURE__ */ new Date();
310
- response.msg = "";
311
- if (this.hooks !== void 0) await this.hooks.executeResponseHooks(context);
312
- outgoingMetadata.freeze();
313
- if (outgoingMetadata.size() > 0) response.customMetadata = outgoingMetadata.data;
314
- if (recordGenerator !== void 0) {
315
- const writeFrames = async () => {
316
- for await (const value of recordGenerator) {
317
- const responseData = this.serializeResponse(value, method, "bebop");
318
- response.data = new Uint8Array(responseData);
319
- await this.send(ctx.sender, response);
320
- }
321
- response.data = new Uint8Array();
322
- response.status = TempoStatusCode.CANCELLED;
323
- await this.send(ctx.sender, response);
324
- };
325
- if (deadline) await deadline.executeWithinDeadline(writeFrames);
326
- else await writeFrames();
327
- } else {
328
- if (record === void 0) throw new TempoError(TempoStatusCode.INTERNAL, "service method did not return a record");
329
- const responseData = this.serializeResponse(record, method, "bebop");
330
- response.data = new Uint8Array(responseData);
331
- await this.send(ctx.sender, response);
332
- }
333
- };
334
- if (deadline !== void 0) await deadline.executeWithinDeadline(handleRequest);
335
- else await handleRequest();
336
- } catch (e) {
337
- let status = TempoStatusCode.UNKNOWN;
338
- let message = "unknown error";
339
- if (e instanceof TempoError) {
340
- status = e.status;
341
- message = e.message;
342
- if (e.status === TempoStatusCode.INTERNAL && this.transmitInternalErrors !== true) message = "internal error";
343
- if (e.status === TempoStatusCode.INTERNAL) this.logger.critical(e.message, void 0, e);
344
- else this.logger.error(message, void 0, e);
345
- } else if (e instanceof Error) {
346
- message = e.message;
347
- this.logger.error(message, void 0, e);
348
- }
349
- if (e instanceof Error && this.hooks !== void 0) await this.hooks.executeErrorHooks(context, e);
350
- this.clientStreams.delete(request.messageId);
351
- response.status = status;
352
- response.msg = message;
353
- response.messageId = request.messageId;
354
- response.timestamp = request.timestamp;
355
- response.methodId = request.methodId;
356
- await this.send(ctx.sender, response);
357
- }
176
+ const request = Message(req);
177
+ const send = (message) => this.send(ctx.sender, message);
178
+ await this.core.processRequest(request, response, ctx, {
179
+ send,
180
+ stampErrorResponse: true,
181
+ decorateResponse: (message) => {
182
+ message.msg = "";
183
+ },
184
+ writeStreamFrames: (generator, resp, method) => this.core.writeStreamFrames({
185
+ generator,
186
+ response: resp,
187
+ method,
188
+ send
189
+ })
190
+ });
358
191
  }
359
192
  async send(sender, message) {
360
193
  if (!sender.tab?.id || sender.tab?.discarded === true) {
@@ -362,7 +195,7 @@ var TempoExtensionRouter = class extends ExtensionBaseRouter {
362
195
  return;
363
196
  }
364
197
  try {
365
- await tabs.sendMessage(sender.tab.id, Array.apply(null, Message.encode(message)));
198
+ await tabs.sendMessage(sender.tab.id, Array.from(Message.encode(message)));
366
199
  } catch (e) {
367
200
  this.logger.error("failed to send message, tab is likely inactive", {}, e);
368
201
  }
@@ -0,0 +1,121 @@
1
+ import { Message } from "./bebop.mjs";
2
+ import { BebopRecord } from "bebop";
3
+ import { BebopContentType, HookRegistry, Metadata, TempoLogger } from "@tempojs/common";
4
+ import { EventEmitter } from "tseep";
5
+ import { AuthInterceptor, BebopMethodAny, ServerContext } from "@tempojs/server";
6
+
7
+ //#region src/router-core.d.ts
8
+ /**
9
+ * The slice of a concrete router (`BaseRouter` / `ExtensionBaseRouter`
10
+ * subclass) that {@link RouterCore} needs. Supplied as delegate closures from
11
+ * the router's constructor because the underlying members are `protected`.
12
+ */
13
+ interface RouterCoreHost {
14
+ readonly logger: TempoLogger;
15
+ readonly maxRetryAttempts: number;
16
+ readonly transmitInternalErrors: boolean;
17
+ readonly authInterceptor?: AuthInterceptor;
18
+ /** Resolve a method from the router's service registry. */
19
+ getMethod(methodId: number): BebopMethodAny | undefined;
20
+ /** Read the router's current hook registry (set later via `useHooks`). */
21
+ getHooks(): HookRegistry<ServerContext, any> | undefined;
22
+ deserializeRequest(requestData: Uint8Array, method: BebopMethodAny, contentType: BebopContentType): BebopRecord;
23
+ serializeResponse(response: BebopRecord, method: BebopMethodAny, contentType: BebopContentType): Uint8Array;
24
+ }
25
+ /**
26
+ * The transport-specific pieces of one `process()` invocation, built fresh per
27
+ * request so the callbacks can close over the transport handle (socket, worker
28
+ * port, extension sender) and the request envelope.
29
+ */
30
+ interface ProcessTransport {
31
+ /** Deliver a fully-populated response envelope to the client. */
32
+ send(response: Message): void | Promise<void>;
33
+ /**
34
+ * Pump a server-stream / duplex generator into response frames (usually via
35
+ * {@link RouterCore.writeStreamFrames}). Runs inside the request deadline.
36
+ */
37
+ writeStreamFrames(generator: AsyncGenerator<BebopRecord, void, unknown>, response: Message, method: BebopMethodAny): Promise<void>;
38
+ /**
39
+ * Build the incoming request {@link Metadata} for the ServerContext.
40
+ * Defaults to parsing the request's custom-metadata header.
41
+ */
42
+ buildIncomingMetadata?(request: Message): Metadata;
43
+ /**
44
+ * Override the server-stream invocation (the WebSocket router layers
45
+ * CANCELLED handling and generator tracking on top of the shared one).
46
+ */
47
+ invokeServerStream?(request: Message, context: ServerContext, method: BebopMethodAny): Promise<AsyncGenerator<BebopRecord, void, unknown>>;
48
+ /** Mutate the response envelope after the OK headers are populated. */
49
+ decorateResponse?(response: Message): void;
50
+ /** Stamp messageId/timestamp/methodId from the request onto error frames. */
51
+ stampErrorResponse?: boolean;
52
+ }
53
+ /** Options for {@link RouterCore.writeStreamFrames}. */
54
+ interface StreamWriteOptions {
55
+ generator: AsyncGenerator<BebopRecord, void, unknown>;
56
+ response: Message;
57
+ method: BebopMethodAny;
58
+ contentType?: BebopContentType;
59
+ /** Deliver one populated frame to the client. */
60
+ send: (response: Message) => void | Promise<void>;
61
+ /**
62
+ * Serialize one record into owned payload bytes. The default copies out of
63
+ * bebop's shared write buffer; the WebSocket router substitutes its
64
+ * encode-once broadcast cache.
65
+ */
66
+ serializePayload?: (value: BebopRecord) => Uint8Array;
67
+ /** Checked before each frame; `true` stops the pump (worker cancellation). */
68
+ shouldStop?: () => boolean;
69
+ /** Send the terminal frame even when the generator throws (worker semantics). */
70
+ alwaysTerminate?: boolean;
71
+ /** Runs after the pump ends, before the terminal frame (tracking-map reaping). */
72
+ onEnd?: () => void;
73
+ }
74
+ /**
75
+ * Shared server-side routing core for the worker, WebSocket, and
76
+ * browser-extension routers.
77
+ *
78
+ * Owns the per-connection stream state (response demultiplexing events and
79
+ * in-flight client/duplex streams) plus every transport-agnostic step of
80
+ * dispatching one framed request: auth interception, hook execution, the four
81
+ * RPC invocation shapes, response population, deadline enforcement, and error
82
+ * mapping. Each concrete router composes an instance and passes the genuinely
83
+ * transport-specific bits per request via {@link ProcessTransport}.
84
+ *
85
+ * Internal to `@vampgg/utils`; the public routers are `TempoWorkerRouter`,
86
+ * `TempoWsRouter`, and `TempoExtensionRouter`.
87
+ */
88
+ declare class RouterCore {
89
+ private readonly host;
90
+ /** Per-stream frame listeners keyed by messageId. */
91
+ readonly events: EventEmitter<Record<string, (message: Message) => void>>;
92
+ /** In-flight client/duplex stream invocations keyed by messageId. */
93
+ readonly clientStreams: Map<string, Promise<any>>;
94
+ constructor(host: RouterCoreHost);
95
+ private setAuthContext;
96
+ invokeUnaryMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<BebopRecord>;
97
+ invokeClientStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<BebopRecord>;
98
+ invokeServerStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<AsyncGenerator<BebopRecord, void, unknown>>;
99
+ invokeDuplexStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<AsyncGenerator<BebopRecord, void, unknown>>;
100
+ /**
101
+ * Emit a synthetic CANCELLED to every in-flight client/duplex stream so its
102
+ * generator ends, and drop the tracking entries (connection teardown).
103
+ */
104
+ cancelClientStreams(): void;
105
+ /** Drop every per-stream frame listener (connection teardown). */
106
+ removeAllListeners(): void;
107
+ /**
108
+ * Pump a server-stream / duplex generator into response frames and emit the
109
+ * terminal CANCELLED frame that tells the client the stream is complete.
110
+ */
111
+ writeStreamFrames(opts: StreamWriteOptions): Promise<void>;
112
+ /**
113
+ * Dispatch one decoded request envelope: resolve the method, enforce retry
114
+ * and deadline limits, build the {@link ServerContext}, invoke the method by
115
+ * type, populate and deliver the response, and map any failure onto an error
116
+ * frame. Transport-specific behavior is injected via `transport`.
117
+ */
118
+ processRequest(request: Message, response: Message, env: unknown, transport: ProcessTransport, contentType?: BebopContentType): Promise<void>;
119
+ }
120
+ //#endregion
121
+ export { ProcessTransport, RouterCore, RouterCoreHost, StreamWriteOptions };
@@ -0,0 +1,282 @@
1
+ import { Message } from "./bebop.mjs";
2
+ import { createEventIterator } from "./create-event-iterator.mjs";
3
+ import { Deadline, Metadata, MethodType, TempoError, TempoStatusCode, TempoUtil, stringifyCredential } from "@tempojs/common";
4
+ import { EventEmitter } from "tseep";
5
+ import { ServerContext } from "@tempojs/server";
6
+ //#region src/router-core.ts
7
+ /**
8
+ * Parse the request's custom-metadata header into a {@link Metadata} (the
9
+ * default incoming metadata for the worker/extension routers).
10
+ */
11
+ function customMetadataOf(request) {
12
+ const metadataHeader = request.customMetadata;
13
+ return metadataHeader ? Metadata.fromHttpHeader(metadataHeader) : new Metadata();
14
+ }
15
+ /**
16
+ * Shared server-side routing core for the worker, WebSocket, and
17
+ * browser-extension routers.
18
+ *
19
+ * Owns the per-connection stream state (response demultiplexing events and
20
+ * in-flight client/duplex streams) plus every transport-agnostic step of
21
+ * dispatching one framed request: auth interception, hook execution, the four
22
+ * RPC invocation shapes, response population, deadline enforcement, and error
23
+ * mapping. Each concrete router composes an instance and passes the genuinely
24
+ * transport-specific bits per request via {@link ProcessTransport}.
25
+ *
26
+ * Internal to `@vampgg/utils`; the public routers are `TempoWorkerRouter`,
27
+ * `TempoWsRouter`, and `TempoExtensionRouter`.
28
+ */
29
+ var RouterCore = class {
30
+ host;
31
+ /** Per-stream frame listeners keyed by messageId. */
32
+ events = new EventEmitter();
33
+ /** In-flight client/duplex stream invocations keyed by messageId. */
34
+ clientStreams = /* @__PURE__ */ new Map();
35
+ constructor(host) {
36
+ this.host = host;
37
+ }
38
+ async setAuthContext(request, context) {
39
+ const authHeader = request.authorization;
40
+ if (authHeader !== void 0 && this.host.authInterceptor !== void 0) {
41
+ const authContext = await this.host.authInterceptor.intercept(context, authHeader);
42
+ if (authContext !== void 0) context.authContext = authContext;
43
+ }
44
+ }
45
+ async invokeUnaryMethod(request, context, method, contentType) {
46
+ await this.setAuthContext(request, context);
47
+ const hooks = this.host.getHooks();
48
+ if (hooks !== void 0) await hooks.executeRequestHooks(context);
49
+ const requestData = request.data;
50
+ const record = this.host.deserializeRequest(requestData, method, contentType);
51
+ if (hooks !== void 0) await hooks.executeDecodeHooks(context, record);
52
+ return await method.invoke(record, context);
53
+ }
54
+ async invokeClientStreamMethod(request, context, method, contentType) {
55
+ await this.setAuthContext(request, context);
56
+ if (this.host.getHooks() !== void 0) await this.host.getHooks().executeRequestHooks(context);
57
+ const messageId = request.messageId;
58
+ const stream = this.clientStreams.get(messageId);
59
+ if (stream) {
60
+ this.events.emit(messageId, request);
61
+ return await stream;
62
+ }
63
+ const generator = () => {
64
+ return createEventIterator(({ emit, cancel }) => {
65
+ const eventHandler = async (message) => {
66
+ const hooks = this.host.getHooks();
67
+ if (hooks !== void 0) await hooks.executeRequestHooks(context);
68
+ if (message.status === TempoStatusCode.CANCELLED) {
69
+ cancel();
70
+ return;
71
+ }
72
+ const requestData = message.data;
73
+ const record = this.host.deserializeRequest(requestData, method, contentType);
74
+ if (hooks !== void 0) await hooks.executeDecodeHooks(context, record);
75
+ emit(record);
76
+ };
77
+ this.events.on(messageId, eventHandler);
78
+ return () => {
79
+ this.events.off(messageId, eventHandler);
80
+ this.clientStreams.delete(messageId);
81
+ };
82
+ });
83
+ };
84
+ const invocation = method.invoke(generator, context);
85
+ this.clientStreams.set(messageId, invocation);
86
+ this.events.emit(messageId, request);
87
+ return await invocation.finally(() => {
88
+ this.clientStreams.delete(messageId);
89
+ });
90
+ }
91
+ async invokeServerStreamMethod(request, context, method, contentType) {
92
+ await this.setAuthContext(request, context);
93
+ const hooks = this.host.getHooks();
94
+ if (hooks !== void 0) await hooks.executeRequestHooks(context);
95
+ const requestData = request.data;
96
+ const record = this.host.deserializeRequest(requestData, method, contentType);
97
+ if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
98
+ if (hooks !== void 0) await hooks.executeDecodeHooks(context, record);
99
+ return method.invoke(record, context);
100
+ }
101
+ async invokeDuplexStreamMethod(request, context, method, contentType) {
102
+ await this.setAuthContext(request, context);
103
+ if (this.host.getHooks() !== void 0) await this.host.getHooks().executeRequestHooks(context);
104
+ const messageId = request.messageId;
105
+ const stream = this.clientStreams.get(messageId);
106
+ if (stream) {
107
+ this.events.emit(messageId, request);
108
+ return stream;
109
+ }
110
+ if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
111
+ const generator = () => {
112
+ return createEventIterator(({ emit, cancel }) => {
113
+ const eventHandler = async (message) => {
114
+ const hooks = this.host.getHooks();
115
+ if (hooks !== void 0) await hooks.executeRequestHooks(context);
116
+ if (message.status === TempoStatusCode.CANCELLED) {
117
+ cancel();
118
+ return;
119
+ }
120
+ const requestData = message.data;
121
+ const record = this.host.deserializeRequest(requestData, method, contentType);
122
+ if (hooks !== void 0) await hooks.executeDecodeHooks(context, record);
123
+ emit(record);
124
+ };
125
+ this.events.on(messageId, eventHandler);
126
+ this.events.emit(messageId, request);
127
+ return () => {
128
+ this.events.off(messageId, eventHandler);
129
+ this.clientStreams.delete(messageId);
130
+ };
131
+ });
132
+ };
133
+ const invocation = method.invoke(generator, context);
134
+ this.clientStreams.set(messageId, invocation);
135
+ return invocation;
136
+ }
137
+ /**
138
+ * Emit a synthetic CANCELLED to every in-flight client/duplex stream so its
139
+ * generator ends, and drop the tracking entries (connection teardown).
140
+ */
141
+ cancelClientStreams() {
142
+ for (const id of [...this.clientStreams.keys()]) {
143
+ this.events.emit(id, Message({
144
+ messageId: id,
145
+ status: TempoStatusCode.CANCELLED,
146
+ data: new Uint8Array()
147
+ }));
148
+ this.clientStreams.delete(id);
149
+ }
150
+ }
151
+ /** Drop every per-stream frame listener (connection teardown). */
152
+ removeAllListeners() {
153
+ this.events.removeAllListeners();
154
+ }
155
+ /**
156
+ * Pump a server-stream / duplex generator into response frames and emit the
157
+ * terminal CANCELLED frame that tells the client the stream is complete.
158
+ */
159
+ async writeStreamFrames(opts) {
160
+ const contentType = opts.contentType ?? "bebop";
161
+ const serializePayload = opts.serializePayload ?? ((value) => new Uint8Array(this.host.serializeResponse(value, opts.method, contentType)));
162
+ const terminate = async () => {
163
+ opts.onEnd?.();
164
+ opts.response.data = new Uint8Array();
165
+ opts.response.status = TempoStatusCode.CANCELLED;
166
+ await opts.send(opts.response);
167
+ };
168
+ const pump = async () => {
169
+ for await (const value of opts.generator) {
170
+ if (opts.shouldStop?.()) break;
171
+ opts.response.data = serializePayload(value);
172
+ await opts.send(opts.response);
173
+ }
174
+ };
175
+ if (opts.alwaysTerminate) try {
176
+ await pump();
177
+ } finally {
178
+ await terminate();
179
+ }
180
+ else {
181
+ await pump();
182
+ await terminate();
183
+ }
184
+ }
185
+ /**
186
+ * Dispatch one decoded request envelope: resolve the method, enforce retry
187
+ * and deadline limits, build the {@link ServerContext}, invoke the method by
188
+ * type, populate and deliver the response, and map any failure onto an error
189
+ * frame. Transport-specific behavior is injected via `transport`.
190
+ */
191
+ async processRequest(request, response, env, transport, contentType = "bebop") {
192
+ let context;
193
+ try {
194
+ const methodId = request.methodId;
195
+ const method = this.host.getMethod(methodId);
196
+ if (!method) throw new TempoError(TempoStatusCode.NOT_FOUND, `no service is registered which contains a method of '${methodId}'`);
197
+ const previousAttempts = request.previousAttempts;
198
+ if (previousAttempts !== void 0 && previousAttempts > this.host.maxRetryAttempts) throw new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, "max retry attempts exceeded");
199
+ let deadline;
200
+ const deadlineHeader = request.deadline;
201
+ if (deadlineHeader !== void 0) deadline = Deadline.fromUnixTimestamp(deadlineHeader.valueOf());
202
+ if (deadline !== void 0 && deadline.isExpired()) throw new TempoError(TempoStatusCode.DEADLINE_EXCEEDED, "incoming request has already exceeded its deadline");
203
+ const outgoingMetadata = new Metadata();
204
+ const incomingContext = {
205
+ headers: new Headers(),
206
+ metadata: transport.buildIncomingMetadata ? transport.buildIncomingMetadata(request) : customMetadataOf(request)
207
+ };
208
+ if (deadline !== void 0) incomingContext.deadline = deadline;
209
+ const serverContext = new ServerContext(incomingContext, { metadata: outgoingMetadata }, env);
210
+ context = serverContext;
211
+ const handleRequest = async () => {
212
+ let recordGenerator = void 0;
213
+ let record;
214
+ switch (method.type) {
215
+ case MethodType.Unary:
216
+ record = await this.invokeUnaryMethod(request, serverContext, method, contentType);
217
+ break;
218
+ case MethodType.ClientStream:
219
+ record = await this.invokeClientStreamMethod(request, serverContext, method, contentType);
220
+ break;
221
+ case MethodType.ServerStream:
222
+ recordGenerator = transport.invokeServerStream ? await transport.invokeServerStream(request, serverContext, method) : await this.invokeServerStreamMethod(request, serverContext, method, contentType);
223
+ break;
224
+ case MethodType.DuplexStream:
225
+ recordGenerator = await this.invokeDuplexStreamMethod(request, serverContext, method, contentType);
226
+ break;
227
+ default: throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: unknown method type");
228
+ }
229
+ const outgoingCredential = serverContext.outgoingCredential;
230
+ if (outgoingCredential) response.credential = stringifyCredential(outgoingCredential);
231
+ response.methodId = request.methodId;
232
+ response.messageId = request.messageId;
233
+ response.status = TempoStatusCode.OK;
234
+ response.timestamp = /* @__PURE__ */ new Date();
235
+ transport.decorateResponse?.(response);
236
+ const hooks = this.host.getHooks();
237
+ if (hooks !== void 0) await hooks.executeResponseHooks(serverContext);
238
+ outgoingMetadata.freeze();
239
+ if (outgoingMetadata.size() > 0) response.customMetadata = outgoingMetadata.data;
240
+ if (recordGenerator !== void 0) {
241
+ const generator = recordGenerator;
242
+ const writeFrames = () => transport.writeStreamFrames(generator, response, method);
243
+ if (deadline) await deadline.executeWithinDeadline(writeFrames);
244
+ else await writeFrames();
245
+ } else {
246
+ if (record === void 0) throw new TempoError(TempoStatusCode.INTERNAL, "service method did not return a record");
247
+ const responseData = this.host.serializeResponse(record, method, contentType);
248
+ response.data = new Uint8Array(responseData);
249
+ await transport.send(response);
250
+ }
251
+ };
252
+ if (deadline !== void 0) await deadline.executeWithinDeadline(handleRequest);
253
+ else await handleRequest();
254
+ } catch (e) {
255
+ let status = TempoStatusCode.UNKNOWN;
256
+ let message = "unknown error";
257
+ if (e instanceof TempoError) {
258
+ status = e.status;
259
+ message = e.message;
260
+ if (e.status === TempoStatusCode.INTERNAL && this.host.transmitInternalErrors !== true) message = "internal error";
261
+ if (e.status === TempoStatusCode.INTERNAL) this.host.logger.critical(e.message, void 0, e);
262
+ else this.host.logger.error(message, void 0, e);
263
+ } else if (e instanceof Error) {
264
+ message = e.message;
265
+ this.host.logger.error(message, void 0, e);
266
+ }
267
+ const hooks = this.host.getHooks();
268
+ if (e instanceof Error && hooks !== void 0) await hooks.executeErrorHooks(context, e);
269
+ this.clientStreams.delete(request.messageId);
270
+ response.status = status;
271
+ response.msg = message;
272
+ if (transport.stampErrorResponse) {
273
+ response.messageId = request.messageId;
274
+ response.timestamp = request.timestamp;
275
+ response.methodId = request.methodId;
276
+ }
277
+ await transport.send(response);
278
+ }
279
+ }
280
+ };
281
+ //#endregion
282
+ export { RouterCore };