@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.
@@ -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() {
@@ -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 core;
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 all open streams for this connection (driven by the DO socket
39
- * close/error lifecycle). Emits a synthetic CANCELLED to each in-flight
40
- * client/duplex stream so its generator ends, then clears all listeners.
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(): Promise<void>;
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 };
@@ -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
- core;
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
- async invokeServerStreamMethod(request, context, method) {
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 = this.serverStreams.get(messageId);
77
+ const existing = connection.serverStreams.get(messageId);
52
78
  if (existing) {
53
- this.serverStreams.delete(messageId);
54
- try {
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 invocation = await this.core.invokeServerStreamMethod(request, context, method, "bebop");
61
- this.serverStreams.set(messageId, invocation);
62
- return invocation;
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 all open streams for this connection (driven by the DO socket
66
- * close/error lifecycle). Emits a synthetic CANCELLED to each in-flight
67
- * client/duplex stream so its generator ends, then clears all listeners.
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
- async closeConnection() {
70
- this.core.cancelClientStreams();
71
- for (const [id, gen] of [...this.serverStreams]) {
72
- this.serverStreams.delete(id);
73
- try {
74
- await gen.return(void 0);
75
- } catch {}
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 this.core.processRequest(request, response, env, {
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) => this.core.writeStreamFrames({
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: () => this.serverStreams.delete(request.messageId)
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.6",
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": {