@vampgg/utils 1.0.0-beta.6 → 1.0.0-beta.7
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/ws-router.d.mts +18 -8
- package/dist/ws-router.mjs +71 -38
- package/package.json +1 -1
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/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,53 +28,83 @@ 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
110
|
if (req.byteLength > this.maxReceiveMessageSize) {
|
|
@@ -83,10 +113,11 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
83
113
|
}
|
|
84
114
|
const request = Message.decode(new Uint8Array(req));
|
|
85
115
|
const [, ws] = env;
|
|
116
|
+
const connection = this.connection(ws);
|
|
86
117
|
const send = (message) => {
|
|
87
118
|
ws.send(new Uint8Array(Message.encode(message)));
|
|
88
119
|
};
|
|
89
|
-
await
|
|
120
|
+
await connection.core.processRequest(request, response, env, {
|
|
90
121
|
send,
|
|
91
122
|
buildIncomingMetadata: (message) => {
|
|
92
123
|
const incomingMetadata = new Metadata();
|
|
@@ -94,8 +125,8 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
94
125
|
if (message.methodId !== void 0) incomingMetadata.set(STREAM_METHOD_ID_KEY, String(message.methodId));
|
|
95
126
|
return incomingMetadata;
|
|
96
127
|
},
|
|
97
|
-
invokeServerStream: (message, context, method) => this.invokeServerStreamMethod(message, context, method),
|
|
98
|
-
writeStreamFrames: (generator, resp, method) =>
|
|
128
|
+
invokeServerStream: (message, context, method) => this.invokeServerStreamMethod(connection, message, context, method),
|
|
129
|
+
writeStreamFrames: (generator, resp, method) => connection.core.writeStreamFrames({
|
|
99
130
|
generator,
|
|
100
131
|
response: resp,
|
|
101
132
|
method,
|
|
@@ -108,7 +139,9 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
108
139
|
}
|
|
109
140
|
return payload;
|
|
110
141
|
},
|
|
111
|
-
onEnd: () =>
|
|
142
|
+
onEnd: () => {
|
|
143
|
+
if (connection.serverStreams.get(request.messageId)?.generator === generator) connection.serverStreams.delete(request.messageId);
|
|
144
|
+
}
|
|
112
145
|
})
|
|
113
146
|
});
|
|
114
147
|
}
|
|
@@ -117,4 +150,4 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
117
150
|
}
|
|
118
151
|
};
|
|
119
152
|
//#endregion
|
|
120
|
-
export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame };
|
|
153
|
+
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.7",
|
|
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": {
|