@vampgg/utils 1.0.0-beta.6 → 1.0.0-beta.8
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/dist/async-queue.mjs +3 -3
- package/dist/channel-core.d.mts +9 -2
- package/dist/channel-core.mjs +32 -4
- package/dist/extension-router.mjs +0 -1
- package/dist/router-core.d.mts +8 -4
- package/dist/router-core.mjs +25 -17
- package/dist/worker-router.mjs +0 -1
- package/dist/ws-channel.d.mts +1 -2
- package/dist/ws-channel.mjs +7 -6
- package/dist/ws-router.d.mts +18 -8
- package/dist/ws-router.mjs +72 -42
- package/package.json +2 -2
package/dist/async-queue.mjs
CHANGED
|
@@ -55,10 +55,10 @@ var AsyncQueue = class {
|
|
|
55
55
|
const resolve = this.pendingResolve;
|
|
56
56
|
this.pendingResolve = null;
|
|
57
57
|
this.pendingReject = null;
|
|
58
|
-
resolve({
|
|
58
|
+
this.dispose().finally(() => resolve({
|
|
59
59
|
value: void 0,
|
|
60
60
|
done: true
|
|
61
|
-
});
|
|
61
|
+
}));
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
/** Producer: terminate the stream with an error thrown into the consumer. Idempotent. */
|
|
@@ -70,7 +70,7 @@ var AsyncQueue = class {
|
|
|
70
70
|
const reject = this.pendingReject;
|
|
71
71
|
this.pendingResolve = null;
|
|
72
72
|
this.pendingReject = null;
|
|
73
|
-
reject(cause);
|
|
73
|
+
this.dispose().finally(() => reject(cause));
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
async next() {
|
package/dist/channel-core.d.mts
CHANGED
|
@@ -25,8 +25,9 @@ declare abstract class CoreChannel extends BaseChannel {
|
|
|
25
25
|
/** Per-call response listeners keyed by messageId (response demultiplexing). */
|
|
26
26
|
readonly events: EventEmitter<Record<string, (message: Message) => void>>;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* messageId. Lets close()
|
|
28
|
+
* Failure callbacks for every in-flight call (unary, client-stream, and the
|
|
29
|
+
* server/duplex streams), keyed by messageId. Lets close() and a dropped
|
|
30
|
+
* connection settle them instead of leaving them parked.
|
|
30
31
|
*/
|
|
31
32
|
readonly pending: Map<string, (cause: unknown) => void>;
|
|
32
33
|
protected closed: boolean;
|
|
@@ -74,6 +75,12 @@ declare abstract class CoreChannel extends BaseChannel {
|
|
|
74
75
|
* explicit caller.
|
|
75
76
|
*/
|
|
76
77
|
close(reason?: TempoError): void;
|
|
78
|
+
/**
|
|
79
|
+
* Fail every in-flight call and stream with `reason` without disposing the
|
|
80
|
+
* channel. A transport that can reconnect calls this when its connection
|
|
81
|
+
* drops; the next call reconnects.
|
|
82
|
+
*/
|
|
83
|
+
protected failInFlight(reason: TempoError): void;
|
|
77
84
|
removeCredential(): Promise<void>;
|
|
78
85
|
getCredential(): Promise<Credential | undefined>;
|
|
79
86
|
/**
|
package/dist/channel-core.mjs
CHANGED
|
@@ -25,8 +25,9 @@ var CoreChannel = class extends BaseChannel {
|
|
|
25
25
|
/** Per-call response listeners keyed by messageId (response demultiplexing). */
|
|
26
26
|
events = new EventEmitter();
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* messageId. Lets close()
|
|
28
|
+
* Failure callbacks for every in-flight call (unary, client-stream, and the
|
|
29
|
+
* server/duplex streams), keyed by messageId. Lets close() and a dropped
|
|
30
|
+
* connection settle them instead of leaving them parked.
|
|
30
31
|
*/
|
|
31
32
|
pending = /* @__PURE__ */ new Map();
|
|
32
33
|
closed = false;
|
|
@@ -80,11 +81,20 @@ var CoreChannel = class extends BaseChannel {
|
|
|
80
81
|
const err = reason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed");
|
|
81
82
|
this.closeReason = err;
|
|
82
83
|
this.onClosing(err);
|
|
83
|
-
|
|
84
|
-
this.pending.clear();
|
|
84
|
+
this.failInFlight(err);
|
|
85
85
|
this.events.removeAllListeners();
|
|
86
86
|
this.closeTransport();
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Fail every in-flight call and stream with `reason` without disposing the
|
|
90
|
+
* channel. A transport that can reconnect calls this when its connection
|
|
91
|
+
* drops; the next call reconnects.
|
|
92
|
+
*/
|
|
93
|
+
failInFlight(reason) {
|
|
94
|
+
const failures = [...this.pending.values()];
|
|
95
|
+
this.pending.clear();
|
|
96
|
+
for (const fail of failures) fail(reason);
|
|
97
|
+
}
|
|
88
98
|
async removeCredential() {}
|
|
89
99
|
async getCredential() {}
|
|
90
100
|
/**
|
|
@@ -180,8 +190,16 @@ var CoreChannel = class extends BaseChannel {
|
|
|
180
190
|
fetchServerStream(init, context, method, options) {
|
|
181
191
|
const messageId = init.messageId;
|
|
182
192
|
return createEventIterator(({ emit, cancel, error }) => {
|
|
193
|
+
if (this.closed) {
|
|
194
|
+
error(this.closeReason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed"));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
183
197
|
let terminated = false;
|
|
184
198
|
let onAbort;
|
|
199
|
+
this.pending.set(messageId, (cause) => {
|
|
200
|
+
terminated = true;
|
|
201
|
+
error(cause);
|
|
202
|
+
});
|
|
185
203
|
const eventHandler = async (message) => {
|
|
186
204
|
try {
|
|
187
205
|
if (message.status === TempoStatusCode.CANCELLED) {
|
|
@@ -204,6 +222,7 @@ var CoreChannel = class extends BaseChannel {
|
|
|
204
222
|
this.events.on(messageId, eventHandler);
|
|
205
223
|
this.sendFrame(init);
|
|
206
224
|
return () => {
|
|
225
|
+
this.pending.delete(messageId);
|
|
207
226
|
this.events.off(messageId, eventHandler);
|
|
208
227
|
if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
|
|
209
228
|
if (terminated || this.closed) return;
|
|
@@ -225,8 +244,16 @@ var CoreChannel = class extends BaseChannel {
|
|
|
225
244
|
data: this.serializeStreamItem(method, value)
|
|
226
245
|
}));
|
|
227
246
|
}, ({ emit, cancel, error }) => {
|
|
247
|
+
if (this.closed) {
|
|
248
|
+
error(this.closeReason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
228
251
|
let terminated = false;
|
|
229
252
|
let onAbort;
|
|
253
|
+
this.pending.set(messageId, (cause) => {
|
|
254
|
+
terminated = true;
|
|
255
|
+
error(cause);
|
|
256
|
+
});
|
|
230
257
|
const eventHandler = async (message) => {
|
|
231
258
|
try {
|
|
232
259
|
if (message.status === TempoStatusCode.CANCELLED) {
|
|
@@ -248,6 +275,7 @@ var CoreChannel = class extends BaseChannel {
|
|
|
248
275
|
}
|
|
249
276
|
this.events.on(messageId, eventHandler);
|
|
250
277
|
return () => {
|
|
278
|
+
this.pending.delete(messageId);
|
|
251
279
|
this.events.off(messageId, eventHandler);
|
|
252
280
|
if (onAbort) options?.controller?.signal.removeEventListener("abort", onAbort);
|
|
253
281
|
if (this.cancelDuplexOnCleanup && !terminated && !this.closed) this.sendFrame(Message({
|
|
@@ -177,7 +177,6 @@ var TempoExtensionRouter = class extends ExtensionBaseRouter {
|
|
|
177
177
|
const send = (message) => this.send(ctx.sender, message);
|
|
178
178
|
await this.core.processRequest(request, response, ctx, {
|
|
179
179
|
send,
|
|
180
|
-
stampErrorResponse: true,
|
|
181
180
|
decorateResponse: (message) => {
|
|
182
181
|
message.msg = "";
|
|
183
182
|
},
|
package/dist/router-core.d.mts
CHANGED
|
@@ -47,9 +47,13 @@ interface ProcessTransport {
|
|
|
47
47
|
invokeServerStream?(request: Message, context: ServerContext, method: BebopMethodAny): Promise<AsyncGenerator<BebopRecord, void, unknown>>;
|
|
48
48
|
/** Mutate the response envelope after the OK headers are populated. */
|
|
49
49
|
decorateResponse?(response: Message): void;
|
|
50
|
-
/** Stamp messageId/timestamp/methodId from the request onto error frames. */
|
|
51
|
-
stampErrorResponse?: boolean;
|
|
52
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Returned by the stream invokers for a frame that continues an already-open
|
|
53
|
+
* client or duplex stream. The frame is delivered to the open invocation, and
|
|
54
|
+
* no response is sent for it: the call that opened the stream owns the reply.
|
|
55
|
+
*/
|
|
56
|
+
declare const CONTINUATION: unique symbol;
|
|
53
57
|
/** Options for {@link RouterCore.writeStreamFrames}. */
|
|
54
58
|
interface StreamWriteOptions {
|
|
55
59
|
generator: AsyncGenerator<BebopRecord, void, unknown>;
|
|
@@ -94,9 +98,9 @@ declare class RouterCore {
|
|
|
94
98
|
constructor(host: RouterCoreHost);
|
|
95
99
|
private setAuthContext;
|
|
96
100
|
invokeUnaryMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<BebopRecord>;
|
|
97
|
-
invokeClientStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<BebopRecord>;
|
|
101
|
+
invokeClientStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<BebopRecord | typeof CONTINUATION>;
|
|
98
102
|
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
|
|
103
|
+
invokeDuplexStreamMethod(request: Message, context: ServerContext, method: BebopMethodAny, contentType: BebopContentType): Promise<AsyncGenerator<BebopRecord, void, unknown> | typeof CONTINUATION>;
|
|
100
104
|
/**
|
|
101
105
|
* Emit a synthetic CANCELLED to every in-flight client/duplex stream so its
|
|
102
106
|
* generator ends, and drop the tracking entries (connection teardown).
|
package/dist/router-core.mjs
CHANGED
|
@@ -5,6 +5,12 @@ import { EventEmitter } from "tseep";
|
|
|
5
5
|
import { ServerContext } from "@tempojs/server";
|
|
6
6
|
//#region src/router-core.ts
|
|
7
7
|
/**
|
|
8
|
+
* Returned by the stream invokers for a frame that continues an already-open
|
|
9
|
+
* client or duplex stream. The frame is delivered to the open invocation, and
|
|
10
|
+
* no response is sent for it: the call that opened the stream owns the reply.
|
|
11
|
+
*/
|
|
12
|
+
const CONTINUATION = Symbol("continuation");
|
|
13
|
+
/**
|
|
8
14
|
* Parse the request's custom-metadata header into a {@link Metadata} (the
|
|
9
15
|
* default incoming metadata for the worker/extension routers).
|
|
10
16
|
*/
|
|
@@ -55,10 +61,9 @@ var RouterCore = class {
|
|
|
55
61
|
await this.setAuthContext(request, context);
|
|
56
62
|
if (this.host.getHooks() !== void 0) await this.host.getHooks().executeRequestHooks(context);
|
|
57
63
|
const messageId = request.messageId;
|
|
58
|
-
|
|
59
|
-
if (stream) {
|
|
64
|
+
if (this.clientStreams.has(messageId)) {
|
|
60
65
|
this.events.emit(messageId, request);
|
|
61
|
-
return
|
|
66
|
+
return CONTINUATION;
|
|
62
67
|
}
|
|
63
68
|
const generator = () => {
|
|
64
69
|
return createEventIterator(({ emit, cancel }) => {
|
|
@@ -102,10 +107,9 @@ var RouterCore = class {
|
|
|
102
107
|
await this.setAuthContext(request, context);
|
|
103
108
|
if (this.host.getHooks() !== void 0) await this.host.getHooks().executeRequestHooks(context);
|
|
104
109
|
const messageId = request.messageId;
|
|
105
|
-
|
|
106
|
-
if (stream) {
|
|
110
|
+
if (this.clientStreams.has(messageId)) {
|
|
107
111
|
this.events.emit(messageId, request);
|
|
108
|
-
return
|
|
112
|
+
return CONTINUATION;
|
|
109
113
|
}
|
|
110
114
|
if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
|
|
111
115
|
const generator = () => {
|
|
@@ -215,15 +219,21 @@ var RouterCore = class {
|
|
|
215
219
|
case MethodType.Unary:
|
|
216
220
|
record = await this.invokeUnaryMethod(request, serverContext, method, contentType);
|
|
217
221
|
break;
|
|
218
|
-
case MethodType.ClientStream:
|
|
219
|
-
|
|
222
|
+
case MethodType.ClientStream: {
|
|
223
|
+
const result = await this.invokeClientStreamMethod(request, serverContext, method, contentType);
|
|
224
|
+
if (result === CONTINUATION) return;
|
|
225
|
+
record = result;
|
|
220
226
|
break;
|
|
227
|
+
}
|
|
221
228
|
case MethodType.ServerStream:
|
|
222
229
|
recordGenerator = transport.invokeServerStream ? await transport.invokeServerStream(request, serverContext, method) : await this.invokeServerStreamMethod(request, serverContext, method, contentType);
|
|
223
230
|
break;
|
|
224
|
-
case MethodType.DuplexStream:
|
|
225
|
-
|
|
231
|
+
case MethodType.DuplexStream: {
|
|
232
|
+
const result = await this.invokeDuplexStreamMethod(request, serverContext, method, contentType);
|
|
233
|
+
if (result === CONTINUATION) return;
|
|
234
|
+
recordGenerator = result;
|
|
226
235
|
break;
|
|
236
|
+
}
|
|
227
237
|
default: throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: unknown method type");
|
|
228
238
|
}
|
|
229
239
|
const outgoingCredential = serverContext.outgoingCredential;
|
|
@@ -261,19 +271,17 @@ var RouterCore = class {
|
|
|
261
271
|
if (e.status === TempoStatusCode.INTERNAL) this.host.logger.critical(e.message, void 0, e);
|
|
262
272
|
else this.host.logger.error(message, void 0, e);
|
|
263
273
|
} else if (e instanceof Error) {
|
|
264
|
-
message
|
|
265
|
-
this.host.
|
|
274
|
+
this.host.logger.error(e.message, void 0, e);
|
|
275
|
+
message = this.host.transmitInternalErrors === true ? e.message : "internal error";
|
|
266
276
|
}
|
|
267
277
|
const hooks = this.host.getHooks();
|
|
268
278
|
if (e instanceof Error && hooks !== void 0) await hooks.executeErrorHooks(context, e);
|
|
269
279
|
this.clientStreams.delete(request.messageId);
|
|
270
280
|
response.status = status;
|
|
271
281
|
response.msg = message;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
response.methodId = request.methodId;
|
|
276
|
-
}
|
|
282
|
+
response.messageId = request.messageId;
|
|
283
|
+
response.timestamp = request.timestamp;
|
|
284
|
+
response.methodId = request.methodId;
|
|
277
285
|
await transport.send(response);
|
|
278
286
|
}
|
|
279
287
|
}
|
package/dist/worker-router.mjs
CHANGED
|
@@ -50,7 +50,6 @@ var TempoWorkerRouter = class extends BaseRouter {
|
|
|
50
50
|
};
|
|
51
51
|
await this.core.processRequest(request, response, env, {
|
|
52
52
|
send,
|
|
53
|
-
stampErrorResponse: true,
|
|
54
53
|
writeStreamFrames: async (generator, resp, method) => {
|
|
55
54
|
const messageId = request.messageId;
|
|
56
55
|
const streamInfo = {
|
package/dist/ws-channel.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Message } from "./bebop.mjs";
|
|
2
2
|
import { CoreChannel } from "./channel-core.mjs";
|
|
3
3
|
import { CallCredential, TempoChannelOptions } from "@tempojs/client";
|
|
4
|
-
import * as _tempojs_common0 from "@tempojs/common";
|
|
5
4
|
import { BebopContentType, Credential } from "@tempojs/common";
|
|
6
5
|
import { Websocket, WebsocketOptions } from "websocket-ts";
|
|
7
6
|
|
|
@@ -27,7 +26,7 @@ declare class TempoWSChannel extends CoreChannel {
|
|
|
27
26
|
private readonly maxSendMessageSize;
|
|
28
27
|
private readonly _url;
|
|
29
28
|
private readonly _wsOptions;
|
|
30
|
-
get log():
|
|
29
|
+
get log(): import("@tempojs/common").TempoLogger;
|
|
31
30
|
/**
|
|
32
31
|
* Constructs a new TempoChannel instance.
|
|
33
32
|
*
|
package/dist/ws-channel.mjs
CHANGED
|
@@ -91,21 +91,21 @@ var TempoWSChannel = class TempoWSChannel extends CoreChannel {
|
|
|
91
91
|
this.logger.trace(`opened TempoWSChannel for ${this._url.href} / ${this.userAgent}`);
|
|
92
92
|
});
|
|
93
93
|
this.ws.addEventListener(WebsocketEvent.close, (ws) => {
|
|
94
|
-
if (this.closed) return;
|
|
94
|
+
if (this.closed || ws !== this.ws) return;
|
|
95
95
|
if (ws.lastConnection === void 0) return;
|
|
96
96
|
this.logger.trace(`closed TempoWSChannel for ${this._url.href} / ${this.userAgent}`);
|
|
97
|
-
this.
|
|
97
|
+
this.failInFlight(new TempoError(TempoStatusCode.UNAVAILABLE, "connection closed"));
|
|
98
98
|
});
|
|
99
99
|
this.ws.addEventListener(WebsocketEvent.error, (ws) => {
|
|
100
|
-
if (this.closed) return;
|
|
100
|
+
if (this.closed || ws !== this.ws) return;
|
|
101
101
|
if (ws.lastConnection === void 0) return;
|
|
102
|
-
this.
|
|
102
|
+
this.failInFlight(new TempoError(TempoStatusCode.UNAVAILABLE, "connection error"));
|
|
103
103
|
});
|
|
104
104
|
this.ws.addEventListener(WebsocketEvent.message, (_ws, ev) => {
|
|
105
105
|
const data = ev.data;
|
|
106
106
|
const byteLength = data instanceof ArrayBuffer ? data.byteLength : data.length;
|
|
107
107
|
if (byteLength > this.maxReceiveMessageSize) {
|
|
108
|
-
this.
|
|
108
|
+
this.failInFlight(new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, `inbound frame ${byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B`));
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
let message;
|
|
@@ -153,9 +153,10 @@ var TempoWSChannel = class TempoWSChannel extends CoreChannel {
|
|
|
153
153
|
await new Promise((r) => setTimeout(r, 100));
|
|
154
154
|
this._reconnect();
|
|
155
155
|
}
|
|
156
|
+
if (this.closed) throw this.closeReason ?? new TempoError(TempoStatusCode.CANCELLED, "channel disposed");
|
|
156
157
|
const state = this.ws.readyState;
|
|
157
158
|
if (state === 1) return;
|
|
158
|
-
if (state === 2 || state === 3)
|
|
159
|
+
if (state === 2 || state === 3) this._reconnect();
|
|
159
160
|
try {
|
|
160
161
|
await Deadline.after(5, "seconds").executeWithinDeadline(() => new Promise((resolve, reject) => {
|
|
161
162
|
const teardown = () => {
|
package/dist/ws-router.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Message } from "./bebop.mjs";
|
|
2
2
|
import { TempoLogger } from "@tempojs/common";
|
|
3
|
-
import { AuthInterceptor, BaseRouter, ServiceRegistry, TempoRouterConfiguration } from "@tempojs/server";
|
|
3
|
+
import { AuthInterceptor, BaseRouter, ServerContext, ServiceRegistry, TempoRouterConfiguration } from "@tempojs/server";
|
|
4
4
|
|
|
5
5
|
//#region src/ws-router.d.ts
|
|
6
6
|
interface GenericWs {
|
|
@@ -28,20 +28,30 @@ declare function encodeServerStreamFrame(opts: {
|
|
|
28
28
|
messageId: string;
|
|
29
29
|
data: Uint8Array;
|
|
30
30
|
}): Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* The abort signal of the server stream `context` belongs to, if it was opened
|
|
33
|
+
* by a {@link TempoWsRouter}. It fires when the client cancels the stream or its
|
|
34
|
+
* connection closes. A handler parked on an external event source (one that
|
|
35
|
+
* never yields on its own) must end its generator when this fires: the router
|
|
36
|
+
* cannot interrupt a generator suspended in `await`.
|
|
37
|
+
*/
|
|
38
|
+
declare function streamAbortSignal(context: ServerContext): AbortSignal | undefined;
|
|
31
39
|
declare class TempoWsRouter<Context, Ws extends GenericWs = WebSocket, Env extends [Context, Ws] = [Context, Ws]> extends BaseRouter<ArrayBuffer, Env, Message> {
|
|
32
|
-
private readonly
|
|
33
|
-
private readonly serverStreams;
|
|
40
|
+
private readonly connections;
|
|
34
41
|
private readonly _streamEncodeCache;
|
|
35
42
|
constructor(logger: TempoLogger, registry: ServiceRegistry, configuration?: TempoRouterConfiguration, authInterceptor?: AuthInterceptor);
|
|
43
|
+
private connection;
|
|
36
44
|
private invokeServerStreamMethod;
|
|
37
45
|
/**
|
|
38
|
-
* Tear down
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* Tear down every open stream on `ws` (driven by the socket close/error
|
|
47
|
+
* lifecycle). Client/duplex streams get a synthetic CANCELLED; server streams
|
|
48
|
+
* are aborted (see {@link streamAbortSignal}). Never waits on a generator, so
|
|
49
|
+
* a handler parked on an external source cannot stall teardown. Other
|
|
50
|
+
* connections are untouched.
|
|
41
51
|
*/
|
|
42
|
-
closeConnection():
|
|
52
|
+
closeConnection(ws: Ws): void;
|
|
43
53
|
process(req: ArrayBuffer, response: Message, env: Env): Promise<void>;
|
|
44
54
|
handle(_request: ArrayBuffer, _env: Env): Promise<Message>;
|
|
45
55
|
}
|
|
46
56
|
//#endregion
|
|
47
|
-
export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame };
|
|
57
|
+
export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame, streamAbortSignal };
|
package/dist/ws-router.mjs
CHANGED
|
@@ -28,65 +28,93 @@ function encodeServerStreamFrame(opts) {
|
|
|
28
28
|
data: opts.data
|
|
29
29
|
})));
|
|
30
30
|
}
|
|
31
|
+
/** Cancellation signals for live server streams, keyed by their ServerContext. */
|
|
32
|
+
const streamSignals = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* The abort signal of the server stream `context` belongs to, if it was opened
|
|
35
|
+
* by a {@link TempoWsRouter}. It fires when the client cancels the stream or its
|
|
36
|
+
* connection closes. A handler parked on an external event source (one that
|
|
37
|
+
* never yields on its own) must end its generator when this fires: the router
|
|
38
|
+
* cannot interrupt a generator suspended in `await`.
|
|
39
|
+
*/
|
|
40
|
+
function streamAbortSignal(context) {
|
|
41
|
+
return streamSignals.get(context);
|
|
42
|
+
}
|
|
43
|
+
/** End a server stream without awaiting it: `return()` queues behind a parked `next()`. */
|
|
44
|
+
function endServerStream(stream) {
|
|
45
|
+
stream.abort.abort();
|
|
46
|
+
stream.generator.return(void 0).catch(() => {});
|
|
47
|
+
}
|
|
31
48
|
var TempoWsRouter = class extends BaseRouter {
|
|
32
|
-
|
|
33
|
-
serverStreams = /* @__PURE__ */ new Map();
|
|
49
|
+
connections = /* @__PURE__ */ new WeakMap();
|
|
34
50
|
_streamEncodeCache = /* @__PURE__ */ new WeakMap();
|
|
35
51
|
constructor(logger, registry, configuration = new TempoRouterConfiguration(), authInterceptor) {
|
|
36
52
|
super(logger, registry, configuration, authInterceptor);
|
|
37
|
-
this.core = new RouterCore({
|
|
38
|
-
logger,
|
|
39
|
-
maxRetryAttempts: this.maxRetryAttempts,
|
|
40
|
-
transmitInternalErrors: this.transmitInternalErrors,
|
|
41
|
-
authInterceptor,
|
|
42
|
-
getMethod: (methodId) => this.registry.getMethod(methodId),
|
|
43
|
-
getHooks: () => this.hooks,
|
|
44
|
-
deserializeRequest: (data, method, contentType) => this.deserializeRequest(data, method, contentType),
|
|
45
|
-
serializeResponse: (record, method, contentType) => this.serializeResponse(record, method, contentType)
|
|
46
|
-
});
|
|
47
53
|
}
|
|
48
|
-
|
|
54
|
+
connection(ws) {
|
|
55
|
+
let connection = this.connections.get(ws);
|
|
56
|
+
if (!connection) {
|
|
57
|
+
connection = {
|
|
58
|
+
core: new RouterCore({
|
|
59
|
+
logger: this.logger,
|
|
60
|
+
maxRetryAttempts: this.maxRetryAttempts,
|
|
61
|
+
transmitInternalErrors: this.transmitInternalErrors,
|
|
62
|
+
authInterceptor: this.authInterceptor,
|
|
63
|
+
getMethod: (methodId) => this.registry.getMethod(methodId),
|
|
64
|
+
getHooks: () => this.hooks,
|
|
65
|
+
deserializeRequest: (data, method, contentType) => this.deserializeRequest(data, method, contentType),
|
|
66
|
+
serializeResponse: (record, method, contentType) => this.serializeResponse(record, method, contentType)
|
|
67
|
+
}),
|
|
68
|
+
serverStreams: /* @__PURE__ */ new Map()
|
|
69
|
+
};
|
|
70
|
+
this.connections.set(ws, connection);
|
|
71
|
+
}
|
|
72
|
+
return connection;
|
|
73
|
+
}
|
|
74
|
+
async invokeServerStreamMethod(connection, request, context, method) {
|
|
49
75
|
const messageId = request.messageId;
|
|
50
76
|
if (request.status === TempoStatusCode.CANCELLED) {
|
|
51
|
-
const existing =
|
|
77
|
+
const existing = connection.serverStreams.get(messageId);
|
|
52
78
|
if (existing) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await existing.return(void 0);
|
|
56
|
-
} catch {}
|
|
79
|
+
connection.serverStreams.delete(messageId);
|
|
80
|
+
endServerStream(existing);
|
|
57
81
|
}
|
|
58
82
|
return (async function* () {})();
|
|
59
83
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
const abort = new AbortController();
|
|
85
|
+
streamSignals.set(context, abort.signal);
|
|
86
|
+
const generator = await connection.core.invokeServerStreamMethod(request, context, method, "bebop");
|
|
87
|
+
connection.serverStreams.set(messageId, {
|
|
88
|
+
generator,
|
|
89
|
+
abort
|
|
90
|
+
});
|
|
91
|
+
return generator;
|
|
63
92
|
}
|
|
64
93
|
/**
|
|
65
|
-
* Tear down
|
|
66
|
-
*
|
|
67
|
-
*
|
|
94
|
+
* Tear down every open stream on `ws` (driven by the socket close/error
|
|
95
|
+
* lifecycle). Client/duplex streams get a synthetic CANCELLED; server streams
|
|
96
|
+
* are aborted (see {@link streamAbortSignal}). Never waits on a generator, so
|
|
97
|
+
* a handler parked on an external source cannot stall teardown. Other
|
|
98
|
+
* connections are untouched.
|
|
68
99
|
*/
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.core.removeAllListeners();
|
|
100
|
+
closeConnection(ws) {
|
|
101
|
+
const connection = this.connections.get(ws);
|
|
102
|
+
if (!connection) return;
|
|
103
|
+
this.connections.delete(ws);
|
|
104
|
+
connection.core.cancelClientStreams();
|
|
105
|
+
for (const stream of connection.serverStreams.values()) endServerStream(stream);
|
|
106
|
+
connection.serverStreams.clear();
|
|
107
|
+
connection.core.removeAllListeners();
|
|
78
108
|
}
|
|
79
109
|
async process(req, response, env) {
|
|
80
|
-
if (req.byteLength > this.maxReceiveMessageSize) {
|
|
81
|
-
this.logger.error(`inbound frame ${req.byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B; dropping`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
110
|
+
if (req.byteLength > this.maxReceiveMessageSize) throw new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, `inbound frame ${req.byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B`);
|
|
84
111
|
const request = Message.decode(new Uint8Array(req));
|
|
85
112
|
const [, ws] = env;
|
|
113
|
+
const connection = this.connection(ws);
|
|
86
114
|
const send = (message) => {
|
|
87
115
|
ws.send(new Uint8Array(Message.encode(message)));
|
|
88
116
|
};
|
|
89
|
-
await
|
|
117
|
+
await connection.core.processRequest(request, response, env, {
|
|
90
118
|
send,
|
|
91
119
|
buildIncomingMetadata: (message) => {
|
|
92
120
|
const incomingMetadata = new Metadata();
|
|
@@ -94,8 +122,8 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
94
122
|
if (message.methodId !== void 0) incomingMetadata.set(STREAM_METHOD_ID_KEY, String(message.methodId));
|
|
95
123
|
return incomingMetadata;
|
|
96
124
|
},
|
|
97
|
-
invokeServerStream: (message, context, method) => this.invokeServerStreamMethod(message, context, method),
|
|
98
|
-
writeStreamFrames: (generator, resp, method) =>
|
|
125
|
+
invokeServerStream: (message, context, method) => this.invokeServerStreamMethod(connection, message, context, method),
|
|
126
|
+
writeStreamFrames: (generator, resp, method) => connection.core.writeStreamFrames({
|
|
99
127
|
generator,
|
|
100
128
|
response: resp,
|
|
101
129
|
method,
|
|
@@ -108,7 +136,9 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
108
136
|
}
|
|
109
137
|
return payload;
|
|
110
138
|
},
|
|
111
|
-
onEnd: () =>
|
|
139
|
+
onEnd: () => {
|
|
140
|
+
if (connection.serverStreams.get(request.messageId)?.generator === generator) connection.serverStreams.delete(request.messageId);
|
|
141
|
+
}
|
|
112
142
|
})
|
|
113
143
|
});
|
|
114
144
|
}
|
|
@@ -117,4 +147,4 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
117
147
|
}
|
|
118
148
|
};
|
|
119
149
|
//#endregion
|
|
120
|
-
export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame };
|
|
150
|
+
export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame, streamAbortSignal };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vampgg/utils",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.8",
|
|
4
4
|
"description": "Tempo RPC, Bebop serialization, transports, and async primitives for @vampgg.",
|
|
5
5
|
"homepage": "https://github.com/sammccord/vamp/tree/main/packages/utils#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"bebop-tools": "^3.2.3",
|
|
58
58
|
"typescript": "^5.9.3",
|
|
59
59
|
"vite-plus": "^0.1.24",
|
|
60
|
-
"vitest": "npm:@voidzero-dev/vite-plus-test@
|
|
60
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "bebopc build && vp pack",
|