@ignex/nova 0.1.3 → 0.1.6

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.
Files changed (98) hide show
  1. package/README.md +4 -1
  2. package/docs/ai/TREE.md +69 -9
  3. package/docs/architecture.md +75 -27
  4. package/docs/events.md +83 -1
  5. package/docs/generic-bindings.md +10 -0
  6. package/docs/wire-format.md +65 -18
  7. package/package.json +2 -1
  8. package/prebuilds/linux-x64/libignex_ffi.so +0 -0
  9. package/public/generate.ts +97 -3
  10. package/public/server.ts +10 -0
  11. package/rust/src/generated/backend.rs +503 -0
  12. package/rust/src/transcode/generated.rs +376 -17
  13. package/src/bridge/nats/inbound.ts +46 -0
  14. package/src/bridge/nats/index.ts +131 -0
  15. package/src/bridge/nats/real-transport.ts +133 -0
  16. package/src/bridge/nats/types.ts +80 -0
  17. package/src/codegen/constants.ts +14 -4
  18. package/src/codegen/direct-gen.ts +20 -6
  19. package/src/codegen/registry-gen.ts +10 -6
  20. package/src/codegen/rust-glue-gen.ts +10 -3
  21. package/src/codegen/schema-model.ts +28 -3
  22. package/src/codegen/ts-ser-gen.ts +12 -3
  23. package/src/core/auth.ts +65 -4
  24. package/src/core/client-rpc.ts +75 -0
  25. package/src/core/client-state.ts +53 -0
  26. package/src/core/client-wire.ts +183 -8
  27. package/src/core/client.ts +84 -4
  28. package/src/core/groups.ts +5 -0
  29. package/src/core/metrics.ts +38 -21
  30. package/src/core/outbound.ts +50 -6
  31. package/src/core/rate-limit.ts +69 -0
  32. package/src/core/replay.ts +41 -1
  33. package/src/core/resume.ts +181 -0
  34. package/src/core/rooms.ts +10 -3
  35. package/src/core/routing.ts +128 -5
  36. package/src/core/server/client-info.ts +37 -0
  37. package/src/core/server/http-routes.ts +59 -0
  38. package/src/core/{server.ts → server/index.ts} +112 -120
  39. package/src/core/server/metrics-view.ts +53 -0
  40. package/src/core/server/socket-lifecycle.ts +57 -0
  41. package/src/core/state.ts +73 -1
  42. package/src/core/topic-log.ts +86 -0
  43. package/src/events/clients.ts +18 -0
  44. package/src/events/cluster/dedupe.ts +43 -0
  45. package/src/events/cluster/envelope.ts +149 -0
  46. package/src/events/cluster/index.ts +50 -0
  47. package/src/events/cluster/keys.ts +33 -0
  48. package/src/events/cluster/kinds.ts +32 -0
  49. package/src/events/cluster/presence-table.ts +99 -0
  50. package/src/events/cluster/presence.ts +53 -0
  51. package/src/events/cluster/redis-client.ts +50 -0
  52. package/src/events/cluster/store-memory.ts +67 -0
  53. package/src/events/cluster/store-redis.ts +44 -0
  54. package/src/events/cluster/subjects.ts +30 -0
  55. package/src/events/cluster/sync.ts +476 -0
  56. package/src/events/cluster/transport-nats.ts +24 -0
  57. package/src/events/cluster/transport-redis.ts +120 -0
  58. package/src/events/cluster-rpc.ts +196 -0
  59. package/src/events/delivery.ts +83 -0
  60. package/src/events/emit.ts +57 -11
  61. package/src/events/hub/context-factory.ts +79 -0
  62. package/src/events/hub/dispatch.ts +86 -0
  63. package/src/events/hub/index.ts +536 -0
  64. package/src/events/hub/internal.ts +31 -0
  65. package/src/events/hub/metrics-snapshot.ts +84 -0
  66. package/src/events/hub/resolve-cluster.ts +49 -0
  67. package/src/events/queue.ts +36 -9
  68. package/src/events/registry.ts +90 -54
  69. package/src/events/schedule.ts +73 -0
  70. package/src/events/trace.ts +283 -0
  71. package/src/events/types/client.ts +68 -0
  72. package/src/events/types/cluster.ts +40 -0
  73. package/src/events/types/context.ts +50 -0
  74. package/src/events/types/emit-target.ts +29 -0
  75. package/src/events/types/groups.ts +35 -0
  76. package/src/events/types/hub.ts +124 -0
  77. package/src/events/types/index.ts +30 -0
  78. package/src/events/types/metrics.ts +52 -0
  79. package/src/events/types/options.ts +62 -0
  80. package/src/generated/direct-ser.ts +146 -59
  81. package/src/generated/fbs/backend.fbs +23 -0
  82. package/src/generated/registry.ts +92 -33
  83. package/src/generated/rust/backend_generated.rs +503 -0
  84. package/src/generated/ts/backend.ts +4 -0
  85. package/src/generated/ts/resume.ts +74 -0
  86. package/src/generated/ts/resumed.ts +88 -0
  87. package/src/generated/ts/rpc-call.ts +112 -0
  88. package/src/generated/ts/rpc-result.ts +126 -0
  89. package/src/generated/ts/snapshot-request.ts +19 -5
  90. package/src/generated/ts-ser.ts +109 -16
  91. package/src/generated/wire-registry.json +7 -3
  92. package/src/schema/index.ts +45 -1
  93. package/src/transport/transport.ts +117 -77
  94. package/src/bridge/nats.ts +0 -309
  95. package/src/events/cluster.ts +0 -732
  96. package/src/events/hub.ts +0 -481
  97. package/src/events/types.ts +0 -378
  98. package/src/transport/stats.ts +0 -48
@@ -1,378 +0,0 @@
1
- /**
2
- * Shared types for the events layer — the typed, event-driven surface on top
3
- * of the FlatBuffer transport core.
4
- *
5
- * The events layer is the application-facing counterpart to `src/core/routing`:
6
- * the server transports frames; this layer receives them (via handlers, "like
7
- * routes are made") and emits them to websocket clients (the global `emit`),
8
- * with first-class client records ("who is connected, on whose behalf, and
9
- * what state does the app keep per connection"), named groups, and optional
10
- * cross-instance sync for horizontally scaled deployments.
11
- *
12
- * Everything here is type-only or interface-shaped; the runtime lives in the
13
- * sibling modules (`clients`, `registry`, `groups`, `cluster`, `emit`, `hub`).
14
- */
15
- import type { ServerWebSocket } from "bun";
16
- import type { Bindings, DefaultBindings, EventNameOf, EventsOf } from "../bindings/types";
17
- import type { IgnServer } from "../core/server";
18
- import type { WsData } from "../core/state";
19
-
20
- // ── client records ──────────────────────────────────────────────────────────
21
-
22
- /** Per-connection state store attached to an active client record. */
23
- export interface ClientData {
24
- /** Read a value previously `set` on this connection. */
25
- get(key: string): unknown;
26
- /** Store a value on this connection (arbitrary app state, per socket). */
27
- set(key: string, value: unknown): void;
28
- has(key: string): boolean;
29
- delete(key: string): boolean;
30
- clear(): void;
31
- keys(): string[];
32
- entries(): Array<[string, unknown]>;
33
- toJSON(): Record<string, unknown>;
34
- }
35
-
36
- /**
37
- * An active user connection — the server-side representation of "who is
38
- * connected, on whose behalf, and what to remember about them".
39
- *
40
- * - `id` is the connection id (the socket identity, unique per connection).
41
- * - `userId` is the identity this connection acts ON BEHALF OF (set via the
42
- * `authenticate` hook, `hub.setUserId`, or later); several connections may
43
- * share a `userId` (multi-tab / multi-device), and `hub.clientsByUser`
44
- * groups them.
45
- * - `data` is the per-connection app store, cleared automatically on close.
46
- * - `groups` / `topics` are shared with the transport (`ws.data`), so control
47
- * frames (`joinGroup` / `subscribe`) stay consistent with the events layer.
48
- */
49
- export interface EventClient {
50
- /** stable connection id (ws identity, unique per socket) */
51
- readonly id: string;
52
- /** identity this connection acts on behalf of (undefined = anonymous) */
53
- readonly userId: string | undefined;
54
- /** arbitrary app metadata from `authenticate` (undefined if none) */
55
- readonly meta: Record<string, unknown> | undefined;
56
- /** per-connection app state store (auto-cleared on disconnect) */
57
- readonly data: ClientData;
58
- /** server-side client groups this connection belongs to */
59
- readonly groups: ReadonlySet<string>;
60
- /** topics/rooms this connection has joined */
61
- readonly topics: ReadonlySet<string>;
62
- /** epoch ms the socket connected */
63
- readonly connectedAt: number;
64
- /** remote IP (from the socket) */
65
- readonly ip: string;
66
- /** true after the socket closed (record is then detached) */
67
- readonly closed: boolean;
68
- /** the underlying socket (advanced / low-level use) */
69
- readonly ws: ServerWebSocket<WsData>;
70
- }
71
-
72
- /** A connection known to exist on ANOTHER instance (via cluster presence). */
73
- export interface RemoteClient {
74
- clientId: string;
75
- /** the instance that reported this connection */
76
- instanceId: string;
77
- userId?: string;
78
- /** epoch ms the connection was (re)confirmed by its instance */
79
- lastSeen: number;
80
- }
81
-
82
- // ── emit targets ────────────────────────────────────────────────────────────
83
-
84
- /**
85
- * Where an emit goes. The discriminated union is how the API "easily
86
- * differentiates" between the addressing modes:
87
- *
88
- * - `{ type: "broadcast" }` — every connected client, on every instance.
89
- * - `{ type: "topic", topic }` — subscribers of a topic (rooms + replay).
90
- * - `{ type: "group", group }` — members of a server-side group.
91
- * - `{ type: "user", userId }` — every socket acting on behalf of `userId`.
92
- * - `{ type: "client", clientId }` — one specific connection.
93
- *
94
- * Local delivery is synchronous and allocation-free (the transport scratch +
95
- * `ws.send` copy); the cross-instance fan-out (when a cluster is configured)
96
- * is deferred to the offload queue so the emit call never blocks.
97
- */
98
- export type EmitTarget =
99
- | { type: "broadcast" }
100
- | { type: "topic"; topic: string }
101
- | { type: "group"; group: string }
102
- | { type: "user"; userId: string }
103
- | { type: "client"; clientId: string };
104
-
105
- export type EmitTargetKind = EmitTarget["type"];
106
-
107
- // ── event contexts ──────────────────────────────────────────────────────────
108
-
109
- /** Where an event reached the hub from. */
110
- export type EventSource = "client" | "remote" | "bridge";
111
-
112
- /**
113
- * The context every handler receives — the "who / where / how do I reply"
114
- * bundle. For client-sent events `client` is the sender's record; for
115
- * server-side events (`onServerEvent`) there is no sender client.
116
- */
117
- export interface EventContext<B extends Bindings = DefaultBindings> {
118
- /** where the event came from: a local client, another instance, or the bridge */
119
- readonly source: EventSource;
120
- /** the client that sent the event (undefined for remote/bridge events) */
121
- readonly client?: EventClient;
122
- /** the events hub (for `hub.emit`, groups, client data, …) */
123
- readonly hub: EventsHub<B>;
124
- /** the underlying server (raw `publish`/`publishToClient`/… escape hatch) */
125
- readonly server: IgnServer<B>;
126
- /** emit helpers bound to this hub (reply without importing the singleton) */
127
- emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K], target?: EmitTarget): void;
128
- emitToGroup<K extends EventNameOf<B>>(group: string, name: K, payload: EventsOf<B>[K]): void;
129
- emitToUser<K extends EventNameOf<B>>(userId: string, name: K, payload: EventsOf<B>[K]): void;
130
- emitToClient<K extends EventNameOf<B>>(clientId: string, name: K, payload: EventsOf<B>[K]): void;
131
- emitToTopic<K extends EventNameOf<B>>(topic: string, name: K, payload: EventsOf<B>[K]): void;
132
- }
133
-
134
- /** A handler registered on the hub (client-sent events). May be async. */
135
- export type EventHandler<B extends Bindings, K extends EventNameOf<B>> = (
136
- payload: EventsOf<B>[K],
137
- ctx: EventContext<B>,
138
- ) => void | Promise<void>;
139
-
140
- /** A handler for server-side events (remote instances / bridge inbound). */
141
- export type ServerEventHandler<B extends Bindings, K extends EventNameOf<B>> = (
142
- payload: EventsOf<B>[K],
143
- ctx: EventContext<B>,
144
- ) => void | Promise<void>;
145
-
146
- // ── groups ──────────────────────────────────────────────────────────────────
147
-
148
- /** A named client group: membership by connection id, fan-out via the hub. */
149
- export interface ClientGroup<B extends Bindings = DefaultBindings> {
150
- readonly name: string;
151
- /** add a connection (by id) to the group (idempotent) */
152
- add(clientId: string): void;
153
- remove(clientId: string): void;
154
- has(clientId: string): boolean;
155
- /** member connection ids */
156
- members(): string[];
157
- readonly size: number;
158
- /** emit an event to every member of this group (cluster-aware) */
159
- emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K]): void;
160
- }
161
-
162
- /** A named USER group: membership by `userId`, fan-out to every socket of each member user. */
163
- export interface UserGroup<B extends Bindings = DefaultBindings> {
164
- readonly name: string;
165
- add(userId: string): void;
166
- remove(userId: string): void;
167
- has(userId: string): boolean;
168
- /** member user ids */
169
- members(): string[];
170
- readonly size: number;
171
- /** emit an event to every socket acting on behalf of each member user */
172
- emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K]): void;
173
- }
174
-
175
- // ── cluster sync ────────────────────────────────────────────────────────────
176
-
177
- /**
178
- * Cross-instance messaging transport (server ⇄ server). NATS and Redis
179
- * adapters are provided; any broker that supports named channels + byte
180
- * payloads can be plugged in (tests use an in-memory bus). All calls are
181
- * fire-and-forget and are invoked from the offload queue, never from the WS
182
- * hot path.
183
- */
184
- export interface ClusterTransport {
185
- readonly connected: boolean;
186
- /** synchronously hand bytes to the broker (throws → caller counts an error) */
187
- publish(subject: string, data: Uint8Array): void;
188
- /** subscribe; `cb` receives raw message bytes; returns an unsubscribe fn */
189
- subscribe(subject: string, cb: (data: Uint8Array) => void): () => void;
190
- close(): Promise<void>;
191
- }
192
-
193
- /**
194
- * Optional shared-state store (presence / cluster group membership / client
195
- * data). A memory implementation is used by default (per-instance); Redis is
196
- * the production choice for horizontally scaled deployments (`createRedisStateStore`).
197
- */
198
- export interface ClusterStateStore {
199
- get(key: string): Promise<string | null>;
200
- set(key: string, value: string, ttlMs?: number): Promise<void>;
201
- del(key: string): Promise<void>;
202
- sadd(key: string, member: string): Promise<void>;
203
- srem(key: string, member: string): Promise<void>;
204
- smembers(key: string): Promise<string[]>;
205
- expire(key: string, ttlMs: number): Promise<void>;
206
- }
207
-
208
- /** Redis connection options — a URL string or an ioredis options object. */
209
- export type RedisConnectionOptions = string | Record<string, unknown>;
210
-
211
- // ── metrics ─────────────────────────────────────────────────────────────────
212
-
213
- /** Events-layer counters (folded into `server.getMetrics().events`). */
214
- export interface EventsMetricsSnapshot {
215
- /** emit calls (all targets) */
216
- emitted: number;
217
- emittedByTarget: Record<EmitTargetKind, number>;
218
- /** frames written to local sockets by the events layer */
219
- deliveredLocal: number;
220
- /** frames handed to the cluster transport */
221
- clusterPublished: number;
222
- /** frames received from other instances */
223
- clusterReceived: number;
224
- /** frames dropped because they originated on this instance */
225
- clusterDroppedSelf: number;
226
- /** cluster transport / decode failures */
227
- clusterErrors: number;
228
- /** offload-queue: tasks accepted */
229
- queueQueued: number;
230
- /** offload-queue: tasks dropped (overflow) */
231
- queueDropped: number;
232
- /** offload-queue: task failures */
233
- queueErrors: number;
234
- /** handler exceptions (caught + isolated) */
235
- handlerErrors: number;
236
- /** local connected clients */
237
- connectedClients: number;
238
- /** remote clients known via cluster presence */
239
- remoteClients: number;
240
- /** active client groups / user groups */
241
- clientGroups: number;
242
- userGroups: number;
243
- }
244
-
245
- // ── hub ─────────────────────────────────────────────────────────────────────
246
-
247
- /**
248
- * The events hub — the public API returned as `server.events` when
249
- * `createServer({ events: {...} })` is used, and the backing store for the
250
- * module-global `emit` / `on` singleton (`ignex-nova/events`).
251
- */
252
- export interface EventsHub<B extends Bindings = DefaultBindings> {
253
- readonly server: IgnServer<B>;
254
- /** stable id of THIS instance (self-delivery dedupe in a cluster) */
255
- readonly instanceId: string;
256
-
257
- // ── receiving events (the "events file": where events come in) ────────
258
- on<K extends EventNameOf<B>>(name: K, handler: EventHandler<B, K>): EventsHub<B>;
259
- off<K extends EventNameOf<B>>(name: K, handler?: EventHandler<B, K>): EventsHub<B>;
260
- once<K extends EventNameOf<B>>(name: K, handler: EventHandler<B, K>): EventsHub<B>;
261
- /** every client-sent inbound event (name + payload + ctx) */
262
- onAny(cb: (name: EventNameOf<B>, payload: unknown, ctx: EventContext<B>) => void): EventsHub<B>;
263
- offAny(cb: (name: EventNameOf<B>, payload: unknown, ctx: EventContext<B>) => void): EventsHub<B>;
264
- /** server-side handlers for events from OTHER instances / the bridge */
265
- onServerEvent<K extends EventNameOf<B>>(name: K, handler: ServerEventHandler<B, K>): EventsHub<B>;
266
- offServerEvent<K extends EventNameOf<B>>(
267
- name: K,
268
- handler?: ServerEventHandler<B, K>,
269
- ): EventsHub<B>;
270
- /** event names with at least one handler */
271
- events(): EventNameOf<B>[];
272
- listenerCount(name: EventNameOf<B>): number;
273
- removeAllListeners(name?: EventNameOf<B>): EventsHub<B>;
274
-
275
- // ── emitting events (through websockets, cluster-aware) ───────────────
276
- emit<K extends EventNameOf<B>>(name: K, payload: EventsOf<B>[K], target?: EmitTarget): void;
277
- emitToTopic<K extends EventNameOf<B>>(topic: string, name: K, payload: EventsOf<B>[K]): void;
278
- emitToGroup<K extends EventNameOf<B>>(group: string, name: K, payload: EventsOf<B>[K]): void;
279
- emitToUser<K extends EventNameOf<B>>(userId: string, name: K, payload: EventsOf<B>[K]): void;
280
- emitToClient<K extends EventNameOf<B>>(clientId: string, name: K, payload: EventsOf<B>[K]): void;
281
-
282
- // ── client records ("who is connected, on whose behalf") ──────────────
283
- client(id: string): EventClient | undefined;
284
- clients(): EventClient[];
285
- /** every connection acting on behalf of `userId` */
286
- clientsByUser(userId: string): EventClient[];
287
- readonly clientCount: number;
288
- /** bind a connection to an identity (on whose behalf it acts) */
289
- setUserId(clientId: string, userId: string): void;
290
- /** per-connection app state */
291
- setClientData(clientId: string, key: string, value: unknown): void;
292
- getClientData(clientId: string, key: string): unknown;
293
- clearClientData(clientId: string): void;
294
-
295
- // ── groups ────────────────────────────────────────────────────────────
296
- /** client group handle (membership by connection id) */
297
- group(name: string): ClientGroup<B>;
298
- /** live client-group names */
299
- groups(): string[];
300
- /** user group handle (membership by userId, fan-out to every socket) */
301
- userGroup(name: string): UserGroup<B>;
302
- /** live user-group names */
303
- userGroups(): string[];
304
-
305
- // ── horizontal scaling ────────────────────────────────────────────────
306
- /** connections known on other instances (presence; [] when unclustered) */
307
- clusterClients(): RemoteClient[];
308
- /** user→clients index from the shared state store, if configured */
309
- clusterUserClients(userId: string): Promise<Array<{ instanceId: string; clientId: string }>>;
310
- /** cluster-wide client-group members (shared state store), if configured */
311
- clusterGroupMembers(group: string): Promise<string[]>;
312
- /** cluster-wide user-group members (shared state store), if configured */
313
- clusterUserGroupMembers(group: string): Promise<string[]>;
314
- /** client data from the shared state store, if configured */
315
- remoteClientData(clientId: string): Promise<Record<string, unknown> | undefined>;
316
-
317
- // ── lifecycle / observability ─────────────────────────────────────────
318
- metrics(): EventsMetricsSnapshot;
319
- queueStats(): {
320
- pending: number;
321
- queued: number;
322
- processed: number;
323
- dropped: number;
324
- errors: number;
325
- };
326
- close(): Promise<void>;
327
- }
328
-
329
- // ── options ─────────────────────────────────────────────────────────────────
330
-
331
- /**
332
- * `createServer({ events: EventsOptions })` — enables the events layer and
333
- * exposes it as `server.events`. The module-global `emit` / `on` singleton is
334
- * bound by default (`global: false` to opt out).
335
- */
336
- export interface EventsOptions<B extends Bindings = DefaultBindings> {
337
- /**
338
- * App events clients are allowed to send to the hub. Default: every event
339
- * registered via `hub.on(...)` is auto-allowed on first use.
340
- */
341
- inbound?: EventNameOf<B>[];
342
- /** bind the module-global `emit`/`on` singleton (default true) */
343
- global?: boolean;
344
- /** called after a connection's client record is attached (seed `data`, …) */
345
- onConnect?: (client: EventClient) => void;
346
- /** called after a connection's client record is detached (cleanup) */
347
- onDisconnect?: (client: EventClient) => void;
348
- /** horizontal-scaling sync (server ⇄ server) */
349
- cluster?: EventsClusterOptions;
350
- /** offload-queue limits (the queue keeps cluster/state work off hot paths) */
351
- queue?: { workers?: number; maxPending?: number };
352
- }
353
-
354
- export interface EventsClusterOptions {
355
- /** stable id of this instance (default: random UUID) — self-dedupe */
356
- instanceId?: string;
357
- /** subject prefix (default: bindings subject prefix or "ignex") */
358
- prefix?: string;
359
- /**
360
- * NATS-based cluster messaging. `true` reuses the server's NATS bridge
361
- * connection (set `options.nats` too); pass `NatsBridgeOptions` to create a
362
- * dedicated bridge; pass a `NatsBridge` to reuse an existing one.
363
- */
364
- nats?: boolean | import("../bridge/nats").NatsBridgeOptions | import("../bridge/nats").NatsBridge;
365
- /** Redis pub/sub cluster messaging (lazy `ioredis`; subjects mirror NATS) */
366
- redis?: RedisConnectionOptions;
367
- /** pluggable messaging transport (tests / custom brokers) */
368
- transport?: ClusterTransport;
369
- /**
370
- * Shared-state store for presence / cluster group membership / client data.
371
- * Default: per-instance memory. Production: `createRedisStateStore(...)`.
372
- */
373
- state?: ClusterStateStore;
374
- /** presence re-announce + prune cadence (ms, default 15_000) */
375
- heartbeatMs?: number;
376
- /** remote presence TTL (ms, default 60_000) */
377
- presenceTtlMs?: number;
378
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Encode-path statistics (direct vs JSON) accumulated per event name across the
3
- * process. `createStats()` returns a small counter object; one instance lives
4
- * in `transport.ts` and is surfaced via `getEncodeStats()` / `server.metrics()`.
5
- *
6
- * `bump` is O(1) on the hot path: each (event, path) pair owns a tiny mutable
7
- * counter box that is allocated once (lazily), so a bump is a single number
8
- * increment — no Map.get + Map.set churn per encode.
9
- */
10
- export interface EncodeStats {
11
- bump(name: string, path: "direct" | "json" | "js"): void;
12
- get(): { direct: Record<string, number>; json: Record<string, number>; js: Record<string, number> };
13
- }
14
-
15
- /** Mutable counter box — allocated once per (event, path), incremented in place. */
16
- interface Counter {
17
- n: number;
18
- }
19
-
20
- function toObj(m: Map<string, Counter>): Record<string, number> {
21
- const out: Record<string, number> = {};
22
- for (const [name, c] of m) out[name] = c.n;
23
- return out;
24
- }
25
-
26
- export function createStats(): EncodeStats {
27
- const direct = new Map<string, Counter>();
28
- const json = new Map<string, Counter>();
29
- const js = new Map<string, Counter>();
30
-
31
- return {
32
- bump(name, path) {
33
- let m: Map<string, Counter>;
34
- if (path === "direct") m = direct;
35
- else if (path === "json") m = json;
36
- else m = js;
37
- let c = m.get(name);
38
- if (!c) {
39
- c = { n: 0 };
40
- m.set(name, c);
41
- }
42
- c.n++;
43
- },
44
- get() {
45
- return { direct: toObj(direct), json: toObj(json), js: toObj(js) };
46
- },
47
- };
48
- }