@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.
- package/LICENSE +21 -0
- package/README.md +467 -0
- package/dist/async-queue.d.mts +68 -0
- package/dist/async-queue.mjs +156 -0
- package/dist/bebop.d.mts +37 -0
- package/dist/bebop.mjs +434 -0
- package/dist/context-logger.d.mts +11 -0
- package/dist/context-logger.mjs +31 -0
- package/dist/create-duplex-iterator.d.mts +16 -0
- package/dist/create-duplex-iterator.mjs +36 -0
- package/dist/create-event-iterator.d.mts +39 -0
- package/dist/create-event-iterator.mjs +67 -0
- package/dist/debug-logger.d.mts +43 -0
- package/dist/debug-logger.mjs +122 -0
- package/dist/error.d.mts +27 -0
- package/dist/error.mjs +49 -0
- package/dist/extension-channel.d.mts +96 -0
- package/dist/extension-channel.mjs +377 -0
- package/dist/extension-router.d.mts +154 -0
- package/dist/extension-router.mjs +375 -0
- package/dist/memory-storage.d.mts +12 -0
- package/dist/memory-storage.mjs +18 -0
- package/dist/pino-logger.d.mts +44 -0
- package/dist/pino-logger.mjs +127 -0
- package/dist/sync-storage.d.mts +11 -0
- package/dist/sync-storage.mjs +18 -0
- package/dist/worker-channel.d.mts +99 -0
- package/dist/worker-channel.mjs +419 -0
- package/dist/worker-router.d.mts +26 -0
- package/dist/worker-router.mjs +254 -0
- package/dist/ws-channel.d.mts +108 -0
- package/dist/ws-channel.mjs +520 -0
- package/dist/ws-router.d.mts +52 -0
- package/dist/ws-router.mjs +278 -0
- package/package.json +73 -0
- package/schema/error.bop +7 -0
- package/schema/index.bop +2 -0
- package/schema/message.bop +12 -0
- package/schema/pool.bop +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam McCord
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# @vampgg/utils
|
|
2
|
+
|
|
3
|
+
Low-level utilities for building bebop/tempo-based applications. Provides the shared wire schema, transport channel/router implementations, error types, loggers, and async iteration primitives used across the vamp.
|
|
4
|
+
|
|
5
|
+
- **bebop** — binary serialization library for fast, compact data interchange
|
|
6
|
+
- **tempo** — typed RPC layer built on top of bebop
|
|
7
|
+
|
|
8
|
+
## Development
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
vp install # install dependencies
|
|
12
|
+
vp test # run tests
|
|
13
|
+
vp run build # regenerate schema then build (runs: bebopc build && vp pack)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> `src/bebop.ts` is auto-generated by `bebopc` from the `.bop` schema files. Do not edit it by hand. Run `bebopc build` (or `vp run build`) to regenerate after changing `schema/*.bop`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Schema (`schema/`)
|
|
21
|
+
|
|
22
|
+
Two bebop types define the wire format for all RPC in this framework.
|
|
23
|
+
|
|
24
|
+
### `Error` (struct — `schema/error.bop`)
|
|
25
|
+
|
|
26
|
+
Structured error type carried inside tempo error responses.
|
|
27
|
+
|
|
28
|
+
| Field | Type | Description |
|
|
29
|
+
| --------- | --------------------- | -------------------------------------------- |
|
|
30
|
+
| `code` | `uint32` | `TempoStatusCode` value |
|
|
31
|
+
| `tag` | `string` | Machine-readable error tag (see `ErrorTags`) |
|
|
32
|
+
| `stack` | `string` | Stack trace |
|
|
33
|
+
| `msg` | `string` | Human-readable message |
|
|
34
|
+
| `details` | `map[string, string]` | Arbitrary key-value context |
|
|
35
|
+
|
|
36
|
+
### `Message` (message — `schema/message.bop`)
|
|
37
|
+
|
|
38
|
+
The universal envelope for all RPC calls. Fields are optional (bebop message type — missing fields are simply absent on the wire).
|
|
39
|
+
|
|
40
|
+
| Field | Type | Description |
|
|
41
|
+
| --------------- | -------- | --------------------------------------- |
|
|
42
|
+
| `methodId` | `uint32` | Numeric identifier for the RPC method |
|
|
43
|
+
| `messageId` | `guid` | UUID correlating request/response |
|
|
44
|
+
| `timestamp` | `date` | When the message was created |
|
|
45
|
+
| `data` | `byte[]` | Bebop-encoded request or response body |
|
|
46
|
+
| `deadline` | `date` | Absolute deadline for the call |
|
|
47
|
+
| `status` | `byte` | `TempoStatusCode` (OK, CANCELLED, etc.) |
|
|
48
|
+
| `msg` | `string` | Error message (on error responses) |
|
|
49
|
+
| `authorization` | `string` | Auth header value |
|
|
50
|
+
| `credential` | `string` | Serialized credential for storage |
|
|
51
|
+
| `headers` | `string` | HTTP-style metadata header string |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Subpath Exports
|
|
56
|
+
|
|
57
|
+
### `@vampgg/utils/bebop` — Generated Schema Bindings
|
|
58
|
+
|
|
59
|
+
Auto-generated. Exports `Error`, `Message`, and `BEBOP_SCHEMA`.
|
|
60
|
+
|
|
61
|
+
Each type exports as both an interface and a factory/static object (the bebop pattern):
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Message, Error } from "@vampgg/utils/bebop";
|
|
65
|
+
|
|
66
|
+
// Encode
|
|
67
|
+
const bytes = Message.encode({ methodId: 1, messageId: crypto.randomUUID(), data: payload });
|
|
68
|
+
|
|
69
|
+
// Decode
|
|
70
|
+
const msg = Message.decode(bytes);
|
|
71
|
+
|
|
72
|
+
// Factory (adds .encode() method to instance)
|
|
73
|
+
const record = Message({ methodId: 1, messageId: "..." });
|
|
74
|
+
record.encode(); // Uint8Array
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### `@vampgg/utils/error` — Typed Errors
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { SystemError, ErrorTags } from "@vampgg/utils/error";
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**`SystemError<Tag>`** extends `TempoError` from `@tempojs/common`. Wraps a bebop `Error` struct. Use this as the base for all thrown errors in the framework.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
throw new SystemError(ErrorTags.Validation, {
|
|
89
|
+
code: TempoStatusCode.INVALID_ARGUMENT,
|
|
90
|
+
msg: "field 'name' is required",
|
|
91
|
+
details: new Map([["field", "name"]]),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Convert an unknown caught error into a SystemError
|
|
95
|
+
const err = SystemError.generic(caughtError, ErrorTags.Unknown);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**`ErrorTags`** enum — the set of recognized error tags:
|
|
99
|
+
|
|
100
|
+
| Tag | Meaning |
|
|
101
|
+
| --------------------- | ------------------------------------ |
|
|
102
|
+
| `PgOutage` | PostgreSQL connectivity failure |
|
|
103
|
+
| `RedisOutage` | Redis connectivity failure |
|
|
104
|
+
| `Unknown` | Unclassified error (default) |
|
|
105
|
+
| `NeedPermissions` | Missing required permissions |
|
|
106
|
+
| `Validation` | Input validation failure |
|
|
107
|
+
| `Authorization` | Auth check failed |
|
|
108
|
+
| `GameStateGeneration` | Game state generation failure |
|
|
109
|
+
| `InvalidPrompt` | AI prompt rejected |
|
|
110
|
+
| `UnknownAIProvider` | Unrecognized AI provider |
|
|
111
|
+
| `FailedToStart` | Service failed to initialize |
|
|
112
|
+
| `Unimplemented` | Method not yet implemented |
|
|
113
|
+
| `IncompatibleVersion` | Client/server version mismatch |
|
|
114
|
+
| `PingUnacceptable` | Ping latency out of acceptable range |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### `@vampgg/utils/context-logger` — Base Logger
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { ContextLogger } from "@vampgg/utils/context-logger";
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Extends `ConsoleLogger` from `@tempojs/common`. Adds structured `bindings` (key-value context attached to every log line) and methods for deriving child loggers.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const logger = new ContextLogger("my-service", TempoLogLevel.Info, undefined, { env: "prod" });
|
|
128
|
+
|
|
129
|
+
// Derive a child logger with additional bindings
|
|
130
|
+
const child = logger.clone("request-handler", false, { requestId: "abc" });
|
|
131
|
+
|
|
132
|
+
// Derive a child with a correlation ID
|
|
133
|
+
const correlated = logger.correlate("db-query", correlationId, false, { table: "users" });
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**`ContextLogger`** is the base class — extend it (or use `DebugLogger` / `PinoLogger`) rather than instantiating it directly in most cases.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `@vampgg/utils/debug-logger` — Debug-Namespace Logger
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { DebugLogger } from "@vampgg/utils/debug-logger";
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Uses the `debug` package. Each log level gets its own namespace: `<sourceName>:trace`, `<sourceName>:info`, etc. Enable output with the `DEBUG` environment variable.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const logger = new DebugLogger("my-service");
|
|
150
|
+
// Enable with: DEBUG=my-service:* node ...
|
|
151
|
+
|
|
152
|
+
logger.info("server started", { port: 3000 });
|
|
153
|
+
logger.error("db failed", { query }, err);
|
|
154
|
+
|
|
155
|
+
const child = logger.clone("auth", false, { userId: "123" });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `@vampgg/utils/pino-logger` — Pino Structured Logger
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { PinoLogger } from "@vampgg/utils/pino-logger";
|
|
164
|
+
import pino from "pino";
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Wraps a `pino` instance. Pass an existing `pino.Logger` to integrate with your existing pino setup (transports, formatters, etc.).
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const root = pino({ level: "info" });
|
|
171
|
+
const logger = new PinoLogger("my-service", TempoLogLevel.Info, root, { env: "prod" });
|
|
172
|
+
|
|
173
|
+
logger.info("started");
|
|
174
|
+
logger.warn("slow query", { duration: 450 });
|
|
175
|
+
|
|
176
|
+
const child = logger.clone("auth");
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### `@vampgg/utils/async-queue` — Bounded Async Queue
|
|
182
|
+
|
|
183
|
+
A single-consumer async queue that backs both iterators below. A backing **ring
|
|
184
|
+
buffer** (no `Array.shift()`), a **high-water mark** with an explicit overflow
|
|
185
|
+
policy, and a real **error channel** — so a slow consumer can't grow memory
|
|
186
|
+
without bound and a transport error surfaces at the `for await` instead of looking
|
|
187
|
+
like a clean end-of-stream.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { AsyncQueue, AsyncQueueOverflowError } from "@vampgg/utils/async-queue";
|
|
191
|
+
|
|
192
|
+
const q = new AsyncQueue<number>(
|
|
193
|
+
{ highWaterMark: 1024, overflow: "error" }, // or "drop-latest"
|
|
194
|
+
() => cleanup(), // optional onDispose, called on close/return/throw
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
q.push(1); // returns false if dropped (overflow: "drop-latest")
|
|
198
|
+
q.close(); // graceful end → consumer sees { done: true }
|
|
199
|
+
q.fail(new Error("transport died")); // error channel → thrown at the consumer
|
|
200
|
+
|
|
201
|
+
for await (const n of q) {
|
|
202
|
+
/* … */
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`overflow: "error"` makes `push` past `highWaterMark` raise `AsyncQueueOverflowError`
|
|
207
|
+
at the consumer; `"drop-latest"` silently drops instead.
|
|
208
|
+
|
|
209
|
+
### `@vampgg/utils/create-event-iterator` — Push-Based Async Generator
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { createEventIterator } from "@vampgg/utils/create-event-iterator";
|
|
213
|
+
import type { Subscriber, Context, CleanupFn } from "@vampgg/utils/create-event-iterator";
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Converts any push-based event source (EventEmitter, WebSocket messages, browser events) into an `AsyncGenerator`. The subscriber receives `{ emit, cancel }` and returns an optional cleanup function.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const stream = createEventIterator<MyEvent>(({ emit, cancel }) => {
|
|
220
|
+
const handler = (ev: MyEvent) => emit(ev);
|
|
221
|
+
emitter.on("data", handler);
|
|
222
|
+
|
|
223
|
+
return () => {
|
|
224
|
+
emitter.off("data", handler);
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
for await (const event of stream) {
|
|
229
|
+
// process event
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Call `cancel()` from within the subscriber (e.g. on a "close" event) to end iteration. Remaining buffered events are yielded before the generator returns.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### `@vampgg/utils/create-duplex-iterator` — Bidirectional Async Stream
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { createDuplexIterator } from "@vampgg/utils/create-duplex-iterator";
|
|
241
|
+
import type { Writer } from "@vampgg/utils/create-duplex-iterator";
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Combines an outgoing `AsyncGenerator<BebopRecord>` with an incoming event source. For each outgoing record it calls `emitter`, then yields the next incoming event. Used internally by duplex-stream RPC methods.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const stream = createDuplexIterator<IncomingRecord>(
|
|
248
|
+
outgoingGenerator, // AsyncGenerator<BebopRecord> — client sends these
|
|
249
|
+
(record) => ws.send(record.encode()), // called for each outgoing record
|
|
250
|
+
({ emit, cancel }) => {
|
|
251
|
+
// subscriber for incoming records
|
|
252
|
+
ws.onmessage = (ev) => emit(decode(ev.data));
|
|
253
|
+
return () => {
|
|
254
|
+
ws.onmessage = null;
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
for await (const incoming of stream) {
|
|
260
|
+
// interleaved with outgoing sends
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### `@vampgg/utils/ws-channel` — WebSocket Tempo Client Channel
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { TempoWSChannel } from "@vampgg/utils/ws-channel";
|
|
270
|
+
import type { TempoWSChannelOptions } from "@vampgg/utils/ws-channel";
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A `BaseChannel` implementation that transports tempo RPC calls over a WebSocket connection (using the `websocket-ts` library with auto-reconnect support). The wire format is `Message` encoded as bebop binary.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// Connect to a new WebSocket
|
|
277
|
+
const channel = TempoWSChannel.forAddress("wss://api.example.com/rpc", {
|
|
278
|
+
maxRetries: 3,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Wrap an existing WebSocket
|
|
282
|
+
const channel = TempoWSChannel.forWebSocket(existingWs);
|
|
283
|
+
|
|
284
|
+
await channel.waitForOpen();
|
|
285
|
+
|
|
286
|
+
// Used by tempo-generated client stubs — not called directly
|
|
287
|
+
// channel.startUnary(request, context, method, options)
|
|
288
|
+
// channel.startServerStream(...)
|
|
289
|
+
// channel.startClientStream(...)
|
|
290
|
+
// channel.startDuplexStream(...)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
All four tempo method types (unary, server-stream, client-stream, duplex) are supported with deadline and retry policy propagation.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### `@vampgg/utils/ws-router` — WebSocket Tempo Server Router
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import { TempoWsRouter } from "@vampgg/utils/ws-router";
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
A `BaseRouter` that dispatches incoming `ArrayBuffer` WebSocket messages to registered tempo service methods. Extend or instantiate with a `ServiceRegistry` and optional `AuthInterceptor`.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const router = new TempoWsRouter<MyContext>(logger, registry, configuration, authInterceptor);
|
|
307
|
+
router.useHooks(hookRegistry);
|
|
308
|
+
|
|
309
|
+
// In your WebSocket message handler:
|
|
310
|
+
ws.onmessage = async (ev) => {
|
|
311
|
+
const response = Message({});
|
|
312
|
+
await router.process(ev.data as ArrayBuffer, response, [ws, myContext]);
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Server-stream and duplex-stream methods must be implemented as `async function*` generators in the service. Streams send successive `Message` frames until a `CANCELLED` status frame closes the stream.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### `@vampgg/utils/worker-channel` — Bun Worker Tempo Client Channel
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { TempoWorkerChannel } from "@vampgg/utils/worker-channel";
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
A `BaseChannel` that communicates with a Bun `Worker` using `postMessage` / `onmessage`. Messages are transferred as `Uint8Array` with zero-copy buffer transfer.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const channel = new TempoWorkerChannel("./my-worker.ts");
|
|
330
|
+
await channel.ready; // resolves when the worker sends its first message
|
|
331
|
+
|
|
332
|
+
// channel.startUnary(...) / startServerStream(...) etc.
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The `worker` property exposes the underlying `Bun.Worker` instance if you need direct access.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
### `@vampgg/utils/worker-router` — Bun Worker Tempo Server Router
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { TempoWorkerRouter } from "@vampgg/utils/worker-router";
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Runs inside a Bun Worker (`declare var self: Bun.Worker`). Dispatches incoming `Buffer` messages to registered tempo service methods, writing responses back via `postMessage`.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
// Inside worker.ts
|
|
349
|
+
const router = new TempoWorkerRouter<MyEnv>(logger, registry);
|
|
350
|
+
|
|
351
|
+
self.onmessage = async (ev: MessageEvent<Uint8Array>) => {
|
|
352
|
+
const response = Message({});
|
|
353
|
+
await router.process(ev.data, response, myEnv);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Signal ready to the channel
|
|
357
|
+
postMessage(new Uint8Array(0));
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Supports client-stream cancellation: a `CANCELLED` status message from the client triggers generator cleanup and stops the stream.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### `@vampgg/utils/extension-channel` — Browser Extension Channel (Client)
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
import { TempoExtensionChannel } from "@vampgg/utils/extension-channel";
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
A `BaseChannel` for browser extensions. Sends messages via `browser.runtime.sendMessage` and receives responses via `browser.runtime.onMessage`. Uses `webextension-polyfill` for cross-browser compatibility.
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
const channel = new TempoExtensionChannel({ logger });
|
|
374
|
+
// channel.startUnary(...) etc. — used by tempo-generated client stubs
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Messages are encoded as `Array<number>` (bebop bytes as a plain array) to satisfy browser extension message serialization constraints.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
### `@vampgg/utils/extension-router` — Browser Extension Router (Server)
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { TempoExtensionRouter, ExtensionBaseRouter } from "@vampgg/utils/extension-router";
|
|
385
|
+
import { TempoExtensionRouterConfiguration } from "@vampgg/utils/extension-router";
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Runs in the extension background script. Dispatches incoming `Message` objects (decoded from `browser.runtime.onMessage`) to registered tempo service methods and sends responses back via `tabs.sendMessage`.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const router = new TempoExtensionRouter<{ sender: Runtime.MessageSender }>(
|
|
392
|
+
logger,
|
|
393
|
+
registry,
|
|
394
|
+
new TempoRouterConfiguration(),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
browser.runtime.onMessage.addListener(async (raw, sender) => {
|
|
398
|
+
const msg = Message(Message.decode(new Uint8Array(raw as number[])));
|
|
399
|
+
const response = Message({});
|
|
400
|
+
await router.process(msg, response, { sender });
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### `@vampgg/utils/memory-storage` — In-Memory Credential Storage
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
import { MemoryStorageStrategy } from "@vampgg/utils/memory-storage";
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Implements `CredentialStorage` from `@tempojs/client` using a `Map`. Suitable for testing or short-lived processes where persistence is not required.
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
const storage = new MemoryStorageStrategy();
|
|
416
|
+
await storage.storeCredential("session", credential);
|
|
417
|
+
const cred = await storage.getCredential("session");
|
|
418
|
+
await storage.removeCredential("session");
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### `@vampgg/utils/sync-storage` — WebExtension Sync Storage
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { SyncStorageStrategy } from "@vampgg/utils/sync-storage";
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Implements `CredentialStorage` backed by `browser.storage.sync` (via `webextension-polyfill`). Use in browser extension contexts where credentials should persist across browser sessions and sync across devices.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
const storage = new SyncStorageStrategy();
|
|
433
|
+
await storage.storeCredential("auth", credential);
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Key Patterns
|
|
439
|
+
|
|
440
|
+
### Adding a New Transport
|
|
441
|
+
|
|
442
|
+
To add a new transport (e.g., SharedWorker, BroadcastChannel):
|
|
443
|
+
|
|
444
|
+
1. **Client side** — extend `BaseChannel` from `@tempojs/client`. Implement `startUnary`, `startClientStream`, `startServerStream`, `startDuplexStream`. Encode requests as `Message` binary (`Message.encode(msg)`), decode responses with `Message.decode(bytes)`. Use `createEventIterator` for server-stream and `createDuplexIterator` for duplex-stream.
|
|
445
|
+
|
|
446
|
+
2. **Server side** — extend `BaseRouter` from `@tempojs/server`. Implement `process(rawRequest, responseMessage, env)`. Decode the incoming buffer with `Message.decode(...)`, dispatch by `methodId` via `this.registry.getMethod(methodId)`, encode responses with `Message.encode(response)`.
|
|
447
|
+
|
|
448
|
+
### Schema Regeneration
|
|
449
|
+
|
|
450
|
+
After editing `schema/error.bop` or `schema/message.bop`, regenerate the TypeScript bindings:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
bebopc build
|
|
454
|
+
# or
|
|
455
|
+
vp run build # runs bebopc build then packs the library
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Never manually edit `src/bebop.ts`.
|
|
459
|
+
|
|
460
|
+
### Wire Protocol
|
|
461
|
+
|
|
462
|
+
All transports use the same protocol:
|
|
463
|
+
|
|
464
|
+
1. Client creates a `Message` with `methodId`, `messageId` (UUID), `data` (bebop-encoded request body), optional `deadline` and `authorization`.
|
|
465
|
+
2. Server decodes the `Message`, routes by `methodId`, executes the method, and replies with a `Message` containing the same `messageId`, `status: OK`, and `data` (bebop-encoded response body).
|
|
466
|
+
3. For streams, the server sends multiple frames with `status: OK` and terminates with `status: CANCELLED` and an empty `data`.
|
|
467
|
+
4. Errors are returned as a `Message` with a non-OK `status` and a human-readable `msg`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/async-queue.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Single-consumer, multi-producer bounded async queue.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the ad-hoc "array + single wakeup promise" queues that previously
|
|
6
|
+
* backed `createEventIterator` / `createDuplexIterator`. Properties:
|
|
7
|
+
* - Ring buffer: O(1) enqueue/dequeue, never `Array.shift()`.
|
|
8
|
+
* - Bounded: `highWaterMark` caps buffered values; `push` returns `false` so
|
|
9
|
+
* producers can apply backpressure, and `overflow: "error"` turns a runaway
|
|
10
|
+
* producer into a thrown error instead of silent unbounded growth (OOM).
|
|
11
|
+
* - Error channel: `fail(e)` makes the consumer's `next()` throw `e`.
|
|
12
|
+
* - Lifecycle: a single `onDispose` runs exactly once on the first terminal
|
|
13
|
+
* transition (close / error / return / throw).
|
|
14
|
+
*
|
|
15
|
+
* Single-consumer contract: at most one `next()` may be outstanding at a time.
|
|
16
|
+
* Every current use is a single `for await` / `yield*`, which satisfies this.
|
|
17
|
+
*/
|
|
18
|
+
interface AsyncQueueOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of buffered, not-yet-consumed values. When the buffer is
|
|
21
|
+
* full, `push` returns `false` (overflow). Defaults to 1024.
|
|
22
|
+
*/
|
|
23
|
+
highWaterMark?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Behavior when `push` is called on a full buffer:
|
|
26
|
+
* - "error" (default): the queue fails with an overflow error that is thrown
|
|
27
|
+
* into the consumer at the next `next()`.
|
|
28
|
+
* - "drop-latest": the pushed value is dropped, `push` returns false.
|
|
29
|
+
* Producers should honor the boolean return value to apply backpressure.
|
|
30
|
+
*/
|
|
31
|
+
overflow?: "error" | "drop-latest";
|
|
32
|
+
}
|
|
33
|
+
declare class AsyncQueueOverflowError extends Error {
|
|
34
|
+
constructor(highWaterMark: number);
|
|
35
|
+
}
|
|
36
|
+
declare class AsyncQueue<T> implements AsyncIterableIterator<T> {
|
|
37
|
+
private buffer;
|
|
38
|
+
private head;
|
|
39
|
+
private count;
|
|
40
|
+
private readonly highWaterMark;
|
|
41
|
+
private readonly overflow;
|
|
42
|
+
private pendingResolve;
|
|
43
|
+
private pendingReject;
|
|
44
|
+
private closed;
|
|
45
|
+
private error;
|
|
46
|
+
private hasError;
|
|
47
|
+
private onDispose?;
|
|
48
|
+
private disposed;
|
|
49
|
+
constructor(opts?: AsyncQueueOptions, onDispose?: () => void | Promise<void>);
|
|
50
|
+
/** Producer: enqueue a value. Returns false if the buffer is full (backpressure signal). */
|
|
51
|
+
push(value: T): boolean;
|
|
52
|
+
/** Producer: terminate the stream as a clean completion. Idempotent. */
|
|
53
|
+
close(): void;
|
|
54
|
+
/** Producer: terminate the stream with an error thrown into the consumer. Idempotent. */
|
|
55
|
+
fail(error: unknown): void;
|
|
56
|
+
next(): Promise<IteratorResult<T>>;
|
|
57
|
+
/** Consumer aborted early (break / return). Runs dispose and ends the stream. */
|
|
58
|
+
return(value?: unknown): Promise<IteratorResult<T>>;
|
|
59
|
+
/** Consumer threw. Propagate after dispose. */
|
|
60
|
+
throw(e?: unknown): Promise<IteratorResult<T>>;
|
|
61
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<T>;
|
|
62
|
+
private enqueue;
|
|
63
|
+
private dequeue;
|
|
64
|
+
private grow;
|
|
65
|
+
private dispose;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { AsyncQueue, AsyncQueueOptions, AsyncQueueOverflowError };
|