@aforoai/mqtt-metering 1.2.2 → 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 -19
- package/dist/index.d.ts +111 -51
- package/dist/index.js +324 -299
- 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 MQTT traffic into Aforo two ways: hook an Aedes broker you operate to meter every PUBLISH/SUBSCRIBE/CONNECT/DISCONNECT, or wrap an `mqtt.js` client to meter what it publishes and receives against a third-party broker (AWS IoT, HiveMQ Cloud, EMQ X Cloud).
|
|
4
4
|
|
|
5
|
-
**Version:**
|
|
5
|
+
**Version:** 2.0.0 · Apache-2.0 · [Changelog](CHANGELOG.md) · [User guide](USER_GUIDE.md)
|
|
6
6
|
|
|
7
7
|
| Mode | When to use | Entry point |
|
|
8
8
|
|---|---|---|
|
|
@@ -19,7 +19,7 @@ npm i @aforoai/mqtt-metering aedes # broker mode
|
|
|
19
19
|
npm i @aforoai/mqtt-metering mqtt # client mode
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
> **Install `
|
|
22
|
+
> **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. `aedes` and `mqtt` are **optional** peer dependencies — install only the one your mode needs.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# from the SDKs repo root
|
|
@@ -51,12 +51,18 @@ const billing = new AforoMqttBilling({
|
|
|
51
51
|
|
|
52
52
|
const broker = aedes();
|
|
53
53
|
billing.wrapAedesBroker(broker, {
|
|
54
|
-
|
|
54
|
+
// Who is billed: the customer for the client your broker authenticated.
|
|
55
|
+
customerResolver: async (clientId, username) => customerStore.byClientId(clientId),
|
|
55
56
|
resolveMetadata: (clientId) => ({ deviceClass: deviceRegistry.classOf(clientId) }),
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
createServer(broker.handle).listen(1883);
|
|
59
|
-
|
|
60
|
+
|
|
61
|
+
// The SDK installs no signal handlers. Flush from your own:
|
|
62
|
+
process.once('SIGTERM', async () => {
|
|
63
|
+
await billing.shutdown();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
});
|
|
60
66
|
```
|
|
61
67
|
|
|
62
68
|
**Client mode (`mqtt.js`):**
|
|
@@ -83,20 +89,28 @@ Each event uses `metricName: "mqtt_broker.<event>"` (`mqtt_broker.publish`, `mqt
|
|
|
83
89
|
|---|---|---|---|
|
|
84
90
|
| `tenantId` | `string` | — (required) | Aforo tenant. Sent as `X-Tenant-Id`. Never read from a client header. |
|
|
85
91
|
| `productId` | `string` | — (required) | Aforo product id; into each event's `metadata.productId`. |
|
|
86
|
-
| `apiKey` | `string` | — (required) | Sent as `X-API-Key: <apiKey
|
|
87
|
-
| `ingestorUrl` | `string` | — (required) | Ingestion base URL. SDK appends `/v1/ingest/batch`. Use `https://api.aforo.ai`. |
|
|
92
|
+
| `apiKey` | `string` | — (required) | Sent only as `X-API-Key: <apiKey>`; never logged or shown by `inspect()` / `JSON.stringify`. |
|
|
93
|
+
| `ingestorUrl` | `string` | — (required) | Ingestion base URL, `https://` (see `allowInsecureHttp`). SDK appends `/v1/ingest/batch`. Use `https://api.aforo.ai`. |
|
|
88
94
|
| `productType` | `string` | `'MQTT_BROKER'` | Aforo product type sent as top-level `productType` on every event (trimmed + uppercased; unknown values pass through). Override via `wrapAedesBroker(broker, { productType })` or `wrapMqttClient(client, { customerId, productType })`. |
|
|
89
95
|
| `emitDeliverEvents` | `boolean` | `false` | Emit a `DELIVER` event per fan-out delivery. Off → `DELIVER` events are dropped (both modes). |
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
96
|
+
| `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`). |
|
|
97
|
+
| `flushCount` | `number` | `500` | Events per request (max 1000). Reaching it wakes the background sender; recording an event never sends by itself. |
|
|
98
|
+
| `flushIntervalMs` | `number` | `5000` | The background sender also runs this often. |
|
|
99
|
+
| `maxQueueSize` | `number` | `10000` | Queue capacity. When it is full — usually during a long outage — the oldest event is dropped with reason `overflow`. |
|
|
100
|
+
| `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). |
|
|
101
|
+
| `timeoutMs` | `number` | `10000` | Total deadline per request. |
|
|
102
|
+
| `shutdownTimeoutMs` | `number` | `5000` | How long `shutdown()` keeps sending. Events still queued then are dropped with reason `shutdown`. |
|
|
103
|
+
| `flushOnExit` | `boolean` | `true` | Send queued events on the process `beforeExit` event, within `shutdownTimeoutMs`. The SDK installs no signal handlers. |
|
|
104
|
+
| `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), a wrong or revoked API key, events lost at the shutdown deadline, and an unusable `executionStatus`. Delivery reports are a `CoreTransportError`: branch on `err.kind` (`outage`, `auth`, `rejected`, `shutdown`, `closed`) and read `err.events`, not the message. Exceptions it throws are caught. |
|
|
105
|
+
| `reconnectDebounceMs` | `number` | `30000` | A connection that drops and comes back within this window is one session: no new CONNECT/DISCONNECT pair is billed for the flap. A drop that does not come back is billed as DISCONNECT, stamped with the time it dropped, once the window passes; a clean disconnect is billed at once. `0` turns the window off. Overridable per `wrapAedesBroker` / `wrapMqttClient` call. |
|
|
106
|
+
| `onDrop` | `(events, reason) => void` | none | Receives events lost for good. See [Dropped events](#dropped-events). |
|
|
94
107
|
|
|
95
108
|
Mode-specific options:
|
|
96
109
|
|
|
97
110
|
| Option | Mode | Type | What it does |
|
|
98
111
|
|---|---|---|---|
|
|
99
|
-
| `
|
|
112
|
+
| `customerResolver` | broker | `(clientId, username?) => string \| undefined \| Promise<…>` | Required (or `trustCustomerHeader`). Map the client your broker authenticated to an Aforo customer. Returning nothing: that event is not metered and is counted as an `invalid` drop. A throw or rejection is reported through `onError` and treated the same way — never an unhandled rejection. `resolveCustomerId` is a deprecated alias. |
|
|
113
|
+
| `trustCustomerHeader` | broker | `boolean` | Bill the MQTT username sent at CONNECT. A client chooses its own username, so only turn this on when your broker authenticates it. Default `false`; `wrapAedesBroker` throws when neither this nor `customerResolver` is given. |
|
|
100
114
|
| `resolveMetadata` | broker | `(clientId) => Record<string, unknown> \| undefined` | Optional per-client tags. |
|
|
101
115
|
| `customerId` | client | `string` | Customer to attribute all traffic on this client to. |
|
|
102
116
|
| `clientId` | client | `string` | Fixed client id (defaults to `client.options.clientId`, then `'mqtt-client'`). |
|
|
@@ -104,17 +118,21 @@ Mode-specific options:
|
|
|
104
118
|
|
|
105
119
|
Every event carries `mqttQos` (0/1/2) and `mqttRetained` — use them in Aforo rate-plan filter conditions to price QoS ≥ 1 or retained messages separately.
|
|
106
120
|
|
|
107
|
-
Exported symbols: `AforoMqttBilling` (with `wrapAedesBroker` / `wrapMqttClient` / `shutdown` / `droppedCount`), `mqttConnectTopic` / `mqttDisconnectTopic`, `DEFAULT_PRODUCT_TYPE`, and the `AforoMqttConfig` / `AedesBrokerOptions` / `MqttClientOptions` / `MqttUsageEvent` / `DropReason` types.
|
|
121
|
+
Exported symbols: `AforoMqttBilling` (with `wrapAedesBroker` / `wrapMqttClient` / `shutdown` / `droppedCount`), `mqttConnectTopic` / `mqttDisconnectTopic`, `DEFAULT_PRODUCT_TYPE`, `DEFAULT_RECONNECT_DEBOUNCE_MS`, `CoreTransportError` (with the `CoreErrorKind` type), and the `AforoMqttConfig` / `AedesBrokerOptions` / `MqttClientOptions` / `MqttUsageEvent` / `DropReason` types.
|
|
122
|
+
|
|
123
|
+
## Delivery
|
|
124
|
+
|
|
125
|
+
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.
|
|
108
126
|
|
|
109
127
|
## Execution status
|
|
110
128
|
|
|
111
129
|
Outcome-based pricing bills each event at the weight set for its `executionStatus`; events without one bill at full price. The SDK trims and upper-cases the value and leaves the key out of the event when it is unset or blank. Accepted values: `SUCCESS`, `PARTIAL`, `TIMEOUT`, `ERROR`, `VALIDATION_FAILED`, `FAILED`, `FAILURE`, `CANCELLED`, `PENDING`, `BLOCKED`, `HITL_REQUIRED`. Any other value (or a Promise returned by an async resolver) is reported through `onError` and left off the event — the event itself is still sent — because the server would reject the event. Resolvers must be synchronous.
|
|
112
130
|
|
|
113
|
-
The SDK never sets `executionStatus` on its own: the broker hook sees no success or failure signal, and client
|
|
131
|
+
The SDK never sets `executionStatus` on its own: the broker hook sees no success or failure signal, and a client publish is metered only after it succeeded, so there is no failure for the SDK to read. Pass it yourself, as a string or as a function called for each event:
|
|
114
132
|
|
|
115
133
|
```typescript
|
|
116
134
|
billing.wrapAedesBroker(broker, {
|
|
117
|
-
|
|
135
|
+
customerResolver: (clientId) => customerIdLookup(clientId),
|
|
118
136
|
// (event, clientId) => status | undefined
|
|
119
137
|
executionStatus: (event) => (event.mqttTopic.startsWith('alerts/') ? 'SUCCESS' : undefined),
|
|
120
138
|
});
|
|
@@ -128,11 +146,14 @@ The SDK does not throw into your broker or client handlers for event content. An
|
|
|
128
146
|
|
|
129
147
|
| Reason | When |
|
|
130
148
|
|---|---|
|
|
131
|
-
| `invalid` | The event failed a client-side check and was never
|
|
149
|
+
| `invalid` | The customer resolver returned nothing or failed, or the event failed a client-side check and was never queued or sent: `customerId` longer than 64 characters, a missing or whitespace-only `mqttTopic`, or `productType` longer than 20. These are never truncated. A topic longer than 500 characters, or a client id longer than 128, is not a drop: it is cut to the limit and the event is sent (one warning per label). 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. |
|
|
132
150
|
| `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. |
|
|
133
|
-
| `
|
|
151
|
+
| `overflow` | The queue (`maxQueueSize`) was full and the oldest event was evicted. |
|
|
152
|
+
| `shutdown` | Still queued when `shutdownTimeoutMs` ran out (also reported through `onError`). |
|
|
153
|
+
| `closed` | Recorded after `shutdown()`. |
|
|
154
|
+
| `retry_exhausted` | Not produced by 2.x (kept in the type): an outage keeps events queued instead of dropping them. |
|
|
134
155
|
|
|
135
|
-
|
|
156
|
+
An event with no resolvable customer is not metered and is counted as an `invalid` drop.
|
|
136
157
|
|
|
137
158
|
```ts
|
|
138
159
|
const billing = new AforoMqttBilling({
|
|
@@ -149,6 +170,7 @@ Step-by-step from install to a verified event in Aforo: [USER_GUIDE.md](USER_GUI
|
|
|
149
170
|
|
|
150
171
|
- **Fan-out billing is opt-in.** `DELIVER` events are dropped unless `emitDeliverEvents: true`. Default metering counts the PUBLISH, not the per-subscriber fan-out.
|
|
151
172
|
- **CONNECT/DISCONNECT have no MQTT topic.** Because the ingestor requires `mqttTopic`, they are sent with `$SYS/clients/<clientId>/connected` and `$SYS/clients/<clientId>/disconnected`. `$SYS/` is reserved for brokers, so these topics can't collide with a real one.
|
|
152
|
-
- **Client-mode
|
|
153
|
-
- **
|
|
173
|
+
- **Client-mode PUBLISH is billed after success.** `wrapMqttClient` meters a publish when mqtt.js reports it succeeded: on the broker's PUBACK/PUBCOMP for QoS 1/2, after the socket write for QoS 0. A publish that throws or whose callback gets an error is not billed. `publishAsync()` is covered too.
|
|
174
|
+
- **Reconnects are one session.** mqtt.js reconnects on its own; a connection that comes back within `reconnectDebounceMs` (default 30 s) bills no new CONNECT and no DISCONNECT, and failed reconnect attempts bill nothing. A clean `client.end()` bills DISCONNECT at once. On the broker, the same rule applies per client id: a client that drops without a DISCONNECT packet and reconnects (or takes over its own session) inside the window is one session. `shutdown()` bills any DISCONNECT still waiting.
|
|
175
|
+
- **No persistent buffer.** Events are in memory until sent; a hard crash loses what is queued. `shutdown()` covers graceful exit only.
|
|
154
176
|
- **The SDK does not enforce or read pricing.** It emits usage; rating/billing happens in Aforo.
|
package/dist/index.d.ts
CHANGED
|
@@ -26,9 +26,15 @@
|
|
|
26
26
|
*
|
|
27
27
|
* const broker = aedes();
|
|
28
28
|
* billing.wrapAedesBroker(broker, {
|
|
29
|
-
*
|
|
29
|
+
* // Who is billed: map the client your broker authenticated to an Aforo customer.
|
|
30
|
+
* customerResolver: (clientId, username) => deviceRegistry.customerFor(clientId),
|
|
30
31
|
* });
|
|
32
|
+
*
|
|
33
|
+
* // The SDK installs no signal handlers. Flush from your own:
|
|
34
|
+
* process.once('SIGTERM', async () => { await billing.shutdown(); process.exit(0); });
|
|
31
35
|
*/
|
|
36
|
+
/** What onError receives for delivery problems: branch on `kind`, read `events`. */
|
|
37
|
+
export { CoreTransportError, type CoreErrorKind } from './transport-core/core';
|
|
32
38
|
export interface AforoMqttConfig {
|
|
33
39
|
tenantId: string;
|
|
34
40
|
productId: string;
|
|
@@ -39,18 +45,41 @@ export interface AforoMqttConfig {
|
|
|
39
45
|
* Default: `MQTT_BROKER`. Override per integration via `wrapAedesBroker(broker, { productType })` / `wrapMqttClient(client, { productType })`.
|
|
40
46
|
*/
|
|
41
47
|
productType?: string;
|
|
42
|
-
/**
|
|
48
|
+
/** Allow a plain http:// ingestorUrl on a non-local host. Default false (https required). */
|
|
49
|
+
allowInsecureHttp?: boolean;
|
|
50
|
+
/** Events per request (default 500, max 1000). Reaching it wakes the background sender. */
|
|
43
51
|
flushCount?: number;
|
|
44
|
-
/**
|
|
52
|
+
/** The background sender also runs this often, in ms (default 5000). */
|
|
45
53
|
flushIntervalMs?: number;
|
|
54
|
+
/** Queue capacity (default 10 000). The oldest event is dropped on overflow. */
|
|
55
|
+
maxQueueSize?: number;
|
|
56
|
+
/** Retries per batch before the breaker opens (default 3). */
|
|
57
|
+
maxRetries?: number;
|
|
58
|
+
/** Total deadline per request, in ms (default 10 000). */
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
/** How long shutdown() keeps sending, in ms (default 5000). */
|
|
61
|
+
shutdownTimeoutMs?: number;
|
|
62
|
+
/** Send queued events on the process 'beforeExit' event (default true). */
|
|
63
|
+
flushOnExit?: boolean;
|
|
46
64
|
/** Emit one event per fanout delivery in addition to the publish (default false — costly). */
|
|
47
65
|
emitDeliverEvents?: boolean;
|
|
48
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* A connection that drops and comes back within this many ms is one
|
|
68
|
+
* session: no new CONNECT/DISCONNECT pair is billed for the flap. A
|
|
69
|
+
* connection that does not come back is billed as DISCONNECT (at the time
|
|
70
|
+
* it dropped) once the window passes; a clean disconnect is billed at once.
|
|
71
|
+
* Default 30 000. 0 turns the window off. Overridable per wrap call.
|
|
72
|
+
*/
|
|
73
|
+
reconnectDebounceMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Callback for delivery problems (rejected batches, outages, a wrong API
|
|
76
|
+
* key, shutdown leftovers), resolver errors and bad executionStatus values.
|
|
77
|
+
* Delivery reports are a `CoreTransportError`: branch on `err.kind`.
|
|
78
|
+
*/
|
|
49
79
|
onError?: (error: Error) => void;
|
|
50
80
|
/**
|
|
51
|
-
* Opt-in hook receiving events that were permanently dropped (
|
|
52
|
-
*
|
|
53
|
-
* see DropReason). Events keep their idempotency keys,
|
|
81
|
+
* Opt-in hook receiving events that were permanently dropped (see
|
|
82
|
+
* DropReason). Events keep their idempotency keys,
|
|
54
83
|
* so persisting and re-submitting them after recovery is dedup-safe.
|
|
55
84
|
* Exceptions thrown by the hook are swallowed. Default: none (drops are
|
|
56
85
|
* still counted in droppedCount and WARN-logged).
|
|
@@ -58,17 +87,35 @@ export interface AforoMqttConfig {
|
|
|
58
87
|
onDrop?: (events: MqttUsageEvent[], reason: DropReason) => void;
|
|
59
88
|
}
|
|
60
89
|
/**
|
|
61
|
-
* Why events were permanently dropped. `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
90
|
+
* Why events were permanently dropped. `overflow`: the queue was full and the
|
|
91
|
+
* oldest event was evicted. `rejected`: the ingestor refused the batch
|
|
92
|
+
* (non-retryable 4xx) or individual events of an accepted batch. `invalid`:
|
|
93
|
+
* the event failed a client-side check, or the customer resolver returned no
|
|
94
|
+
* customer. `shutdown`: still queued when the shutdown deadline passed.
|
|
95
|
+
* `closed`: recorded after shutdown(). `retry_exhausted`: kept for
|
|
96
|
+
* compatibility, not produced by 2.x — an outage keeps events queued.
|
|
67
97
|
*/
|
|
68
|
-
export type DropReason = 'retry_exhausted' | 'rejected' | 'invalid';
|
|
98
|
+
export type DropReason = 'overflow' | 'retry_exhausted' | 'rejected' | 'invalid' | 'shutdown' | 'closed';
|
|
99
|
+
/** Maps a broker client to the Aforo customer billed for it. */
|
|
100
|
+
export type MqttCustomerResolver = (clientId: string, username?: string) => string | undefined | null | Promise<string | undefined | null>;
|
|
69
101
|
export interface AedesBrokerOptions {
|
|
70
|
-
/**
|
|
71
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Who is billed for a client's traffic: map the client your broker
|
|
104
|
+
* authenticated (its client id / username after your `authenticate` hook)
|
|
105
|
+
* to an Aforo customer. Required unless `trustCustomerHeader` is true. May
|
|
106
|
+
* be async. Returning nothing drops that event as 'invalid' (counted); a
|
|
107
|
+
* throw or rejection is reported through onError and the event is dropped
|
|
108
|
+
* as 'invalid'. MQTT traffic is never blocked.
|
|
109
|
+
*/
|
|
110
|
+
customerResolver?: MqttCustomerResolver;
|
|
111
|
+
/** @deprecated Use `customerResolver` (same signature). */
|
|
112
|
+
resolveCustomerId?: MqttCustomerResolver;
|
|
113
|
+
/**
|
|
114
|
+
* Bill the MQTT username the client sent at CONNECT. A client picks its own
|
|
115
|
+
* username, so only turn this on when your broker authenticates the
|
|
116
|
+
* username (password / certificate check in `authenticate`). Default false.
|
|
117
|
+
*/
|
|
118
|
+
trustCustomerHeader?: boolean;
|
|
72
119
|
/** Optional per-client metadata (tags, device type). */
|
|
73
120
|
resolveMetadata?: (clientId: string) => Record<string, unknown> | undefined;
|
|
74
121
|
/**
|
|
@@ -88,6 +135,13 @@ export interface AedesBrokerOptions {
|
|
|
88
135
|
executionStatus?: string | ((event: MqttUsageEvent, clientId: string) => string | undefined);
|
|
89
136
|
/** Product type for events from this broker. Default: the client-level `productType`. */
|
|
90
137
|
productType?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Reconnect window per client id, in ms (default: the client-level
|
|
140
|
+
* `reconnectDebounceMs`, 30 000). A client that disconnects without a
|
|
141
|
+
* DISCONNECT packet and reconnects with the same client id inside the
|
|
142
|
+
* window — or takes over its own session — is one session. 0 = off.
|
|
143
|
+
*/
|
|
144
|
+
reconnectDebounceMs?: number;
|
|
91
145
|
}
|
|
92
146
|
export interface MqttClientOptions {
|
|
93
147
|
/** Customer ID to attribute all traffic on this client to. */
|
|
@@ -104,14 +158,23 @@ export interface MqttClientOptions {
|
|
|
104
158
|
* and left off the event (the event is still sent), because the server would
|
|
105
159
|
* reject the event. Resolvers must be synchronous.
|
|
106
160
|
*
|
|
107
|
-
* Never derived by the SDK
|
|
108
|
-
*
|
|
109
|
-
*
|
|
161
|
+
* Never derived by the SDK: a publish is metered only once mqtt.js
|
|
162
|
+
* reports it succeeded, so there is no failure to derive a status from.
|
|
163
|
+
* Pass a fixed string, or a function called for each event that returns
|
|
164
|
+
* the status or undefined.
|
|
110
165
|
*/
|
|
111
166
|
executionStatus?: string | ((event: MqttUsageEvent) => string | undefined);
|
|
112
167
|
/** Product type for events from this client. Default: the client-level `productType`. */
|
|
113
168
|
productType?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Reconnect window in ms (default: the client-level `reconnectDebounceMs`,
|
|
171
|
+
* 30 000). mqtt.js reconnects on its own after a drop; a reconnect inside
|
|
172
|
+
* the window bills no new CONNECT and no DISCONNECT. 0 = off.
|
|
173
|
+
*/
|
|
174
|
+
reconnectDebounceMs?: number;
|
|
114
175
|
}
|
|
176
|
+
/** Default reconnect window, the same as python-mqtt's `reconnect_debounce_sec`. */
|
|
177
|
+
export declare const DEFAULT_RECONNECT_DEBOUNCE_MS = 30000;
|
|
115
178
|
/** Default top-level `productType` for this SDK. */
|
|
116
179
|
export declare const DEFAULT_PRODUCT_TYPE = "MQTT_BROKER";
|
|
117
180
|
/**
|
|
@@ -154,27 +217,32 @@ interface MinimalMqttClient {
|
|
|
154
217
|
on(event: 'message', fn: (topic: string, payload: Buffer, packet: any) => void): void;
|
|
155
218
|
on(event: 'connect', fn: () => void): void;
|
|
156
219
|
on(event: 'close', fn: () => void): void;
|
|
220
|
+
on(event: 'end', fn: () => void): void;
|
|
157
221
|
publish: (topic: string, message: any, opts?: any, cb?: any) => any;
|
|
158
222
|
options?: {
|
|
159
223
|
clientId?: string;
|
|
160
224
|
};
|
|
225
|
+
/** mqtt.js sets this while end() runs, so a 'close' then is a clean disconnect. */
|
|
226
|
+
disconnecting?: boolean;
|
|
161
227
|
}
|
|
162
228
|
export declare class AforoMqttBilling {
|
|
163
229
|
private readonly config;
|
|
164
230
|
private readonly productType;
|
|
165
|
-
private readonly flushCount;
|
|
166
|
-
private readonly flushIntervalMs;
|
|
167
231
|
private readonly emitDeliverEvents;
|
|
168
232
|
private readonly onError;
|
|
169
|
-
private readonly
|
|
170
|
-
private buffer;
|
|
171
|
-
private flushTimer;
|
|
172
|
-
private dropped;
|
|
173
|
-
/** Invalid-event count per field, for WARN throttling. */
|
|
174
|
-
private readonly invalidSeen;
|
|
233
|
+
private readonly core;
|
|
175
234
|
/** Request-derived labels already reported as truncated (one WARN per label). */
|
|
176
235
|
private readonly truncationWarned;
|
|
236
|
+
private readonly reconnectDebounceMs;
|
|
237
|
+
/** DISCONNECTs inside their reconnect window; billed at shutdown at the latest. */
|
|
238
|
+
private readonly pendingDisconnects;
|
|
177
239
|
constructor(config: AforoMqttConfig);
|
|
240
|
+
/**
|
|
241
|
+
* Attach to an Aedes broker. Throws when neither `customerResolver` nor
|
|
242
|
+
* `trustCustomerHeader: true` is given. Every listener catches its own
|
|
243
|
+
* errors (a resolver rejection is reported through onError), so metering can
|
|
244
|
+
* never produce an unhandled rejection in the broker process.
|
|
245
|
+
*/
|
|
178
246
|
wrapAedesBroker(broker: MinimalAedes, options: AedesBrokerOptions): void;
|
|
179
247
|
wrapMqttClient(client: MinimalMqttClient, options: MqttClientOptions): void;
|
|
180
248
|
/**
|
|
@@ -189,39 +257,31 @@ export declare class AforoMqttBilling {
|
|
|
189
257
|
/** Client id a connecting client chose, bound to the ingestor's mqttClientId limit. */
|
|
190
258
|
private clientIdLabel;
|
|
191
259
|
private push;
|
|
192
|
-
|
|
193
|
-
private
|
|
194
|
-
/**
|
|
195
|
-
* A 2xx can still carry per-event rejections:
|
|
196
|
-
* `{accepted, duplicates, failed, errors:[{index, message}]}`. Events the
|
|
197
|
-
* response names by index are dropped with reason 'rejected'; a `failed`
|
|
198
|
-
* count the response does not attribute to an index is counted only.
|
|
199
|
-
*/
|
|
200
|
-
private handlePartialFailures;
|
|
260
|
+
/** Hold a DISCONNECT for `ms`; it is billed when the window passes unless cancelled. */
|
|
261
|
+
private scheduleDisconnect;
|
|
201
262
|
/** Invoke onError; a hook bug must never break metering or flushing. */
|
|
202
263
|
private reportError;
|
|
203
|
-
/** Number of events permanently dropped since this instance was created. */
|
|
264
|
+
/** Number of events permanently dropped since this instance was created (every DropReason). */
|
|
204
265
|
get droppedCount(): number;
|
|
266
|
+
/** Number of events waiting to be sent. */
|
|
267
|
+
get queuedCount(): number;
|
|
268
|
+
/** Send what is queued now and wait for it (one request at a time; never concurrent). */
|
|
269
|
+
flush(): Promise<{
|
|
270
|
+
sent: number;
|
|
271
|
+
failed: number;
|
|
272
|
+
requeued: number;
|
|
273
|
+
queued: number;
|
|
274
|
+
}>;
|
|
205
275
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*/
|
|
210
|
-
private recordDrop;
|
|
211
|
-
/**
|
|
212
|
-
* Account for an event that failed a client-side check: never buffered,
|
|
213
|
-
* never sent. Runs on the hot path, so the WARN is throttled per field
|
|
214
|
-
* (first occurrence, then every 1000th).
|
|
276
|
+
* Stop accepting events and send what is queued, for at most
|
|
277
|
+
* `shutdownTimeoutMs`. Events still queued then are dropped with reason
|
|
278
|
+
* 'shutdown' (counted, passed to onDrop). Call it from your SIGTERM handler.
|
|
215
279
|
*/
|
|
216
|
-
private recordInvalid;
|
|
217
|
-
private notifyDrop;
|
|
218
|
-
private startTimer;
|
|
219
|
-
/** Flush any buffered events and stop the background timer. Call before process exit. */
|
|
220
280
|
shutdown(): Promise<void>;
|
|
281
|
+
toJSON(): Record<string, unknown>;
|
|
221
282
|
}
|
|
222
283
|
/**
|
|
223
284
|
* Cut `value` to at most `max` UTF-16 code units — how the ingestor counts
|
|
224
285
|
* (`String.length()` in Java) — without leaving half a surrogate pair.
|
|
225
286
|
*/
|
|
226
287
|
export declare function truncateToLimit(value: string, max: number): string;
|
|
227
|
-
export {};
|