@vampgg/utils 1.0.0-beta.7 → 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/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.mjs +1 -4
- package/package.json +2 -2
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.mjs
CHANGED
|
@@ -107,10 +107,7 @@ var TempoWsRouter = class extends BaseRouter {
|
|
|
107
107
|
connection.core.removeAllListeners();
|
|
108
108
|
}
|
|
109
109
|
async process(req, response, env) {
|
|
110
|
-
if (req.byteLength > this.maxReceiveMessageSize) {
|
|
111
|
-
this.logger.error(`inbound frame ${req.byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B; dropping`);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
110
|
+
if (req.byteLength > this.maxReceiveMessageSize) throw new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, `inbound frame ${req.byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B`);
|
|
114
111
|
const request = Message.decode(new Uint8Array(req));
|
|
115
112
|
const [, ws] = env;
|
|
116
113
|
const connection = this.connection(ws);
|
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",
|