@vampgg/utils 1.0.0-beta.1

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.
@@ -0,0 +1,278 @@
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 { BaseRouter, ServerContext, TempoRouterConfiguration } from "@tempojs/server";
6
+ //#region src/ws-router.ts
7
+ /**
8
+ * Client-metadata keys under which {@link TempoWsRouter.process} exposes the
9
+ * current call's message/method ids to a service handler (read via
10
+ * `context.clientMetadata`). A streaming handler persists these so the
11
+ * hibernation-safe broadcast path can re-frame server->client pushes.
12
+ */
13
+ const STREAM_MESSAGE_ID_KEY = "tempo-message-id";
14
+ const STREAM_METHOD_ID_KEY = "tempo-method-id";
15
+ /**
16
+ * Build an encoded server-stream frame identical to the one {@link TempoWsRouter}
17
+ * emits from a streaming handler: status OK, the given `methodId`/`messageId`, and
18
+ * the already-serialized record bytes `data`. Used by the hibernation-safe
19
+ * broadcast path to push frames over a live socket WITHOUT a live generator — the
20
+ * client's server-stream iterator matches them by `messageId` exactly like
21
+ * generator-emitted frames. `messageId` MUST be the original observe call's id
22
+ * (a GUID); `methodId` its method id.
23
+ */
24
+ function encodeServerStreamFrame(opts) {
25
+ return new Uint8Array(Message.encode(Message({
26
+ methodId: opts.methodId,
27
+ messageId: opts.messageId,
28
+ status: TempoStatusCode.OK,
29
+ data: opts.data
30
+ })));
31
+ }
32
+ var TempoWsRouter = class extends BaseRouter {
33
+ events = new EventEmitter();
34
+ clientStreams = /* @__PURE__ */ new Map();
35
+ serverStreams = /* @__PURE__ */ new Map();
36
+ _streamEncodeCache = /* @__PURE__ */ new WeakMap();
37
+ constructor(logger, registry, configuration = new TempoRouterConfiguration(), authInterceptor) {
38
+ super(logger, registry, configuration, authInterceptor);
39
+ }
40
+ async setAuthContext(request, context) {
41
+ const authHeader = request.authorization;
42
+ if (authHeader !== void 0 && this.authInterceptor !== void 0) {
43
+ const authContext = await this.authInterceptor.intercept(context, authHeader);
44
+ if (authContext !== void 0) context.authContext = authContext;
45
+ }
46
+ }
47
+ async invokeUnaryMethod(request, context, method, contentType) {
48
+ await this.setAuthContext(request, context);
49
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
50
+ const requestData = request.data;
51
+ const record = this.deserializeRequest(requestData, method, contentType);
52
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
53
+ return await method.invoke(record, context);
54
+ }
55
+ async invokeClientStreamMethod(request, context, method, contentType) {
56
+ await this.setAuthContext(request, context);
57
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
58
+ const messageId = request.messageId;
59
+ const stream = this.clientStreams.get(messageId);
60
+ if (stream) {
61
+ this.events.emit(messageId, request);
62
+ return await stream;
63
+ }
64
+ const generator = () => {
65
+ return createEventIterator(({ emit, cancel }) => {
66
+ const eventHandler = async (message) => {
67
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
68
+ if (message.status === TempoStatusCode.CANCELLED) {
69
+ cancel();
70
+ return;
71
+ }
72
+ const requestData = message.data;
73
+ const record = this.deserializeRequest(requestData, method, contentType);
74
+ if (this.hooks !== void 0) await this.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 messageId = request.messageId;
94
+ if (request.status === TempoStatusCode.CANCELLED) {
95
+ const existing = this.serverStreams.get(messageId);
96
+ if (existing) {
97
+ this.serverStreams.delete(messageId);
98
+ try {
99
+ await existing.return(void 0);
100
+ } catch {}
101
+ }
102
+ return (async function* () {})();
103
+ }
104
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
105
+ const requestData = request.data;
106
+ const record = this.deserializeRequest(requestData, method, contentType);
107
+ if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
108
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
109
+ const invocation = method.invoke(record, context);
110
+ this.serverStreams.set(messageId, invocation);
111
+ return invocation;
112
+ }
113
+ async invokeDuplexStreamMethod(request, context, method, contentType) {
114
+ await this.setAuthContext(request, context);
115
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
116
+ const messageId = request.messageId;
117
+ const stream = this.clientStreams.get(messageId);
118
+ if (stream) {
119
+ this.events.emit(messageId, request);
120
+ return stream;
121
+ }
122
+ if (!TempoUtil.isAsyncGeneratorFunction(method.invoke.bind(method))) throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: method must be async generator");
123
+ const generator = () => {
124
+ return createEventIterator(({ emit, cancel }) => {
125
+ const eventHandler = async (message) => {
126
+ if (this.hooks !== void 0) await this.hooks.executeRequestHooks(context);
127
+ if (message.status === TempoStatusCode.CANCELLED) {
128
+ cancel();
129
+ return;
130
+ }
131
+ const requestData = message.data;
132
+ const record = this.deserializeRequest(requestData, method, contentType);
133
+ if (this.hooks !== void 0) await this.hooks.executeDecodeHooks(context, record);
134
+ emit(record);
135
+ };
136
+ this.events.on(messageId, eventHandler);
137
+ this.events.emit(messageId, request);
138
+ return () => {
139
+ this.events.off(messageId, eventHandler);
140
+ this.clientStreams.delete(messageId);
141
+ };
142
+ });
143
+ };
144
+ const invocation = method.invoke(generator, context);
145
+ this.clientStreams.set(messageId, invocation);
146
+ return invocation;
147
+ }
148
+ /**
149
+ * Tear down all open streams for this connection (driven by the DO socket
150
+ * close/error lifecycle). Emits a synthetic CANCELLED to each in-flight
151
+ * client/duplex stream so its generator ends, then clears all listeners.
152
+ */
153
+ async closeConnection() {
154
+ for (const id of [...this.clientStreams.keys()]) {
155
+ this.events.emit(id, Message({
156
+ messageId: id,
157
+ status: TempoStatusCode.CANCELLED,
158
+ data: new Uint8Array()
159
+ }));
160
+ this.clientStreams.delete(id);
161
+ }
162
+ for (const [id, gen] of [...this.serverStreams]) {
163
+ this.serverStreams.delete(id);
164
+ try {
165
+ await gen.return(void 0);
166
+ } catch {}
167
+ }
168
+ this.events.removeAllListeners();
169
+ }
170
+ async process(req, response, env) {
171
+ if (req.byteLength > this.maxReceiveMessageSize) {
172
+ this.logger.error(`inbound frame ${req.byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B; dropping`);
173
+ return;
174
+ }
175
+ let request = Message.decode(new Uint8Array(req));
176
+ const [, ws] = env;
177
+ try {
178
+ const methodId = request.methodId;
179
+ const method = this.registry.getMethod(methodId);
180
+ if (!method) throw new TempoError(TempoStatusCode.NOT_FOUND, `no service is registered which contains a method of '${methodId}'`);
181
+ const previousAttempts = request.previousAttempts;
182
+ if (previousAttempts !== void 0) {
183
+ if (previousAttempts > this.maxRetryAttempts) throw new TempoError(TempoStatusCode.RESOURCE_EXHAUSTED, "max retry attempts exceeded");
184
+ }
185
+ let deadline;
186
+ const deadlineHeader = request.deadline;
187
+ if (deadlineHeader !== void 0) deadline = Deadline.fromUnixTimestamp(deadlineHeader.valueOf());
188
+ if (deadline !== void 0 && deadline.isExpired()) throw new TempoError(TempoStatusCode.DEADLINE_EXCEEDED, "incoming request has already exceeded its deadline");
189
+ const outgoingMetadata = new Metadata();
190
+ const incomingMetadata = new Metadata();
191
+ if (request.messageId !== void 0) incomingMetadata.set(STREAM_MESSAGE_ID_KEY, request.messageId);
192
+ if (request.methodId !== void 0) incomingMetadata.set(STREAM_METHOD_ID_KEY, String(request.methodId));
193
+ const incomingContext = {
194
+ headers: new Headers(),
195
+ metadata: incomingMetadata
196
+ };
197
+ if (deadline !== void 0) incomingContext.deadline = deadline;
198
+ const context = new ServerContext(incomingContext, { metadata: outgoingMetadata }, env);
199
+ const handleRequest = async () => {
200
+ let recordGenerator = void 0;
201
+ let record;
202
+ switch (method.type) {
203
+ case MethodType.Unary:
204
+ record = await this.invokeUnaryMethod(request, context, method, "bebop");
205
+ break;
206
+ case MethodType.ClientStream:
207
+ record = await this.invokeClientStreamMethod(request, context, method, "bebop");
208
+ break;
209
+ case MethodType.ServerStream:
210
+ recordGenerator = await this.invokeServerStreamMethod(request, context, method, "bebop");
211
+ break;
212
+ case MethodType.DuplexStream:
213
+ recordGenerator = await this.invokeDuplexStreamMethod(request, context, method, "bebop");
214
+ break;
215
+ default: throw new TempoError(TempoStatusCode.INTERNAL, "service method incorrect: unknown method type");
216
+ }
217
+ const outgoingCredential = context.outgoingCredential;
218
+ if (outgoingCredential) response.credential = stringifyCredential(outgoingCredential);
219
+ response.methodId = request.methodId;
220
+ response.messageId = request.messageId;
221
+ response.status = TempoStatusCode.OK;
222
+ response.timestamp = /* @__PURE__ */ new Date();
223
+ if (this.hooks !== void 0) await this.hooks.executeResponseHooks(context);
224
+ outgoingMetadata.freeze();
225
+ if (outgoingMetadata.size() > 0) response.customMetadata = outgoingMetadata.data;
226
+ if (recordGenerator !== void 0) {
227
+ const writeFrames = async () => {
228
+ for await (const value of recordGenerator) {
229
+ let payload = this._streamEncodeCache.get(value);
230
+ if (payload === void 0) {
231
+ payload = new Uint8Array(this.serializeResponse(value, method, "bebop"));
232
+ this._streamEncodeCache.set(value, payload);
233
+ }
234
+ response.data = payload;
235
+ ws.send(new Uint8Array(Message.encode(response)));
236
+ }
237
+ this.serverStreams.delete(request.messageId);
238
+ response.data = new Uint8Array();
239
+ response.status = TempoStatusCode.CANCELLED;
240
+ ws.send(new Uint8Array(Message.encode(response)));
241
+ };
242
+ if (deadline) await deadline.executeWithinDeadline(writeFrames);
243
+ else await writeFrames();
244
+ } else {
245
+ if (record === void 0) throw new TempoError(TempoStatusCode.INTERNAL, "service method did not return a record");
246
+ const responseData = this.serializeResponse(record, method, "bebop");
247
+ response.data = new Uint8Array(responseData);
248
+ ws.send(new Uint8Array(Message.encode(response)));
249
+ }
250
+ };
251
+ if (deadline !== void 0) await deadline.executeWithinDeadline(handleRequest);
252
+ else await handleRequest();
253
+ } catch (e) {
254
+ let status = TempoStatusCode.UNKNOWN;
255
+ let message = "unknown error";
256
+ if (e instanceof TempoError) {
257
+ status = e.status;
258
+ message = e.message;
259
+ if (e.status === TempoStatusCode.INTERNAL && this.transmitInternalErrors !== true) message = "internal error";
260
+ if (e.status === TempoStatusCode.INTERNAL) this.logger.critical(e.message, void 0, e);
261
+ else this.logger.error(message, void 0, e);
262
+ } else if (e instanceof Error) {
263
+ message = e.message;
264
+ this.logger.error(message, void 0, e);
265
+ }
266
+ if (e instanceof Error && this.hooks !== void 0) await this.hooks.executeErrorHooks(void 0, e);
267
+ this.clientStreams.delete(request.messageId);
268
+ response.status = status;
269
+ response.msg = message;
270
+ ws.send(new Uint8Array(Message.encode(response)));
271
+ }
272
+ }
273
+ handle(_request, _env) {
274
+ throw new TempoError(TempoStatusCode.UNIMPLEMENTED, "Method not implemented.");
275
+ }
276
+ };
277
+ //#endregion
278
+ export { STREAM_MESSAGE_ID_KEY, STREAM_METHOD_ID_KEY, TempoWsRouter, encodeServerStreamFrame };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@vampgg/utils",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Tempo RPC, Bebop serialization, transports, and async primitives for @vampgg.",
5
+ "homepage": "https://github.com/sammccord/vamp/tree/main/packages/utils#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/sammccord/vamp/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Sam McCord <sam.mccord@protonmail.com>",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/sammccord/vamp.git",
14
+ "directory": "packages/utils"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "schema"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ "./async-queue": "./dist/async-queue.mjs",
23
+ "./bebop": "./dist/bebop.mjs",
24
+ "./context-logger": "./dist/context-logger.mjs",
25
+ "./create-duplex-iterator": "./dist/create-duplex-iterator.mjs",
26
+ "./create-event-iterator": "./dist/create-event-iterator.mjs",
27
+ "./debug-logger": "./dist/debug-logger.mjs",
28
+ "./error": "./dist/error.mjs",
29
+ "./extension-channel": "./dist/extension-channel.mjs",
30
+ "./extension-router": "./dist/extension-router.mjs",
31
+ "./memory-storage": "./dist/memory-storage.mjs",
32
+ "./pino-logger": "./dist/pino-logger.mjs",
33
+ "./sync-storage": "./dist/sync-storage.mjs",
34
+ "./worker-channel": "./dist/worker-channel.mjs",
35
+ "./worker-router": "./dist/worker-router.mjs",
36
+ "./ws-channel": "./dist/ws-channel.mjs",
37
+ "./ws-router": "./dist/ws-router.mjs",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@tempojs/client": "^0.0.12",
45
+ "@tempojs/common": "^0.0.12",
46
+ "@tempojs/server": "^0.0.12",
47
+ "bebop": "3.2.3",
48
+ "debug": "^4.4.3",
49
+ "lodash-es": "^4.17.23",
50
+ "pino": "^10.3.1",
51
+ "tseep": "^1.3.1",
52
+ "webextension-polyfill": "^0.12.0",
53
+ "websocket-ts": "^2.2.1"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "^1.3.14",
57
+ "@types/debug": "^4.1.12",
58
+ "@types/lodash-es": "^4.17.12",
59
+ "@types/node": "^25.5.0",
60
+ "@types/webextension-polyfill": "^0.12.5",
61
+ "@typescript/native-preview": "7.0.0-dev.20260315.1",
62
+ "bebop-tools": "^3.2.3",
63
+ "typescript": "^5.9.3",
64
+ "vite-plus": "^0.1.24",
65
+ "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
66
+ },
67
+ "scripts": {
68
+ "build": "bebopc build && vp pack",
69
+ "dev": "bebopc build && vp pack --watch",
70
+ "test": "vp test",
71
+ "typecheck": "tsc --noEmit"
72
+ }
73
+ }
@@ -0,0 +1,7 @@
1
+ struct Error {
2
+ uint32 code;
3
+ string tag;
4
+ string stack;
5
+ string msg;
6
+ map[string, string] details;
7
+ }
@@ -0,0 +1,2 @@
1
+ import "./error.bop"
2
+ import "./message.bop"
@@ -0,0 +1,12 @@
1
+ message Message {
2
+ 1 -> uint32 methodId;
3
+ 2 -> guid messageId;
4
+ 3 -> date timestamp;
5
+ 4 -> date deadline;
6
+ 5 -> byte[] data;
7
+ 6 -> string credential;
8
+ 7 -> byte status;
9
+ 8 -> uint8 previousAttempts;
10
+ 9 -> string msg;
11
+ 10 -> string authorization;
12
+ }
@@ -0,0 +1,16 @@
1
+ message Pool {
2
+ 1 -> uint32 points;
3
+ 2 -> uint32 min;
4
+ 3 -> uint32 max;
5
+ 4 -> int32 rate;
6
+ 5 -> uint32 interval;
7
+ }
8
+
9
+ // Pool delta - points is CRDT counter, others are last-write-wins
10
+ message PoolDelta {
11
+ 1 -> int32 points; // Delta for CRDT counter (+/-)
12
+ 2 -> int32 min; // Delta for CRDT counter (+/-)
13
+ 3 -> int32 max; // Delta for CRDT counter (+/-)
14
+ 4 -> int32 rate; // Delta for CRDT counter (+/-)
15
+ 5 -> int32 interval; // Delta for CRDT counter (+/-)
16
+ }