@aforoai/metering 1.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 +88 -0
- package/dist/cjs/buffer.d.ts +27 -0
- package/dist/cjs/buffer.d.ts.map +1 -0
- package/dist/cjs/buffer.js +75 -0
- package/dist/cjs/buffer.js.map +1 -0
- package/dist/cjs/client.d.ts +86 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +301 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/idempotency.d.ts +21 -0
- package/dist/cjs/idempotency.d.ts.map +1 -0
- package/dist/cjs/idempotency.js +31 -0
- package/dist/cjs/idempotency.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/limits.d.ts +80 -0
- package/dist/cjs/limits.d.ts.map +1 -0
- package/dist/cjs/limits.js +143 -0
- package/dist/cjs/limits.js.map +1 -0
- package/dist/cjs/middleware/common.d.ts +22 -0
- package/dist/cjs/middleware/common.d.ts.map +1 -0
- package/dist/cjs/middleware/common.js +48 -0
- package/dist/cjs/middleware/common.js.map +1 -0
- package/dist/cjs/middleware/express.d.ts +15 -0
- package/dist/cjs/middleware/express.d.ts.map +1 -0
- package/dist/cjs/middleware/express.js +104 -0
- package/dist/cjs/middleware/express.js.map +1 -0
- package/dist/cjs/middleware/fastify.d.ts +13 -0
- package/dist/cjs/middleware/fastify.d.ts.map +1 -0
- package/dist/cjs/middleware/fastify.js +90 -0
- package/dist/cjs/middleware/fastify.js.map +1 -0
- package/dist/cjs/middleware/koa.d.ts +13 -0
- package/dist/cjs/middleware/koa.d.ts.map +1 -0
- package/dist/cjs/middleware/koa.js +82 -0
- package/dist/cjs/middleware/koa.js.map +1 -0
- package/dist/cjs/path-normalizer.d.ts +15 -0
- package/dist/cjs/path-normalizer.d.ts.map +1 -0
- package/dist/cjs/path-normalizer.js +49 -0
- package/dist/cjs/path-normalizer.js.map +1 -0
- package/dist/cjs/transport.d.ts +39 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +116 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/types.d.ts +128 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/buffer.js +71 -0
- package/dist/esm/buffer.js.map +1 -0
- package/dist/esm/client.js +297 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/idempotency.js +27 -0
- package/dist/esm/idempotency.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/limits.js +138 -0
- package/dist/esm/limits.js.map +1 -0
- package/dist/esm/middleware/common.js +41 -0
- package/dist/esm/middleware/common.js.map +1 -0
- package/dist/esm/middleware/express.js +100 -0
- package/dist/esm/middleware/express.js.map +1 -0
- package/dist/esm/middleware/fastify.js +87 -0
- package/dist/esm/middleware/fastify.js.map +1 -0
- package/dist/esm/middleware/koa.js +79 -0
- package/dist/esm/middleware/koa.js.map +1 -0
- package/dist/esm/path-normalizer.js +46 -0
- package/dist/esm/path-normalizer.js.map +1 -0
- package/dist/esm/transport.js +112 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/buffer.d.ts +27 -0
- package/dist/types/buffer.d.ts.map +1 -0
- package/dist/types/client.d.ts +86 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/idempotency.d.ts +21 -0
- package/dist/types/idempotency.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/limits.d.ts +80 -0
- package/dist/types/limits.d.ts.map +1 -0
- package/dist/types/middleware/common.d.ts +22 -0
- package/dist/types/middleware/common.d.ts.map +1 -0
- package/dist/types/middleware/express.d.ts +15 -0
- package/dist/types/middleware/express.d.ts.map +1 -0
- package/dist/types/middleware/fastify.d.ts +13 -0
- package/dist/types/middleware/fastify.d.ts.map +1 -0
- package/dist/types/middleware/koa.d.ts +13 -0
- package/dist/types/middleware/koa.d.ts.map +1 -0
- package/dist/types/path-normalizer.d.ts +15 -0
- package/dist/types/path-normalizer.d.ts.map +1 -0
- package/dist/types/transport.d.ts +39 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/types.d.ts +128 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @aforoai/metering
|
|
2
|
+
|
|
3
|
+
Track API usage from your Node service and send it to Aforo for billing. A buffered, batched, retrying client plus drop-in Express / Fastify / Koa middleware — `track()` returns immediately and events flush in the background, so metering never sits in your request path.
|
|
4
|
+
|
|
5
|
+
**Version:** 1.0.0 · Apache-2.0 · [Changelog](CHANGELOG.md) · [User guide](USER_GUIDE.md)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @aforoai/metering
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> **Not yet on the public npm registry.** Until it's published, install from source:
|
|
14
|
+
> ```bash
|
|
15
|
+
> git clone https://github.com/aforoai/SDKs.git
|
|
16
|
+
> cd SDKs/aforo-metering-sdks/node
|
|
17
|
+
> npm install && npm run build
|
|
18
|
+
> npm pack # produces aforo-metering-1.0.0.tgz to install in your app
|
|
19
|
+
> # then in your app: npm i /path/to/aforo-metering-1.0.0.tgz
|
|
20
|
+
> ```
|
|
21
|
+
|
|
22
|
+
Requires Node >= 18 (uses the built-in `fetch`).
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
Meter a single event:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { AforoClient } from '@aforoai/metering';
|
|
30
|
+
|
|
31
|
+
const aforo = new AforoClient({ apiKey: process.env.AFORO_API_KEY!, productType: 'API' });
|
|
32
|
+
|
|
33
|
+
await aforo.track({ customerId: 'cust_123', metricName: 'api_calls', quantity: 1 });
|
|
34
|
+
|
|
35
|
+
// Flush remaining events before the process exits:
|
|
36
|
+
await aforo.shutdown();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or meter every HTTP request with middleware — no per-route code:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { expressMiddleware } from '@aforoai/metering/middleware/express';
|
|
43
|
+
|
|
44
|
+
app.use(expressMiddleware({
|
|
45
|
+
apiKey: process.env.AFORO_API_KEY!,
|
|
46
|
+
customerId: (req) => req.user?.id ?? null, // return null to skip metering this request
|
|
47
|
+
metricName: 'api_calls', // or (req, res) => string; must exist in your Aforo catalog
|
|
48
|
+
productType: 'API', // default: the client default ("API")
|
|
49
|
+
}));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The middleware hooks `res.on('finish')`, so it runs after the response is sent — zero added latency.
|
|
53
|
+
|
|
54
|
+
- **Metric:** `metricName` is a fixed name or a `(req, res) => string` resolver; the default is `"api_calls"` (exported as `DEFAULT_METRIC_NAME`). The metric must exist in your tenant's Aforo catalog: the ingestor rejects an unknown `metricName`, and because it validates a batch as a whole, one rejected event fails every event in that batch.
|
|
55
|
+
- **Customer:** `customerId` is a fixed id or a `(req) => string | null` resolver; the default is `req.user.id` / `req.user.sub`, then the `X-Customer-Id` header. The caller's `X-Api-Key` header is never used — it is the end user's secret, not a customer id. Requests with no customer are not metered.
|
|
56
|
+
- **CORS preflights** (`OPTIONS`) are never metered, nor are requests whose quantity is `<= 0`.
|
|
57
|
+
- **HTTP context:** each event carries top-level `endpointPath` (path without query string, max 512 chars), `httpMethod`, `statusCode` and `responseTimeMs`, plus `productType`.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
`new AforoClient(options)` — `options: AforoOptions`:
|
|
62
|
+
|
|
63
|
+
| Option | Type | Default | What it does |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `apiKey` | `string` | — (required) | Aforo API key, sent as `X-API-Key`. |
|
|
66
|
+
| `baseUrl` | `string` | `https://api.aforo.ai` | Ingestor base URL. Events POST to `<baseUrl>/v1/ingest/batch`. |
|
|
67
|
+
| `productType` | `string` | `"API"` | Top-level `productType` stamped on every event (required by the ingestor). One of `API`, `AGENTIC_API`, `AI_AGENT`, `MCP_SERVER`, `GRPC_API`, `GRAPHQL_API`, `WEBSOCKET_API`, `MQTT_BROKER`; trimmed and uppercased. |
|
|
68
|
+
| `flushCount` | `number` | `50` | Buffered events that trigger a flush (and events per request; capped at 1000). |
|
|
69
|
+
| `flushInterval` | `number` (ms) | `5000` | Background flush cadence. |
|
|
70
|
+
| `maxQueueSize` | `number` | `10000` | Ring-buffer cap; oldest events drop on overflow. |
|
|
71
|
+
| `maxRetries` | `number` | `3` | Retries on 5xx/timeout (exponential backoff). |
|
|
72
|
+
| `retryBaseMs` | `number` (ms) | `1000` | Base backoff delay. |
|
|
73
|
+
| `timeout` | `number` (ms) | `10000` | Per-request timeout. |
|
|
74
|
+
| `shutdownTimeoutMs` | `number` (ms) | `5000` | Max time `shutdown()` waits for a final flush. |
|
|
75
|
+
|
|
76
|
+
`track(event)` — `event: TrackEvent`: `customerId` (required, non-blank), `metricName` (required, non-blank), `quantity` (default 1, must be > 0 — otherwise `track()` throws), `productType` (overrides the client default for this event), `idempotencyKey` (a fresh random UUID v4 per event if omitted — pass your own key if you want the ingestor to deduplicate retries), `occurredAt` (ISO string or epoch ms; defaults to now), `metadata` (string/number/boolean map).
|
|
77
|
+
|
|
78
|
+
### Sessions
|
|
79
|
+
|
|
80
|
+
`startSession(sessionId, productType = 'AI_AGENT')` sends a `system.session.heartbeat` event immediately and every 30s; `endSession()` sends a final `SESSION_END` heartbeat and flushes buffered usage. Heartbeats carry quantity 1, top-level `sessionId`/`productType`/`sessionBoundary`, and customer `system`; the ingestor intercepts them before billing. Each one goes in its own request, never inside a usage batch, and is best-effort (no retries; failures never affect usage delivery). The timer does not keep the process alive and stops on `endSession()`/`shutdown()`.
|
|
81
|
+
|
|
82
|
+
## Walk me through it
|
|
83
|
+
|
|
84
|
+
Step-by-step from install to a verified event in Aforo: see the **[User guide](USER_GUIDE.md)**.
|
|
85
|
+
|
|
86
|
+
## What this doesn't cover
|
|
87
|
+
|
|
88
|
+
This SDK only *emits* usage. Pricing, rate plans, and which `metricName`s are billable are configured in the Aforo console — this package does not create or validate them. A `metricName` that isn't defined in your tenant is **rejected** by the ingestor — and fails the whole batch it travels in — so define the metric before sending it.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ResolvedEvent } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Bounded ring buffer for usage events.
|
|
4
|
+
* Thread-safe for Node.js single-threaded event loop.
|
|
5
|
+
* When full, the oldest event is dropped (FIFO overflow).
|
|
6
|
+
*/
|
|
7
|
+
export declare class RingBuffer {
|
|
8
|
+
private readonly items;
|
|
9
|
+
private readonly capacity;
|
|
10
|
+
private head;
|
|
11
|
+
private tail;
|
|
12
|
+
private count;
|
|
13
|
+
constructor(capacity?: number);
|
|
14
|
+
/** Add an event to the buffer. Returns true if added, false if overflow (oldest dropped). */
|
|
15
|
+
push(event: ResolvedEvent): boolean;
|
|
16
|
+
/** Drain all events from the buffer. Returns a new array and clears the buffer. */
|
|
17
|
+
drain(): ResolvedEvent[];
|
|
18
|
+
/** Drain up to `max` events from the front of the buffer. */
|
|
19
|
+
drainUpTo(max: number): ResolvedEvent[];
|
|
20
|
+
/** Current number of events in the buffer. */
|
|
21
|
+
get size(): number;
|
|
22
|
+
/** Whether the buffer is empty. */
|
|
23
|
+
get isEmpty(): boolean;
|
|
24
|
+
/** Whether the buffer is at capacity. */
|
|
25
|
+
get isFull(): boolean;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/buffer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,KAAK,CAAK;gBAEN,QAAQ,GAAE,MAAe;IAMrC,6FAA6F;IAC7F,IAAI,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO;IAiBnC,mFAAmF;IACnF,KAAK,IAAI,aAAa,EAAE;IAgBxB,6DAA6D;IAC7D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE;IAgBvC,8CAA8C;IAC9C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mCAAmC;IACnC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,OAAO,CAEpB;CACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RingBuffer = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Bounded ring buffer for usage events.
|
|
6
|
+
* Thread-safe for Node.js single-threaded event loop.
|
|
7
|
+
* When full, the oldest event is dropped (FIFO overflow).
|
|
8
|
+
*/
|
|
9
|
+
class RingBuffer {
|
|
10
|
+
constructor(capacity = 10000) {
|
|
11
|
+
this.head = 0;
|
|
12
|
+
this.tail = 0;
|
|
13
|
+
this.count = 0;
|
|
14
|
+
if (capacity < 1)
|
|
15
|
+
throw new Error('Buffer capacity must be >= 1');
|
|
16
|
+
this.capacity = capacity;
|
|
17
|
+
this.items = new Array(capacity);
|
|
18
|
+
}
|
|
19
|
+
/** Add an event to the buffer. Returns true if added, false if overflow (oldest dropped). */
|
|
20
|
+
push(event) {
|
|
21
|
+
let overflow = false;
|
|
22
|
+
if (this.count === this.capacity) {
|
|
23
|
+
// Buffer full — drop oldest (advance head)
|
|
24
|
+
this.head = (this.head + 1) % this.capacity;
|
|
25
|
+
this.count--;
|
|
26
|
+
overflow = true;
|
|
27
|
+
}
|
|
28
|
+
this.items[this.tail] = event;
|
|
29
|
+
this.tail = (this.tail + 1) % this.capacity;
|
|
30
|
+
this.count++;
|
|
31
|
+
return !overflow;
|
|
32
|
+
}
|
|
33
|
+
/** Drain all events from the buffer. Returns a new array and clears the buffer. */
|
|
34
|
+
drain() {
|
|
35
|
+
if (this.count === 0)
|
|
36
|
+
return [];
|
|
37
|
+
const result = [];
|
|
38
|
+
for (let i = 0; i < this.count; i++) {
|
|
39
|
+
const idx = (this.head + i) % this.capacity;
|
|
40
|
+
result.push(this.items[idx]);
|
|
41
|
+
}
|
|
42
|
+
this.head = 0;
|
|
43
|
+
this.tail = 0;
|
|
44
|
+
this.count = 0;
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/** Drain up to `max` events from the front of the buffer. */
|
|
48
|
+
drainUpTo(max) {
|
|
49
|
+
if (this.count === 0 || max <= 0)
|
|
50
|
+
return [];
|
|
51
|
+
const take = Math.min(max, this.count);
|
|
52
|
+
const result = [];
|
|
53
|
+
for (let i = 0; i < take; i++) {
|
|
54
|
+
const idx = (this.head + i) % this.capacity;
|
|
55
|
+
result.push(this.items[idx]);
|
|
56
|
+
}
|
|
57
|
+
this.head = (this.head + take) % this.capacity;
|
|
58
|
+
this.count -= take;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/** Current number of events in the buffer. */
|
|
62
|
+
get size() {
|
|
63
|
+
return this.count;
|
|
64
|
+
}
|
|
65
|
+
/** Whether the buffer is empty. */
|
|
66
|
+
get isEmpty() {
|
|
67
|
+
return this.count === 0;
|
|
68
|
+
}
|
|
69
|
+
/** Whether the buffer is at capacity. */
|
|
70
|
+
get isFull() {
|
|
71
|
+
return this.count === this.capacity;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.RingBuffer = RingBuffer;
|
|
75
|
+
//# sourceMappingURL=buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffer.js","sourceRoot":"","sources":["../../src/buffer.ts"],"names":[],"mappings":";;;AAEA;;;;GAIG;AACH,MAAa,UAAU;IAOrB,YAAY,WAAmB,KAAM;QAJ7B,SAAI,GAAG,CAAC,CAAC;QACT,SAAI,GAAG,CAAC,CAAC;QACT,UAAK,GAAG,CAAC,CAAC;QAGhB,IAAI,QAAQ,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED,6FAA6F;IAC7F,IAAI,CAAC,KAAoB;QACvB,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,2CAA2C;YAC3C,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,OAAO,CAAC,QAAQ,CAAC;IACnB,CAAC;IAED,mFAAmF;IACnF,KAAK;QACH,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEhC,MAAM,MAAM,GAAoB,EAAE,CAAC;QACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/B,CAAC;QAED,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QAEf,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,6DAA6D;IAC7D,SAAS,CAAC,GAAW;QACnB,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QAE5C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,MAAM,GAAoB,EAAE,CAAC;QACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/B,CAAC;QAED,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/C,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;QAEnB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,8CAA8C;IAC9C,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,yCAAyC;IACzC,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,QAAQ,CAAC;IACtC,CAAC;CACF;AA/ED,gCA+EC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AforoOptions, TrackEvent, FlushResult } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Aforo metering client.
|
|
4
|
+
*
|
|
5
|
+
* Enqueues usage events into a ring buffer, flushes them in batches
|
|
6
|
+
* to the Aforo ingestor service via HTTP. Non-blocking — `track()`
|
|
7
|
+
* returns immediately, flushing happens in the background.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const client = new AforoClient({ apiKey: 'your-key', productType: 'API' });
|
|
11
|
+
* await client.track({ customerId: 'cust_1', metricName: 'api_calls', quantity: 1 });
|
|
12
|
+
* // On shutdown:
|
|
13
|
+
* await client.shutdown();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare class AforoClient {
|
|
17
|
+
private readonly buffer;
|
|
18
|
+
private readonly transport;
|
|
19
|
+
private readonly flushCount;
|
|
20
|
+
private readonly flushInterval;
|
|
21
|
+
private readonly shutdownTimeoutMs;
|
|
22
|
+
private readonly productType;
|
|
23
|
+
private flushTimer;
|
|
24
|
+
private flushing;
|
|
25
|
+
private closed;
|
|
26
|
+
private pendingFlush;
|
|
27
|
+
private heartbeatTimer;
|
|
28
|
+
private activeSessionId;
|
|
29
|
+
private sessionStartedAt;
|
|
30
|
+
private sessionProductType;
|
|
31
|
+
constructor(options: AforoOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Start a session and emit `system.session.heartbeat` events: one now, then
|
|
34
|
+
* every 30s until `endSession()` / `shutdown()`.
|
|
35
|
+
*
|
|
36
|
+
* Each heartbeat is POSTed in its own request (`{"events":[heartbeat]}`),
|
|
37
|
+
* never mixed into a usage batch, so it always takes the ingestor's
|
|
38
|
+
* synchronous path, where it is intercepted before billing. Heartbeats are
|
|
39
|
+
* best-effort: sent once, failures ignored, never affecting usage delivery.
|
|
40
|
+
* The timer is unref'd so it never keeps the process alive.
|
|
41
|
+
*
|
|
42
|
+
* @param sessionId - Unique session identifier
|
|
43
|
+
* @param productType - Product type for the session (default AI_AGENT)
|
|
44
|
+
*/
|
|
45
|
+
startSession(sessionId: string, productType?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* End the current session: stop heartbeats, send a final SESSION_END
|
|
48
|
+
* heartbeat (in its own request, best-effort) and flush buffered usage.
|
|
49
|
+
*/
|
|
50
|
+
endSession(): Promise<void>;
|
|
51
|
+
private stopHeartbeatTimer;
|
|
52
|
+
private emitSessionHeartbeat;
|
|
53
|
+
/**
|
|
54
|
+
* Enqueue a usage event for batched delivery.
|
|
55
|
+
* Returns immediately — does not await HTTP.
|
|
56
|
+
* Triggers a flush if the buffer reaches flushCount.
|
|
57
|
+
*
|
|
58
|
+
* Throws if the event breaks any constraint the ingestor enforces — a blank
|
|
59
|
+
* or over-long `customerId` / `metricName` / `idempotencyKey`, a `quantity`
|
|
60
|
+
* that is not positive or carries more than 14 integer digits / 6 decimal
|
|
61
|
+
* places, an `occurredAt` that is not an ISO-8601 instant, or any other
|
|
62
|
+
* capped field that is too long. Limits the server makes configurable (event
|
|
63
|
+
* age, clock skew, metadata size) are left to the server, so the SDK can
|
|
64
|
+
* never refuse usage a given deployment would accept. Such an event is rejected by the ingestor and never billed,
|
|
65
|
+
* and because flushing happens in the background nobody would see that
|
|
66
|
+
* rejection — so it is reported here instead, and never enters the buffer.
|
|
67
|
+
* See `limits.ts` for the limits and where each one comes from.
|
|
68
|
+
*/
|
|
69
|
+
track(event: TrackEvent): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Force-flush all buffered events to the ingestor.
|
|
72
|
+
* Safe to call concurrently — only one flush runs at a time.
|
|
73
|
+
*/
|
|
74
|
+
flush(): Promise<FlushResult>;
|
|
75
|
+
private doFlush;
|
|
76
|
+
/**
|
|
77
|
+
* Flush remaining events and stop the client.
|
|
78
|
+
* After shutdown, track() will throw.
|
|
79
|
+
*/
|
|
80
|
+
shutdown(): Promise<void>;
|
|
81
|
+
/** Number of events currently buffered. */
|
|
82
|
+
get bufferedCount(): number;
|
|
83
|
+
/** Whether the client has been shut down. */
|
|
84
|
+
get isShutdown(): boolean;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAiB,WAAW,EAAE,MAAM,SAAS,CAAC;AAuB/E;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAqC;IAGzD,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,kBAAkB,CAAwC;gBAEtD,OAAO,EAAE,YAAY;IAsCjC;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,GAAE,MAAqC,GAAG,IAAI;IAezF;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,oBAAoB;IAgC5B;;;;;;;;;;;;;;;OAeG;IACG,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA+D7C;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;YAgBrB,OAAO;IAgBrB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB/B,2CAA2C;IAC3C,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED,6CAA6C;IAC7C,IAAI,UAAU,IAAI,OAAO,CAExB;CACF"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AforoClient = void 0;
|
|
4
|
+
const buffer_1 = require("./buffer");
|
|
5
|
+
const transport_1 = require("./transport");
|
|
6
|
+
const idempotency_1 = require("./idempotency");
|
|
7
|
+
const limits_1 = require("./limits");
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://api.aforo.ai';
|
|
9
|
+
const DEFAULT_PRODUCT_TYPE = 'API';
|
|
10
|
+
const DEFAULT_SESSION_PRODUCT_TYPE = 'AI_AGENT';
|
|
11
|
+
/** Ingestor hard limit on events per batch request (IngestBatchRequest @Size(max = 1000)). */
|
|
12
|
+
const MAX_BATCH_SIZE = 1000;
|
|
13
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
14
|
+
const HEARTBEAT_METRIC = 'system.session.heartbeat';
|
|
15
|
+
/** Heartbeats carry no billable customer; the ingestor intercepts them before billing. */
|
|
16
|
+
const HEARTBEAT_CUSTOMER_ID = 'system';
|
|
17
|
+
const DEFAULT_FLUSH_COUNT = 50;
|
|
18
|
+
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
19
|
+
const DEFAULT_MAX_QUEUE_SIZE = 10000;
|
|
20
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
21
|
+
const DEFAULT_RETRY_BASE_MS = 1000;
|
|
22
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
23
|
+
const DEFAULT_SHUTDOWN_TIMEOUT = 5000;
|
|
24
|
+
/**
|
|
25
|
+
* Aforo metering client.
|
|
26
|
+
*
|
|
27
|
+
* Enqueues usage events into a ring buffer, flushes them in batches
|
|
28
|
+
* to the Aforo ingestor service via HTTP. Non-blocking — `track()`
|
|
29
|
+
* returns immediately, flushing happens in the background.
|
|
30
|
+
*
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const client = new AforoClient({ apiKey: 'your-key', productType: 'API' });
|
|
33
|
+
* await client.track({ customerId: 'cust_1', metricName: 'api_calls', quantity: 1 });
|
|
34
|
+
* // On shutdown:
|
|
35
|
+
* await client.shutdown();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
class AforoClient {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.flushTimer = null;
|
|
41
|
+
this.flushing = false;
|
|
42
|
+
this.closed = false;
|
|
43
|
+
this.pendingFlush = null;
|
|
44
|
+
// Session heartbeat state
|
|
45
|
+
this.heartbeatTimer = null;
|
|
46
|
+
this.activeSessionId = null;
|
|
47
|
+
this.sessionStartedAt = null;
|
|
48
|
+
this.sessionProductType = DEFAULT_SESSION_PRODUCT_TYPE;
|
|
49
|
+
if (!options.apiKey)
|
|
50
|
+
throw new Error('apiKey is required');
|
|
51
|
+
this.flushCount = Math.max(1, Math.min(options.flushCount ?? DEFAULT_FLUSH_COUNT, MAX_BATCH_SIZE));
|
|
52
|
+
this.productType = normalizeProductType(options.productType) ?? DEFAULT_PRODUCT_TYPE;
|
|
53
|
+
this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
|
|
54
|
+
this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT;
|
|
55
|
+
this.buffer = new buffer_1.RingBuffer(options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE);
|
|
56
|
+
this.transport = new transport_1.Transport({
|
|
57
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
58
|
+
apiKey: options.apiKey,
|
|
59
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT,
|
|
60
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
61
|
+
retryBaseMs: options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS,
|
|
62
|
+
});
|
|
63
|
+
// Start periodic flush timer
|
|
64
|
+
this.flushTimer = setInterval(() => {
|
|
65
|
+
this.flush().catch(() => { });
|
|
66
|
+
}, this.flushInterval);
|
|
67
|
+
// Unref the timer so it doesn't prevent Node from exiting
|
|
68
|
+
if (this.flushTimer && typeof this.flushTimer === 'object' && 'unref' in this.flushTimer) {
|
|
69
|
+
this.flushTimer.unref();
|
|
70
|
+
}
|
|
71
|
+
// Register graceful shutdown handlers
|
|
72
|
+
const shutdownHandler = () => {
|
|
73
|
+
this.shutdown().catch(() => { });
|
|
74
|
+
};
|
|
75
|
+
process.once('SIGTERM', shutdownHandler);
|
|
76
|
+
process.once('SIGINT', shutdownHandler);
|
|
77
|
+
}
|
|
78
|
+
// ─── Session lifecycle with heartbeat ─────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Start a session and emit `system.session.heartbeat` events: one now, then
|
|
81
|
+
* every 30s until `endSession()` / `shutdown()`.
|
|
82
|
+
*
|
|
83
|
+
* Each heartbeat is POSTed in its own request (`{"events":[heartbeat]}`),
|
|
84
|
+
* never mixed into a usage batch, so it always takes the ingestor's
|
|
85
|
+
* synchronous path, where it is intercepted before billing. Heartbeats are
|
|
86
|
+
* best-effort: sent once, failures ignored, never affecting usage delivery.
|
|
87
|
+
* The timer is unref'd so it never keeps the process alive.
|
|
88
|
+
*
|
|
89
|
+
* @param sessionId - Unique session identifier
|
|
90
|
+
* @param productType - Product type for the session (default AI_AGENT)
|
|
91
|
+
*/
|
|
92
|
+
startSession(sessionId, productType = DEFAULT_SESSION_PRODUCT_TYPE) {
|
|
93
|
+
if (this.closed || !sessionId || !String(sessionId).trim())
|
|
94
|
+
return;
|
|
95
|
+
this.stopHeartbeatTimer();
|
|
96
|
+
this.activeSessionId = String(sessionId);
|
|
97
|
+
this.sessionStartedAt = Date.now();
|
|
98
|
+
this.sessionProductType = normalizeProductType(productType) ?? DEFAULT_SESSION_PRODUCT_TYPE;
|
|
99
|
+
this.emitSessionHeartbeat('HEARTBEAT');
|
|
100
|
+
this.heartbeatTimer = setInterval(() => this.emitSessionHeartbeat('HEARTBEAT'), HEARTBEAT_INTERVAL_MS);
|
|
101
|
+
if (this.heartbeatTimer && typeof this.heartbeatTimer === 'object' && 'unref' in this.heartbeatTimer) {
|
|
102
|
+
this.heartbeatTimer.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* End the current session: stop heartbeats, send a final SESSION_END
|
|
107
|
+
* heartbeat (in its own request, best-effort) and flush buffered usage.
|
|
108
|
+
*/
|
|
109
|
+
async endSession() {
|
|
110
|
+
this.stopHeartbeatTimer();
|
|
111
|
+
const end = this.activeSessionId ? this.emitSessionHeartbeat('SESSION_END') : Promise.resolve();
|
|
112
|
+
this.activeSessionId = null;
|
|
113
|
+
this.sessionStartedAt = null;
|
|
114
|
+
await Promise.all([end, this.flush()]);
|
|
115
|
+
}
|
|
116
|
+
stopHeartbeatTimer() {
|
|
117
|
+
if (this.heartbeatTimer) {
|
|
118
|
+
clearInterval(this.heartbeatTimer);
|
|
119
|
+
this.heartbeatTimer = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
emitSessionHeartbeat(boundary) {
|
|
123
|
+
const sessionId = this.activeSessionId;
|
|
124
|
+
if (!sessionId || (this.closed && boundary === 'HEARTBEAT'))
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const occurredAt = new Date(now).toISOString();
|
|
128
|
+
const heartbeat = {
|
|
129
|
+
customerId: HEARTBEAT_CUSTOMER_ID,
|
|
130
|
+
metricName: HEARTBEAT_METRIC,
|
|
131
|
+
// quantity must be > 0 to pass the ingestor's bean validation; the
|
|
132
|
+
// heartbeat is intercepted before billing and never counted as usage.
|
|
133
|
+
quantity: 1,
|
|
134
|
+
idempotencyKey: `hb:${boundary === 'SESSION_END' ? 'end:' : ''}${sessionId}:${now}:${randomSuffix()}`.slice(0, 255),
|
|
135
|
+
occurredAt,
|
|
136
|
+
productType: this.sessionProductType,
|
|
137
|
+
sessionId,
|
|
138
|
+
sessionBoundary: boundary,
|
|
139
|
+
metadata: {
|
|
140
|
+
sessionId,
|
|
141
|
+
sessionBoundary: boundary,
|
|
142
|
+
productType: this.sessionProductType,
|
|
143
|
+
heartbeatType: boundary === 'SESSION_END' ? 'SESSION_END' : 'PERIODIC',
|
|
144
|
+
uptimeMs: now - (this.sessionStartedAt ?? now),
|
|
145
|
+
sdkLanguage: 'node',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
return this.transport.sendSingleBestEffort(heartbeat).then(() => undefined, () => undefined);
|
|
149
|
+
}
|
|
150
|
+
// ─── Event tracking ──────────────────────────────────────────────
|
|
151
|
+
/**
|
|
152
|
+
* Enqueue a usage event for batched delivery.
|
|
153
|
+
* Returns immediately — does not await HTTP.
|
|
154
|
+
* Triggers a flush if the buffer reaches flushCount.
|
|
155
|
+
*
|
|
156
|
+
* Throws if the event breaks any constraint the ingestor enforces — a blank
|
|
157
|
+
* or over-long `customerId` / `metricName` / `idempotencyKey`, a `quantity`
|
|
158
|
+
* that is not positive or carries more than 14 integer digits / 6 decimal
|
|
159
|
+
* places, an `occurredAt` that is not an ISO-8601 instant, or any other
|
|
160
|
+
* capped field that is too long. Limits the server makes configurable (event
|
|
161
|
+
* age, clock skew, metadata size) are left to the server, so the SDK can
|
|
162
|
+
* never refuse usage a given deployment would accept. Such an event is rejected by the ingestor and never billed,
|
|
163
|
+
* and because flushing happens in the background nobody would see that
|
|
164
|
+
* rejection — so it is reported here instead, and never enters the buffer.
|
|
165
|
+
* See `limits.ts` for the limits and where each one comes from.
|
|
166
|
+
*/
|
|
167
|
+
async track(event) {
|
|
168
|
+
if (this.closed) {
|
|
169
|
+
throw new Error('AforoClient is shut down — cannot track new events');
|
|
170
|
+
}
|
|
171
|
+
if (event.customerId === undefined || event.customerId === null || !String(event.customerId).trim()) {
|
|
172
|
+
throw new Error('customerId is required');
|
|
173
|
+
}
|
|
174
|
+
if (event.metricName === undefined || event.metricName === null || !String(event.metricName).trim()) {
|
|
175
|
+
throw new Error('metricName is required');
|
|
176
|
+
}
|
|
177
|
+
const occurredAt = resolveOccurredAt(event.occurredAt);
|
|
178
|
+
const quantity = event.quantity ?? 1;
|
|
179
|
+
if (typeof quantity !== 'number' || !Number.isFinite(quantity) || quantity <= 0) {
|
|
180
|
+
throw new Error('quantity must be a positive number (> 0)');
|
|
181
|
+
}
|
|
182
|
+
const productType = normalizeProductType(event.productType) ?? this.productType;
|
|
183
|
+
// Minted once, here, when the event is enqueued — never at flush/retry
|
|
184
|
+
// time, so a retried batch carries the same keys and the ingestor
|
|
185
|
+
// deduplicates it. A caller-supplied key is passed through verbatim;
|
|
186
|
+
// otherwise each event gets its own random UUID. A deterministic hash of
|
|
187
|
+
// the event fields would make two genuinely distinct events in the same
|
|
188
|
+
// millisecond collide, and the ingestor would silently drop the second.
|
|
189
|
+
// Minted before the limit check so an over-long caller-supplied key is
|
|
190
|
+
// reported here rather than silently dropped by the server later.
|
|
191
|
+
const idempotencyKey = event.idempotencyKey ?? (0, idempotency_1.generateRandomKey)();
|
|
192
|
+
const violation = (0, limits_1.describeLimitViolation)({
|
|
193
|
+
customerId: event.customerId,
|
|
194
|
+
metricName: event.metricName,
|
|
195
|
+
quantity,
|
|
196
|
+
idempotencyKey,
|
|
197
|
+
occurredAt,
|
|
198
|
+
productType,
|
|
199
|
+
endpointPath: event.endpointPath,
|
|
200
|
+
httpMethod: event.httpMethod,
|
|
201
|
+
});
|
|
202
|
+
if (violation)
|
|
203
|
+
throw new Error(violation);
|
|
204
|
+
const resolved = {
|
|
205
|
+
customerId: event.customerId,
|
|
206
|
+
metricName: event.metricName,
|
|
207
|
+
quantity,
|
|
208
|
+
idempotencyKey,
|
|
209
|
+
occurredAt,
|
|
210
|
+
productType,
|
|
211
|
+
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
212
|
+
...(event.endpointPath !== undefined ? { endpointPath: event.endpointPath } : {}),
|
|
213
|
+
...(event.httpMethod !== undefined ? { httpMethod: event.httpMethod } : {}),
|
|
214
|
+
...(event.statusCode !== undefined ? { statusCode: event.statusCode } : {}),
|
|
215
|
+
...(event.responseTimeMs !== undefined ? { responseTimeMs: event.responseTimeMs } : {}),
|
|
216
|
+
};
|
|
217
|
+
this.buffer.push(resolved);
|
|
218
|
+
// Trigger flush if buffer threshold reached
|
|
219
|
+
if (this.buffer.size >= this.flushCount) {
|
|
220
|
+
this.flush().catch(() => { });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Force-flush all buffered events to the ingestor.
|
|
225
|
+
* Safe to call concurrently — only one flush runs at a time.
|
|
226
|
+
*/
|
|
227
|
+
async flush() {
|
|
228
|
+
if (this.flushing && this.pendingFlush) {
|
|
229
|
+
return this.pendingFlush;
|
|
230
|
+
}
|
|
231
|
+
this.flushing = true;
|
|
232
|
+
this.pendingFlush = this.doFlush();
|
|
233
|
+
try {
|
|
234
|
+
return await this.pendingFlush;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
this.flushing = false;
|
|
238
|
+
this.pendingFlush = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async doFlush() {
|
|
242
|
+
let totalSent = 0;
|
|
243
|
+
let totalFailed = 0;
|
|
244
|
+
while (!this.buffer.isEmpty) {
|
|
245
|
+
const batch = this.buffer.drainUpTo(this.flushCount);
|
|
246
|
+
if (batch.length === 0)
|
|
247
|
+
break;
|
|
248
|
+
const result = await this.transport.send(batch);
|
|
249
|
+
totalSent += result.sent;
|
|
250
|
+
totalFailed += result.failed;
|
|
251
|
+
}
|
|
252
|
+
return { sent: totalSent, failed: totalFailed };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Flush remaining events and stop the client.
|
|
256
|
+
* After shutdown, track() will throw.
|
|
257
|
+
*/
|
|
258
|
+
async shutdown() {
|
|
259
|
+
if (this.closed)
|
|
260
|
+
return;
|
|
261
|
+
this.closed = true;
|
|
262
|
+
// Stop session heartbeats
|
|
263
|
+
this.stopHeartbeatTimer();
|
|
264
|
+
// Clear periodic flush timer
|
|
265
|
+
if (this.flushTimer) {
|
|
266
|
+
clearInterval(this.flushTimer);
|
|
267
|
+
this.flushTimer = null;
|
|
268
|
+
}
|
|
269
|
+
// Flush with timeout
|
|
270
|
+
await Promise.race([
|
|
271
|
+
this.flush(),
|
|
272
|
+
new Promise((resolve) => setTimeout(resolve, this.shutdownTimeoutMs)),
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
/** Number of events currently buffered. */
|
|
276
|
+
get bufferedCount() {
|
|
277
|
+
return this.buffer.size;
|
|
278
|
+
}
|
|
279
|
+
/** Whether the client has been shut down. */
|
|
280
|
+
get isShutdown() {
|
|
281
|
+
return this.closed;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
exports.AforoClient = AforoClient;
|
|
285
|
+
function normalizeProductType(value) {
|
|
286
|
+
if (value === undefined || value === null)
|
|
287
|
+
return undefined;
|
|
288
|
+
const trimmed = String(value).trim();
|
|
289
|
+
return trimmed ? trimmed.toUpperCase() : undefined;
|
|
290
|
+
}
|
|
291
|
+
function randomSuffix() {
|
|
292
|
+
return Math.random().toString(36).slice(2, 10);
|
|
293
|
+
}
|
|
294
|
+
function resolveOccurredAt(value) {
|
|
295
|
+
if (!value)
|
|
296
|
+
return new Date().toISOString();
|
|
297
|
+
if (typeof value === 'number')
|
|
298
|
+
return new Date(value).toISOString();
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a deterministic idempotency key from event fields.
|
|
3
|
+
* Uses SHA-256, truncated to 32 hex chars for compact storage.
|
|
4
|
+
*
|
|
5
|
+
* Formula: SHA256(customerId + metricName + quantity + occurredAt)
|
|
6
|
+
*
|
|
7
|
+
* NOT the client default any more. `occurredAt` only carries millisecond
|
|
8
|
+
* precision, so two genuinely distinct events for the same customer + metric
|
|
9
|
+
* + quantity inside one millisecond hash to the same key and the ingestor
|
|
10
|
+
* drops the second as a DUPLICATE — silent under-billing. Kept exported for
|
|
11
|
+
* callers who deliberately want content-addressed dedup (e.g. replaying a
|
|
12
|
+
* fixed batch) and pass the result to `track({ idempotencyKey })` themselves.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateIdempotencyKey(customerId: string, metricName: string, quantity: number, occurredAt: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a random UUID v4. This is the default key for an event whose caller
|
|
17
|
+
* supplied none: every event gets its own key, so no two distinct events can
|
|
18
|
+
* collide. Dedup stays opt-in via an explicit `idempotencyKey`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateRandomKey(): string;
|
|
21
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/idempotency.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|