@aforoai/ws-metering 1.2.1 → 2.0.0
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/README.md +41 -21
- package/dist/index.d.ts +102 -53
- package/dist/index.js +249 -264
- package/dist/transport-core/core.d.ts +354 -0
- package/dist/transport-core/core.js +1077 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Meter WebSocket connections into Aforo — open, close, bytes, frame counts, and duration — by wrapping a `ws` server, or by tracking any connection that exposes the standard WebSocket event surface (Fastify-WebSocket, Socket.io, Deno, Bun).
|
|
4
4
|
|
|
5
|
-
**Version:**
|
|
5
|
+
**Version:** 2.0.0 · Apache-2.0 · [Changelog](CHANGELOG.md) · [User guide](USER_GUIDE.md)
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ Intended public install (once published):
|
|
|
12
12
|
npm i @aforoai/ws-metering ws
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
> **Install `
|
|
15
|
+
> **Install `2.0.0` or later. `1.0.0` on npm was built from an older copy of this code and lacks the fixes listed in the changelog.** If `2.0.0` is not on npm yet, install from source with the steps below. `ws` (`^8`) is an **optional** peer dependency — needed only if you use `wrapServer`. `trackConnection` works with any compatible socket.
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# from the SDKs repo root
|
|
@@ -40,10 +40,15 @@ const billing = new AforoWsBilling({
|
|
|
40
40
|
|
|
41
41
|
const wss = new WebSocketServer({ port: 8080 });
|
|
42
42
|
billing.wrapServer(wss, {
|
|
43
|
-
|
|
43
|
+
// Who is billed: from identity your app verified on the upgrade request.
|
|
44
|
+
customerResolver: (req) => sessions.customerIdFor(req),
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
// The SDK installs no signal handlers. Flush from your own:
|
|
48
|
+
process.once('SIGTERM', async () => {
|
|
49
|
+
await billing.shutdown();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
47
52
|
```
|
|
48
53
|
|
|
49
54
|
For frameworks that don't expose a `ws`-style server, track each socket directly:
|
|
@@ -52,7 +57,7 @@ For frameworks that don't expose a `ws`-style server, track each socket directly
|
|
|
52
57
|
billing.trackConnection(socket, { customerId: 'cust_001', metadata: { feed: 'market' } });
|
|
53
58
|
```
|
|
54
59
|
|
|
55
|
-
By default the SDK emits two events per connection — `CONNECTION_OPENED` on connect and `CONNECTION_CLOSED` on close (the billing anchor, carrying aggregated sent/recv counts + bytes + duration). The
|
|
60
|
+
By default the SDK emits two events per connection — `CONNECTION_OPENED` on connect and `CONNECTION_CLOSED` on close (the billing anchor, carrying aggregated sent/recv counts + bytes + duration). The open event uses `metricName: "websocket_api.connection_opened"` with `wsFrameType: "OPEN"`, the close event `websocket_api.connection_closed`, and per-frame events `websocket_api.message`. A connection still open at `shutdown()` is billed a `CONNECTION_CLOSED` (`GOING_AWAY`, `closedBy: "sdk_shutdown"`), and one with no frame for `connectionIdleTimeoutMs` (default 1 hour) is closed by a sweep (`ABNORMAL_CLOSURE`, `closedBy: "sdk_idle_timeout"`), so every billed open has a close. Events ship to `POST https://api.aforo.ai/v1/ingest/batch` with `X-API-Key: <api_key>` and `X-Tenant-Id: <tenant_id>`.
|
|
56
61
|
|
|
57
62
|
> **Per-frame metering is off by default.** Set `perFrameEvents: true` to emit one event per inbound and outbound frame — high volume, so size your batching accordingly. With it off, individual frames are still counted and rolled into the `CONNECTION_CLOSED` event.
|
|
58
63
|
|
|
@@ -64,28 +69,40 @@ By default the SDK emits two events per connection — `CONNECTION_OPENED` on co
|
|
|
64
69
|
|---|---|---|---|
|
|
65
70
|
| `tenantId` | `string` | — (required) | Aforo tenant. Sent as `X-Tenant-Id`. Never read from a client header. |
|
|
66
71
|
| `productId` | `string` | — (required) | Aforo product id; into each event's `metadata.productId`. |
|
|
67
|
-
| `apiKey` | `string` | — (required) | Sent as `X-API-Key: <apiKey
|
|
68
|
-
| `ingestorUrl` | `string` | — (required) | Ingestion base URL. SDK appends `/v1/ingest/batch`. Use `https://api.aforo.ai`. |
|
|
72
|
+
| `apiKey` | `string` | — (required) | Sent only as `X-API-Key: <apiKey>`; never logged or shown by `inspect()` / `JSON.stringify`. |
|
|
73
|
+
| `ingestorUrl` | `string` | — (required) | Ingestion base URL, `https://` (see `allowInsecureHttp`). SDK appends `/v1/ingest/batch`. Use `https://api.aforo.ai`. |
|
|
69
74
|
| `productType` | `string` | `'WEBSOCKET_API'` | Aforo product type sent as top-level `productType` on every event (trimmed + uppercased; unknown values pass through). Override via `wrapServer(wss, { productType })` or `trackConnection(ws, { customerId, productType })`. |
|
|
70
75
|
| `perFrameEvents` | `boolean` | `false` | Emit one event per frame (each direction). Off → frames are aggregated into the close event only. |
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
|
|
76
|
-
`
|
|
76
|
+
| `connectionIdleTimeoutMs` | `number` | `3600000` | Close (and bill `CONNECTION_CLOSED` for) a connection with no frame for this long, so a socket that died without a close event is not tracked forever. Must be positive. |
|
|
77
|
+
| `allowInsecureHttp` | `boolean` | `false` | Accept a plain `http://` `ingestorUrl` on a non-local host. `https://` is required otherwise (plain http is fine for `localhost`, `127.0.0.1`, `::1`). |
|
|
78
|
+
| `flushCount` | `number` | `500` | Events per request (max 1000). Reaching it wakes the background sender; recording an event never sends by itself. |
|
|
79
|
+
| `flushIntervalMs` | `number` | `5000` | The background sender also runs this often. |
|
|
80
|
+
| `maxQueueSize` | `number` | `10000` | Queue capacity. When it is full — usually during a long outage — the oldest event is dropped with reason `overflow`. |
|
|
81
|
+
| `maxRetries` | `number` | `3` | Retries per batch on network errors, timeouts, 408, 429 and 5xx, with full-jitter backoff (`Retry-After` in seconds or as a date is honoured, capped at 60 s). |
|
|
82
|
+
| `timeoutMs` | `number` | `10000` | Total deadline per request. |
|
|
83
|
+
| `shutdownTimeoutMs` | `number` | `5000` | How long `shutdown()` keeps sending. Events still queued then are dropped with reason `shutdown`. |
|
|
84
|
+
| `flushOnExit` | `boolean` | `true` | Send queued events on the process `beforeExit` event, within `shutdownTimeoutMs`. The SDK installs no signal handlers. |
|
|
85
|
+
| `onError` | `(error: Error) => void` | logs to `console.error` | Called for delivery problems: a batch the ingestor refused (the message includes its `errors[].message`), an ingestor outage (once per outage), events lost at the shutdown deadline, and an unusable `executionStatus`. Exceptions it throws are caught. Delivery reports are a `CoreTransportError`: branch on `err.kind` (`outage`, `auth`, `rejected`, `shutdown`, `closed`) and read `err.events`, not the message. |
|
|
86
|
+
| `onDrop` | `(events, reason) => void` | none | Receives events lost for good. See [Dropped events](#dropped-events). |
|
|
87
|
+
|
|
88
|
+
`wrapServer(wss, options)` / `trackConnection(ws, opts)` take the customer:
|
|
77
89
|
|
|
78
90
|
| Option | Where | Type | What it does |
|
|
79
91
|
|---|---|---|---|
|
|
80
|
-
| `
|
|
92
|
+
| `customerResolver` | `wrapServer` | `(req) => string \| undefined` | Required (or `trustCustomerHeader`). The Aforo customer id from identity your app verified on the upgrade request. Returning nothing: the connection is not metered and one `invalid` drop is counted; the connection itself is not affected. `extractCustomerId` is a deprecated alias. |
|
|
93
|
+
| `trustCustomerHeader` | `wrapServer` | `boolean` | Bill the `x-customer-id` header of the upgrade request. Any client can send that header, so only turn this on behind a proxy you control that sets it. Default `false`; `wrapServer` throws when neither this nor `customerResolver` is given. |
|
|
81
94
|
| `extractMetadata` | `wrapServer` | `(req) => Record<string, unknown> \| undefined` | Optional per-connection tags. |
|
|
82
95
|
| `customerId` | `trackConnection` | `string` | Customer to attribute all traffic on this socket to. |
|
|
83
96
|
| `metadata` | `trackConnection` | `Record<string, unknown>` | Optional per-connection tags. |
|
|
84
97
|
| `productType` | both | `string` | Product type for this server's / socket's events. Default: the client-level `productType`. |
|
|
85
98
|
|
|
86
|
-
Close codes map to labels via `WS_CLOSE_REASONS` — `1000 → NORMAL_CLOSURE`, `1006 → ABNORMAL_CLOSURE`, `1009 → MESSAGE_TOO_BIG`, `4000 → IDLE_TIMEOUT`, etc. A socket `error` emits a synthetic `CONNECTION_CLOSED` with `wsCloseReason: INTERNAL_ERROR` and `metadata.event: CONNECTION_ERROR
|
|
99
|
+
Close codes map to labels via `WS_CLOSE_REASONS` — `1000 → NORMAL_CLOSURE`, `1006 → ABNORMAL_CLOSURE`, `1009 → MESSAGE_TOO_BIG`, `4000 → IDLE_TIMEOUT`, etc. A socket `error` emits a synthetic `CONNECTION_CLOSED` with `wsCloseReason: INTERNAL_ERROR` and `metadata.event: CONNECTION_ERROR`; the `close` that follows it is not billed again (one closing event per connection).
|
|
100
|
+
|
|
101
|
+
Exported symbols: `AforoWsBilling` (with `wrapServer` / `trackConnection` / `flush` / `shutdown` / `droppedCount` / `queuedCount`), the `WS_CLOSE_REASONS` map, `DEFAULT_PRODUCT_TYPE`, and the `AforoWsConfig` / `WrapServerOptions` / `TrackConnectionOptions` / `WsUsageEvent` / `DropReason` types.
|
|
102
|
+
|
|
103
|
+
## Delivery
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
One background sender per client sends one request at a time; recording an event only queues it. If the ingestor is unreachable, keeps answering 408/429/5xx after the retries, or answers 401/403 (a wrong or revoked key — logged as an error), the batch goes back to the front of the queue with its idempotency keys and sending pauses (5 s, doubling to 5 min, randomized). New events keep queueing up to `maxQueueSize`; the first successful request resumes sending, and the ingestor deduplicates the re-sent keys. A 413 splits the batch. Request bodies are plain JSON (the ingestor does not decode gzip). `flush()` sends what is queued now; `shutdown()` sends for at most `shutdownTimeoutMs` — call it from your own SIGTERM handler.
|
|
89
106
|
|
|
90
107
|
## Execution status
|
|
91
108
|
|
|
@@ -97,7 +114,7 @@ WebSocket frames carry no success or failure signal, so the SDK never sets `exec
|
|
|
97
114
|
|
|
98
115
|
```typescript
|
|
99
116
|
billing.wrapServer(wss, {
|
|
100
|
-
|
|
117
|
+
customerResolver: (req) => sessions.customerIdFor(req),
|
|
101
118
|
// (event, req) => status | undefined
|
|
102
119
|
executionStatus: (event, req) =>
|
|
103
120
|
event.wsFrameType === 'CLOSE' && event.wsCloseReason !== 'NORMAL_CLOSURE' ? 'ERROR' : undefined,
|
|
@@ -112,11 +129,14 @@ The SDK does not throw into your socket handlers for event content. An event tha
|
|
|
112
129
|
|
|
113
130
|
| Reason | When |
|
|
114
131
|
|---|---|
|
|
115
|
-
| `invalid` | The event failed a client-side check and was never
|
|
132
|
+
| `invalid` | The customer resolver returned nothing (one event per connection), or the event failed a client-side check and was never queued or sent: `customerId` longer than 64 characters or `productType` longer than 20 (every event of that connection is counted). Values are never truncated. The warning names the field, the limit and the value (first 80 characters); it is logged for the first invalid event and for every 1000th after that. |
|
|
116
133
|
| `rejected` | The ingestor refused the batch with a 4xx other than 408 / 429 (not retried), or named individual events in the `errors[]` of a 2xx response — then only those events are dropped. When a 2xx reports `failed > 0` without a usable `index`, the count is added to `droppedCount` and `onDrop` is not called. |
|
|
117
|
-
| `
|
|
134
|
+
| `overflow` | The queue (`maxQueueSize`) was full and the oldest event was evicted. |
|
|
135
|
+
| `shutdown` | Still queued when `shutdownTimeoutMs` ran out (also reported through `onError`). |
|
|
136
|
+
| `closed` | Recorded after `shutdown()`. |
|
|
137
|
+
| `retry_exhausted` | Not produced by 2.x (kept in the type): an outage keeps events queued instead of dropping them. |
|
|
118
138
|
|
|
119
|
-
A
|
|
139
|
+
A connection with no resolvable customer is not metered and is counted as one `invalid` drop.
|
|
120
140
|
|
|
121
141
|
```ts
|
|
122
142
|
const billing = new AforoWsBilling({
|
|
@@ -132,6 +152,6 @@ Step-by-step from install to a verified event in Aforo: [USER_GUIDE.md](USER_GUI
|
|
|
132
152
|
## What this doesn't cover
|
|
133
153
|
|
|
134
154
|
- **`send()` is wrapped by reference.** `trackConnection` reassigns `ws.send` to count outbound frames. If another layer wraps `send` afterward, ordering matters.
|
|
135
|
-
- **No persistent buffer.** Events are in memory until
|
|
155
|
+
- **No persistent buffer.** Events are in memory until sent; a hard crash loses what is queued. `shutdown()` covers graceful exit only.
|
|
136
156
|
- **Connection identity is per-process.** `wsConnectionId` is a fresh UUID per `trackConnection` call; it does not survive reconnects or correlate across processes.
|
|
137
157
|
- **The SDK does not enforce or read pricing.** It emits usage; rating/billing happens in Aforo.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* @aforoai/ws-metering — Aforo WebSocket Metering SDK
|
|
3
3
|
*
|
|
4
4
|
* Wraps WebSocket server connections to emit three classes of billing events:
|
|
5
|
-
* - CONNECTION_OPENED — once on upgrade completion
|
|
5
|
+
* - CONNECTION_OPENED — once on upgrade completion (wsFrameType OPEN,
|
|
6
|
+
* metric websocket_api.connection_opened)
|
|
6
7
|
* - MESSAGE — one event per frame (direction + bytes + type)
|
|
7
|
-
* - CONNECTION_CLOSED — once
|
|
8
|
+
* - CONNECTION_CLOSED — once per connection, carrying aggregated counters +
|
|
9
|
+
* duration: on close, on a socket error, when an idle
|
|
10
|
+
* connection is swept, or at shutdown
|
|
8
11
|
*
|
|
9
12
|
* Works with the `ws` library out of the box (wrap WebSocketServer), or use
|
|
10
13
|
* trackConnection() directly for Fastify-WebSocket, Socket.io, Deno, Bun, or
|
|
@@ -24,9 +27,15 @@
|
|
|
24
27
|
*
|
|
25
28
|
* const wss = new WebSocketServer({ port: 8080 });
|
|
26
29
|
* billing.wrapServer(wss, {
|
|
27
|
-
*
|
|
30
|
+
* // Who is billed, from identity your app verified at upgrade time.
|
|
31
|
+
* customerResolver: (req) => sessionStore.customerFor(req),
|
|
28
32
|
* });
|
|
33
|
+
*
|
|
34
|
+
* // The SDK installs no signal handlers. Flush from your own:
|
|
35
|
+
* process.once('SIGTERM', async () => { await billing.shutdown(); process.exit(0); });
|
|
29
36
|
*/
|
|
37
|
+
/** What onError receives for delivery problems: branch on `kind`, read `events`. */
|
|
38
|
+
export { CoreTransportError, type CoreErrorKind } from './transport-core/core';
|
|
30
39
|
export interface AforoWsConfig {
|
|
31
40
|
tenantId: string;
|
|
32
41
|
productId: string;
|
|
@@ -37,18 +46,37 @@ export interface AforoWsConfig {
|
|
|
37
46
|
* Default: `WEBSOCKET_API`. Override per integration via `wrapServer(wss, { productType })` / `trackConnection(ws, { productType })`.
|
|
38
47
|
*/
|
|
39
48
|
productType?: string;
|
|
40
|
-
/**
|
|
49
|
+
/** Allow a plain http:// ingestorUrl on a non-local host. Default false (https required). */
|
|
50
|
+
allowInsecureHttp?: boolean;
|
|
51
|
+
/** Events per request (default 500, max 1000). Reaching it wakes the background sender. */
|
|
41
52
|
flushCount?: number;
|
|
42
|
-
/**
|
|
53
|
+
/** The background sender also runs this often, in ms (default 5000). */
|
|
43
54
|
flushIntervalMs?: number;
|
|
55
|
+
/** Queue capacity (default 10 000). The oldest event is dropped on overflow. */
|
|
56
|
+
maxQueueSize?: number;
|
|
57
|
+
/** Retries per batch before the breaker opens (default 3). */
|
|
58
|
+
maxRetries?: number;
|
|
59
|
+
/** Total deadline per request, in ms (default 10 000). */
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** How long shutdown() keeps sending, in ms (default 5000). */
|
|
62
|
+
shutdownTimeoutMs?: number;
|
|
63
|
+
/** Send queued events on the process 'beforeExit' event (default true). */
|
|
64
|
+
flushOnExit?: boolean;
|
|
44
65
|
/** If true, emit one MESSAGE event per frame. If false (default) only aggregate on close. */
|
|
45
66
|
perFrameEvents?: boolean;
|
|
46
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Close a tracked connection that saw no frame for this long, in ms
|
|
69
|
+
* (default 3 600 000 = 1 hour): it gets a CONNECTION_CLOSED with close
|
|
70
|
+
* reason ABNORMAL_CLOSURE and metadata `closedBy: "sdk_idle_timeout"`. This
|
|
71
|
+
* keeps the open-connection map bounded when a socket dies without a close
|
|
72
|
+
* event. Must be a positive number; 0 means the default.
|
|
73
|
+
*/
|
|
74
|
+
connectionIdleTimeoutMs?: number;
|
|
75
|
+
/** Callback for delivery problems (rejected batches, ingestor outages) and bad executionStatus values. */
|
|
47
76
|
onError?: (error: Error) => void;
|
|
48
77
|
/**
|
|
49
|
-
* Opt-in hook receiving events that were permanently dropped (
|
|
50
|
-
*
|
|
51
|
-
* see DropReason). Events keep their idempotency keys,
|
|
78
|
+
* Opt-in hook receiving events that were permanently dropped (see
|
|
79
|
+
* DropReason). Events keep their idempotency keys,
|
|
52
80
|
* so persisting and re-submitting them after recovery is dedup-safe.
|
|
53
81
|
* Exceptions thrown by the hook are swallowed. Default: none (drops are
|
|
54
82
|
* still counted in droppedCount and WARN-logged).
|
|
@@ -56,17 +84,31 @@ export interface AforoWsConfig {
|
|
|
56
84
|
onDrop?: (events: WsUsageEvent[], reason: DropReason) => void;
|
|
57
85
|
}
|
|
58
86
|
/**
|
|
59
|
-
* Why events were permanently dropped. `
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
87
|
+
* Why events were permanently dropped. `overflow`: the queue was full and the
|
|
88
|
+
* oldest event was evicted. `rejected`: the ingestor refused the batch
|
|
89
|
+
* (non-retryable 4xx) or individual events of an accepted batch. `invalid`:
|
|
90
|
+
* the event failed a client-side check, or the customer resolver returned no
|
|
91
|
+
* customer. `shutdown`: still queued when the shutdown deadline passed.
|
|
92
|
+
* `closed`: recorded after shutdown(). `retry_exhausted`: kept for
|
|
93
|
+
* compatibility, not produced by 2.x — an outage keeps events queued.
|
|
65
94
|
*/
|
|
66
|
-
export type DropReason = 'retry_exhausted' | 'rejected' | 'invalid';
|
|
95
|
+
export type DropReason = 'overflow' | 'retry_exhausted' | 'rejected' | 'invalid' | 'shutdown' | 'closed';
|
|
67
96
|
export interface WrapServerOptions {
|
|
68
|
-
/**
|
|
69
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Who is billed for a connection, from identity your app verified on the
|
|
99
|
+
* upgrade request (session cookie, verified token). Required unless
|
|
100
|
+
* `trustCustomerHeader` is true. Returning nothing drops the connection's
|
|
101
|
+
* opening event as 'invalid' (counted); the connection is not affected.
|
|
102
|
+
*/
|
|
103
|
+
customerResolver?: (req: any) => string | undefined | null;
|
|
104
|
+
/** @deprecated Use `customerResolver` (same signature). */
|
|
105
|
+
extractCustomerId?: (req: any) => string | undefined | null;
|
|
106
|
+
/**
|
|
107
|
+
* Bill the `x-customer-id` header of the upgrade request. Any client can
|
|
108
|
+
* set that header, so only turn this on behind a proxy you control that
|
|
109
|
+
* sets it. Default false.
|
|
110
|
+
*/
|
|
111
|
+
trustCustomerHeader?: boolean;
|
|
70
112
|
/** Optional per-connection metadata (product-defined tags). */
|
|
71
113
|
extractMetadata?: (req: any) => Record<string, unknown> | undefined;
|
|
72
114
|
/**
|
|
@@ -121,13 +163,16 @@ interface MinimalWs {
|
|
|
121
163
|
on(event: 'message', fn: (data: any, isBinary?: boolean) => void): void;
|
|
122
164
|
on(event: 'close', fn: (code: number, reason: Buffer | string) => void): void;
|
|
123
165
|
on(event: 'error', fn: (err: Error) => void): void;
|
|
124
|
-
|
|
166
|
+
/** `ws` takes (data, cb) or (data, options, cb); every argument is passed through. */
|
|
167
|
+
send(data: any, ...rest: any[]): void;
|
|
125
168
|
readyState?: number;
|
|
126
169
|
}
|
|
127
170
|
/** Minimal WebSocketServer surface — matches `ws` WebSocketServer. */
|
|
128
171
|
interface MinimalWss {
|
|
129
172
|
on(event: 'connection', fn: (ws: MinimalWs, req: any) => void): void;
|
|
130
173
|
}
|
|
174
|
+
/** Default for AforoWsConfig.connectionIdleTimeoutMs: one hour. */
|
|
175
|
+
export declare const DEFAULT_CONNECTION_IDLE_TIMEOUT_MS: number;
|
|
131
176
|
/** Default top-level `productType` for this SDK. */
|
|
132
177
|
export declare const DEFAULT_PRODUCT_TYPE = "WEBSOCKET_API";
|
|
133
178
|
export interface WsUsageEvent {
|
|
@@ -139,7 +184,7 @@ export interface WsUsageEvent {
|
|
|
139
184
|
productType: string;
|
|
140
185
|
wsConnectionId: string;
|
|
141
186
|
wsDirection: 'CLIENT_TO_SERVER' | 'SERVER_TO_CLIENT';
|
|
142
|
-
wsFrameType: 'TEXT' | 'BINARY' | 'PING' | 'PONG' | 'CLOSE';
|
|
187
|
+
wsFrameType: 'OPEN' | 'TEXT' | 'BINARY' | 'PING' | 'PONG' | 'CLOSE';
|
|
143
188
|
wsCloseReason?: string;
|
|
144
189
|
messageCount: number;
|
|
145
190
|
dataBytes: number;
|
|
@@ -151,51 +196,55 @@ export interface WsUsageEvent {
|
|
|
151
196
|
export declare class AforoWsBilling {
|
|
152
197
|
private readonly config;
|
|
153
198
|
private readonly productType;
|
|
154
|
-
private readonly flushCount;
|
|
155
|
-
private readonly flushIntervalMs;
|
|
156
199
|
private readonly perFrameEvents;
|
|
157
200
|
private readonly onError;
|
|
158
|
-
private readonly
|
|
159
|
-
private
|
|
160
|
-
private
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
private readonly invalidSeen;
|
|
201
|
+
private readonly core;
|
|
202
|
+
private readonly idleTimeoutMs;
|
|
203
|
+
private readonly connections;
|
|
204
|
+
private readonly sweepTimer;
|
|
205
|
+
private shutdownPromise;
|
|
164
206
|
constructor(config: AforoWsConfig);
|
|
165
|
-
/**
|
|
207
|
+
/** Connections being tracked: opened and not yet closed by the socket, the idle sweep or shutdown(). */
|
|
208
|
+
get openConnections(): number;
|
|
209
|
+
/**
|
|
210
|
+
* Close every tracked connection idle longer than `connectionIdleTimeoutMs`
|
|
211
|
+
* at `now` (CONNECTION_CLOSED, ABNORMAL_CLOSURE, `closedBy: "sdk_idle_timeout"`).
|
|
212
|
+
* Runs on a timer; callable directly. Returns how many it closed.
|
|
213
|
+
*/
|
|
214
|
+
sweepIdleConnections(now?: number): number;
|
|
215
|
+
/**
|
|
216
|
+
* Wrap a `ws` WebSocketServer (or any object emitting 'connection' events).
|
|
217
|
+
* Throws when neither `customerResolver` nor `trustCustomerHeader: true` is given.
|
|
218
|
+
*/
|
|
166
219
|
wrapServer(wss: MinimalWss, options: WrapServerOptions): void;
|
|
167
220
|
/** Track a single WebSocket connection. Returns an unsubscribe function. */
|
|
168
221
|
trackConnection(ws: MinimalWs, opts: TrackConnectionOptions): () => void;
|
|
222
|
+
/** Fill in the fields every event carries. */
|
|
223
|
+
private buildEvent;
|
|
169
224
|
private push;
|
|
170
|
-
private flush;
|
|
171
|
-
private send;
|
|
172
|
-
/**
|
|
173
|
-
* A 2xx can still carry per-event rejections:
|
|
174
|
-
* `{accepted, duplicates, failed, errors:[{index, message}]}`. Events the
|
|
175
|
-
* response names by index are dropped with reason 'rejected'; a `failed`
|
|
176
|
-
* count the response does not attribute to an index is counted only.
|
|
177
|
-
*/
|
|
178
|
-
private handlePartialFailures;
|
|
179
225
|
/** Invoke onError; a hook bug must never break metering or flushing. */
|
|
180
226
|
private reportError;
|
|
181
|
-
/** Number of events permanently dropped since this instance was created. */
|
|
227
|
+
/** Number of events permanently dropped since this instance was created (every DropReason). */
|
|
182
228
|
get droppedCount(): number;
|
|
229
|
+
/** Number of events waiting to be sent. */
|
|
230
|
+
get queuedCount(): number;
|
|
231
|
+
/** Send what is queued now and wait for it (one request at a time; never concurrent). */
|
|
232
|
+
flush(): Promise<{
|
|
233
|
+
sent: number;
|
|
234
|
+
failed: number;
|
|
235
|
+
requeued: number;
|
|
236
|
+
queued: number;
|
|
237
|
+
}>;
|
|
183
238
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
*
|
|
191
|
-
* never sent. Runs on the hot path, so the WARN is throttled per field
|
|
192
|
-
* (first occurrence, then every 1000th).
|
|
239
|
+
* Bill every connection still open with a CONNECTION_CLOSED (close code
|
|
240
|
+
* 1001, GOING_AWAY, metadata `closedBy: "sdk_shutdown"`), stop accepting
|
|
241
|
+
* events and send what is queued, for at most `shutdownTimeoutMs`. Events
|
|
242
|
+
* still queued then are dropped with reason 'shutdown' (counted, passed to
|
|
243
|
+
* onDrop). A connection opened from now on is reported as 'closed' and not
|
|
244
|
+
* tracked. Calling it again returns the first call's promise. Call it from
|
|
245
|
+
* your SIGTERM handler.
|
|
193
246
|
*/
|
|
194
|
-
private recordInvalid;
|
|
195
|
-
private notifyDrop;
|
|
196
|
-
private startTimer;
|
|
197
|
-
/** Flush any buffered events and stop the background timer. Call before process exit. */
|
|
198
247
|
shutdown(): Promise<void>;
|
|
248
|
+
toJSON(): Record<string, unknown>;
|
|
199
249
|
}
|
|
200
250
|
export declare const WS_CLOSE_REASONS: Record<number, string>;
|
|
201
|
-
export {};
|