@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.
- package/README.md +20 -1
- package/dist/channel-core.d.mts +139 -0
- package/dist/channel-core.mjs +411 -0
- package/dist/extension-channel.d.mts +14 -70
- package/dist/extension-channel.mjs +17 -327
- package/dist/extension-router.d.mts +5 -7
- package/dist/extension-router.mjs +35 -202
- package/dist/router-core.d.mts +121 -0
- package/dist/router-core.mjs +282 -0
- package/dist/worker-channel.d.mts +14 -70
- package/dist/worker-channel.mjs +18 -335
- package/dist/worker-router.d.mts +1 -7
- package/dist/worker-router.mjs +40 -216
- package/dist/ws-channel.d.mts +17 -48
- package/dist/ws-channel.mjs +24 -335
- package/dist/ws-router.d.mts +1 -6
- package/dist/ws-router.mjs +46 -204
- package/package.json +3 -9
- package/dist/debug-logger.d.mts +0 -43
- package/dist/debug-logger.mjs +0 -122
- package/dist/error.d.mts +0 -27
- package/dist/error.mjs +0 -49
- package/dist/memory-storage.d.mts +0 -12
- package/dist/memory-storage.mjs +0 -18
- package/dist/sync-storage.d.mts +0 -11
- package/dist/sync-storage.mjs +0 -18
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Message } from "./bebop.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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.
|
|
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 };
|