@shiprev/server-sdk-node 0.0.0-stage → 0.1.0-beta.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.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +253 -2
  3. package/dist/auth/token-verifier.d.ts +42 -0
  4. package/dist/auth/token-verifier.js +230 -0
  5. package/dist/client/connection-supervisor.d.ts +117 -0
  6. package/dist/client/connection-supervisor.js +547 -0
  7. package/dist/client/shiprev-client.d.ts +106 -0
  8. package/dist/client/shiprev-client.js +267 -0
  9. package/dist/index.d.ts +16 -0
  10. package/dist/index.js +47 -0
  11. package/dist/metering/backlog/host-adapter-backlog.d.ts +10 -0
  12. package/dist/metering/backlog/host-adapter-backlog.js +27 -0
  13. package/dist/metering/backlog/journal-backlog.d.ts +14 -0
  14. package/dist/metering/backlog/journal-backlog.js +135 -0
  15. package/dist/metering/backlog/memory-backlog.d.ts +9 -0
  16. package/dist/metering/backlog/memory-backlog.js +34 -0
  17. package/dist/metering/usage-buffer.d.ts +59 -0
  18. package/dist/metering/usage-buffer.js +175 -0
  19. package/dist/mirror/entitlement-mirror.d.ts +47 -0
  20. package/dist/mirror/entitlement-mirror.js +109 -0
  21. package/dist/nest/decorators.d.ts +15 -0
  22. package/dist/nest/decorators.js +21 -0
  23. package/dist/nest/index.d.ts +4 -0
  24. package/dist/nest/index.js +21 -0
  25. package/dist/nest/shiprev-auth.guard.d.ts +7 -0
  26. package/dist/nest/shiprev-auth.guard.js +52 -0
  27. package/dist/nest/shiprev-capability.guard.d.ts +9 -0
  28. package/dist/nest/shiprev-capability.guard.js +65 -0
  29. package/dist/nest/shiprev.module.d.ts +17 -0
  30. package/dist/nest/shiprev.module.js +57 -0
  31. package/dist/ruling/adjudicator.d.ts +15 -0
  32. package/dist/ruling/adjudicator.js +131 -0
  33. package/dist/transport/event-stream.d.ts +27 -0
  34. package/dist/transport/event-stream.js +92 -0
  35. package/dist/transport/http.transport.d.ts +64 -0
  36. package/dist/transport/http.transport.js +218 -0
  37. package/dist/transport/wire.d.ts +79 -0
  38. package/dist/transport/wire.js +8 -0
  39. package/dist/types.d.ts +191 -0
  40. package/dist/types.js +25 -0
  41. package/dist/util/decimal.d.ts +14 -0
  42. package/dist/util/decimal.js +57 -0
  43. package/package.json +98 -4
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShipRevClient = void 0;
4
+ const token_verifier_1 = require("../auth/token-verifier");
5
+ const usage_buffer_1 = require("../metering/usage-buffer");
6
+ const entitlement_mirror_1 = require("../mirror/entitlement-mirror");
7
+ const adjudicator_1 = require("../ruling/adjudicator");
8
+ const http_transport_1 = require("../transport/http.transport");
9
+ const types_1 = require("../types");
10
+ const connection_supervisor_1 = require("./connection-supervisor");
11
+ class ShipRevClient {
12
+ options;
13
+ transport;
14
+ tokenVerifier;
15
+ mirror;
16
+ adjudicator;
17
+ usageBuffer;
18
+ supervisor;
19
+ /** COLD until start() or setOnline(); otherwise the state follows the online signal and the mirror. */
20
+ active = false;
21
+ isOnline = false;
22
+ /** Issuer and application expected of session tokens, learned from the machine token. */
23
+ expectations = null;
24
+ /** Amounts reserved by this client, so `confirm()` without an amount confirms what was reserved. */
25
+ reservedAmounts = new Map();
26
+ constructor(options) {
27
+ this.options = options;
28
+ this.transport = new http_transport_1.HttpTransport({
29
+ baseUrl: options.base_url,
30
+ connectTimeoutMs: options.presets?.connect_timeout_ms,
31
+ });
32
+ this.tokenVerifier = new token_verifier_1.TokenVerifier(this.transport);
33
+ this.mirror = new entitlement_mirror_1.EntitlementMirror();
34
+ this.adjudicator = new adjudicator_1.Adjudicator(this.mirror, options.presets);
35
+ this.supervisor = new connection_supervisor_1.ConnectionSupervisor({
36
+ transport: this.transport,
37
+ mirror: this.mirror,
38
+ apiKey: options.api_key,
39
+ connectTimeoutMs: options.presets?.connect_timeout_ms ?? 5000,
40
+ loadKeys: () => this.tokenVerifier.refreshKeys(),
41
+ onToken: (token) => this.learnExpectations(token),
42
+ setOnline: (online) => this.applyOnline(online),
43
+ flush: () => this.usageBuffer.flush(),
44
+ });
45
+ this.usageBuffer = new usage_buffer_1.UsageBuffer({
46
+ transport: this.transport,
47
+ getToken: () => this.supervisor.currentToken(),
48
+ isOnline: () => this.isOnline,
49
+ send: (events) => this.supervisor.authorized((token) => this.transport.reportUsageEvents(events, token)),
50
+ bufferConfig: options.buffer,
51
+ backlogConfig: options.backlog,
52
+ onFlushFailure: options.on_flush_failure,
53
+ onRejected: options.on_rejected,
54
+ });
55
+ }
56
+ /**
57
+ * Connects the client: fetches the signing keys, exchanges the API key for a service token, opens
58
+ * the push stream and waits for its `ready` frame, then loads the full entitlement snapshot. Never
59
+ * throws: LIVE when all four succeed, DISCONNECTED when only the stream failed, COLD_OFFLINE when
60
+ * nothing could be loaded. From here until `stop()` the client renews its token, follows the push
61
+ * stream and reconnects by itself.
62
+ */
63
+ async start() {
64
+ this.active = true;
65
+ await this.supervisor.start();
66
+ return {
67
+ project_id: this.mirror.getProjectId(),
68
+ snapshot_revision: this.mirror.getRevision(),
69
+ state: this.state().state,
70
+ };
71
+ }
72
+ /**
73
+ * Closes the push stream, stops every timer and flushes buffered usage events.
74
+ */
75
+ async stop() {
76
+ this.supervisor.stop();
77
+ await this.usageBuffer.stop();
78
+ this.active = false;
79
+ this.isOnline = false;
80
+ }
81
+ /**
82
+ * Enqueues a metered usage event into the memory buffer for batched delivery.
83
+ */
84
+ async reportUsage(event) {
85
+ if (!Number.isFinite(event.quantity)) {
86
+ throw new RangeError(`Usage quantity must be a finite number, got ${event.quantity}`);
87
+ }
88
+ await this.usageBuffer.reportUsage(event);
89
+ }
90
+ /**
91
+ * Manually flushes currently buffered usage events to the core service.
92
+ */
93
+ async flush() {
94
+ await this.usageBuffer.flush();
95
+ }
96
+ /**
97
+ * Verifies an RS256 JWT token locally and extracts subject and isolation keys.
98
+ */
99
+ async verifyToken(jwt) {
100
+ return this.tokenVerifier.verifyToken(jwt, this.expectations);
101
+ }
102
+ /**
103
+ * Executes a four-tier local adjudication (synchronous, zero-IO, P95 <= 5ms).
104
+ */
105
+ adjudicate(options) {
106
+ return this.adjudicator.adjudicate(options, this.isOnline);
107
+ }
108
+ /**
109
+ * Asynchronous adjudication with authoritative fallback.
110
+ * If local mirror misses the organization or rule and client is online, queries core service and backfills mirror.
111
+ */
112
+ async adjudicateAsync(options) {
113
+ const ruling = this.adjudicate(options);
114
+ // If local ruling is already allowed (realtime or degraded read) or degraded write rejection, return directly
115
+ if (ruling.allowed || ruling.source === 'degraded' || !this.isOnline) {
116
+ return ruling;
117
+ }
118
+ // When the mirror misses the organization or capability and the client is online, ask the
119
+ // service for an authoritative ruling, then refill that organization from the snapshot endpoint.
120
+ if (this.supervisor.currentToken() && ruling.reason === 'capability_not_granted') {
121
+ try {
122
+ const remote = await this.supervisor.authorized((token) => this.transport.lookupAdjudication({
123
+ organization_id: options.organization_id,
124
+ capability_key: options.capability_key,
125
+ quantity: options.quantity,
126
+ operation: options.operation,
127
+ }, token));
128
+ if (remote.allowed) {
129
+ await this.refillOrganization(options.organization_id);
130
+ }
131
+ return {
132
+ allowed: remote.allowed,
133
+ remaining: remote.remaining,
134
+ limit: remote.limit,
135
+ source: 'authoritative',
136
+ reason: remote.reason,
137
+ adjudicated_at: Date.now(),
138
+ };
139
+ }
140
+ catch {
141
+ // Fall back to original ruling if remote call fails
142
+ return ruling;
143
+ }
144
+ }
145
+ return ruling;
146
+ }
147
+ /**
148
+ * Guard for write operations (fail-closed).
149
+ * Intercepts action if write operation is denied (e.g. disconnected).
150
+ */
151
+ async guardWrite(options, action) {
152
+ const ruling = this.adjudicate({ ...options, operation: 'write' });
153
+ if (!ruling.allowed) {
154
+ throw new types_1.DisconnectedWriteError(ruling.reason === 'disconnected_write'
155
+ ? 'Write operation denied while client is disconnected'
156
+ : `Write operation denied: ${ruling.reason ?? 'not allowed'}`);
157
+ }
158
+ return action();
159
+ }
160
+ /**
161
+ * Two-phase entitlement reservation: reserve
162
+ */
163
+ async reserve(options) {
164
+ const ttlSeconds = options.ttl_seconds ?? this.options.presets?.reservation_ttl_s;
165
+ const result = await this.supervisor.authorized((token) => this.transport.reserve(ttlSeconds !== undefined ? { ...options, ttl_seconds: ttlSeconds } : options, token));
166
+ this.reservedAmounts.set(result.reservation_id, options.amount);
167
+ return result;
168
+ }
169
+ /**
170
+ * Two-phase entitlement reservation: confirm. Without `actualAmount` the reserved amount is confirmed.
171
+ */
172
+ async confirm(reservationId, actualAmount) {
173
+ const amount = actualAmount ?? this.reservedAmounts.get(reservationId);
174
+ if (amount === undefined) {
175
+ throw new Error(`Reservation '${reservationId}' was not made by this client: pass the actual amount to confirm it`);
176
+ }
177
+ const result = await this.supervisor.authorized((token) => this.transport.confirmReservation(reservationId, amount, token));
178
+ this.reservedAmounts.delete(reservationId);
179
+ return result;
180
+ }
181
+ /**
182
+ * Two-phase entitlement reservation: release
183
+ */
184
+ async release(reservationId) {
185
+ const result = await this.supervisor.authorized((token) => this.transport.releaseReservation(reservationId, token));
186
+ this.reservedAmounts.delete(reservationId);
187
+ return result;
188
+ }
189
+ /**
190
+ * Directly loads a snapshot into the local mirror (e.g. for offline seed testing).
191
+ */
192
+ loadSnapshot(snapshot) {
193
+ return this.mirror.loadSnapshot(snapshot);
194
+ }
195
+ /**
196
+ * Returns current internal state and mirror revision.
197
+ */
198
+ state() {
199
+ const state = !this.active
200
+ ? 'COLD'
201
+ : this.isOnline
202
+ ? 'LIVE'
203
+ : this.mirror.isEmpty()
204
+ ? 'COLD_OFFLINE'
205
+ : 'DISCONNECTED';
206
+ return {
207
+ state,
208
+ snapshot_revision: this.mirror.getRevision(),
209
+ last_sync_at: this.mirror.getLastUpdatedAt() || undefined,
210
+ };
211
+ }
212
+ /**
213
+ * Switches the connection state by hand, for fault injection and tests. `false` closes the push
214
+ * stream and suspends reconnection until `setOnline(true)`, which sets LIVE at once and resumes
215
+ * supervision; on a started client the supervisor then corrects the state.
216
+ */
217
+ setOnline(online) {
218
+ this.active = true;
219
+ this.applyOnline(online);
220
+ if (online) {
221
+ this.supervisor.resume();
222
+ }
223
+ else {
224
+ this.supervisor.pause();
225
+ }
226
+ }
227
+ /**
228
+ * Sets the machine token by hand (for tests without `start()`); a decodable JWT also sets the
229
+ * issuer and application `verifyToken()` expects.
230
+ */
231
+ setM2MToken(token) {
232
+ this.supervisor.injectToken(token);
233
+ this.learnExpectations(token);
234
+ }
235
+ /**
236
+ * Directly preloads a public key into the token verifier cache (for testing without network).
237
+ */
238
+ setPublicKey(kid, pem) {
239
+ this.tokenVerifier.setPublicKey(kid, pem);
240
+ }
241
+ getUsageBuffer() {
242
+ return this.usageBuffer;
243
+ }
244
+ getTransport() {
245
+ return this.transport;
246
+ }
247
+ applyOnline(online) {
248
+ this.isOnline = online;
249
+ }
250
+ learnExpectations(token) {
251
+ const expectations = (0, token_verifier_1.expectationsFromMachineToken)(token);
252
+ if (expectations) {
253
+ this.expectations = expectations;
254
+ }
255
+ }
256
+ async refillOrganization(organizationId) {
257
+ try {
258
+ const snapshot = await this.supervisor.authorized((token) => this.transport.fetchSnapshot(token, organizationId));
259
+ this.mirror.mergeSnapshot(snapshot);
260
+ }
261
+ catch {
262
+ // The authoritative ruling stands; the next full sync fills the mirror.
263
+ }
264
+ }
265
+ }
266
+ exports.ShipRevClient = ShipRevClient;
267
+ //# sourceMappingURL=shiprev-client.js.map
@@ -0,0 +1,16 @@
1
+ import { ShipRevClient } from './client/shiprev-client';
2
+ import { ShipRevClientOptions } from './types';
3
+ export { jwkToPem, TokenVerifier } from './auth/token-verifier';
4
+ export { ShipRevClient } from './client/shiprev-client';
5
+ export { HostAdapterBacklog } from './metering/backlog/host-adapter-backlog';
6
+ export { JournalBacklog } from './metering/backlog/journal-backlog';
7
+ export { MemoryBacklog } from './metering/backlog/memory-backlog';
8
+ export { UsageBuffer } from './metering/usage-buffer';
9
+ export { EntitlementMirror, MAX_SUPPORTED_SCHEMA_VERSION } from './mirror/entitlement-mirror';
10
+ export { Adjudicator } from './ruling/adjudicator';
11
+ export { HttpTransport } from './transport/http.transport';
12
+ export * from './types';
13
+ /**
14
+ * Creates and initializes a ShipRev Server SDK client instance for Node.js services.
15
+ */
16
+ export declare function createClient(options: ShipRevClientOptions): ShipRevClient;
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.HttpTransport = exports.Adjudicator = exports.MAX_SUPPORTED_SCHEMA_VERSION = exports.EntitlementMirror = exports.UsageBuffer = exports.MemoryBacklog = exports.JournalBacklog = exports.HostAdapterBacklog = exports.ShipRevClient = exports.TokenVerifier = exports.jwkToPem = void 0;
18
+ exports.createClient = createClient;
19
+ const shiprev_client_1 = require("./client/shiprev-client");
20
+ var token_verifier_1 = require("./auth/token-verifier");
21
+ Object.defineProperty(exports, "jwkToPem", { enumerable: true, get: function () { return token_verifier_1.jwkToPem; } });
22
+ Object.defineProperty(exports, "TokenVerifier", { enumerable: true, get: function () { return token_verifier_1.TokenVerifier; } });
23
+ var shiprev_client_2 = require("./client/shiprev-client");
24
+ Object.defineProperty(exports, "ShipRevClient", { enumerable: true, get: function () { return shiprev_client_2.ShipRevClient; } });
25
+ var host_adapter_backlog_1 = require("./metering/backlog/host-adapter-backlog");
26
+ Object.defineProperty(exports, "HostAdapterBacklog", { enumerable: true, get: function () { return host_adapter_backlog_1.HostAdapterBacklog; } });
27
+ var journal_backlog_1 = require("./metering/backlog/journal-backlog");
28
+ Object.defineProperty(exports, "JournalBacklog", { enumerable: true, get: function () { return journal_backlog_1.JournalBacklog; } });
29
+ var memory_backlog_1 = require("./metering/backlog/memory-backlog");
30
+ Object.defineProperty(exports, "MemoryBacklog", { enumerable: true, get: function () { return memory_backlog_1.MemoryBacklog; } });
31
+ var usage_buffer_1 = require("./metering/usage-buffer");
32
+ Object.defineProperty(exports, "UsageBuffer", { enumerable: true, get: function () { return usage_buffer_1.UsageBuffer; } });
33
+ var entitlement_mirror_1 = require("./mirror/entitlement-mirror");
34
+ Object.defineProperty(exports, "EntitlementMirror", { enumerable: true, get: function () { return entitlement_mirror_1.EntitlementMirror; } });
35
+ Object.defineProperty(exports, "MAX_SUPPORTED_SCHEMA_VERSION", { enumerable: true, get: function () { return entitlement_mirror_1.MAX_SUPPORTED_SCHEMA_VERSION; } });
36
+ var adjudicator_1 = require("./ruling/adjudicator");
37
+ Object.defineProperty(exports, "Adjudicator", { enumerable: true, get: function () { return adjudicator_1.Adjudicator; } });
38
+ var http_transport_1 = require("./transport/http.transport");
39
+ Object.defineProperty(exports, "HttpTransport", { enumerable: true, get: function () { return http_transport_1.HttpTransport; } });
40
+ __exportStar(require("./types"), exports);
41
+ /**
42
+ * Creates and initializes a ShipRev Server SDK client instance for Node.js services.
43
+ */
44
+ function createClient(options) {
45
+ return new shiprev_client_1.ShipRevClient(options);
46
+ }
47
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ import { BacklogAdapter, BacklogItem, UsageEventItem } from '../../types';
2
+ export declare class HostAdapterBacklog implements BacklogAdapter {
3
+ private readonly adapter;
4
+ constructor(customAdapter?: BacklogAdapter);
5
+ enqueue(event: UsageEventItem): Promise<void>;
6
+ peek(limit: number): Promise<BacklogItem[]>;
7
+ ack(ids: string[]): Promise<void>;
8
+ nack(ids: string[]): Promise<void>;
9
+ size(): Promise<number>;
10
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HostAdapterBacklog = void 0;
4
+ const memory_backlog_1 = require("./memory-backlog");
5
+ class HostAdapterBacklog {
6
+ adapter;
7
+ constructor(customAdapter) {
8
+ this.adapter = customAdapter ?? new memory_backlog_1.MemoryBacklog();
9
+ }
10
+ async enqueue(event) {
11
+ return this.adapter.enqueue(event);
12
+ }
13
+ async peek(limit) {
14
+ return this.adapter.peek(limit);
15
+ }
16
+ async ack(ids) {
17
+ return this.adapter.ack(ids);
18
+ }
19
+ async nack(ids) {
20
+ return this.adapter.nack(ids);
21
+ }
22
+ async size() {
23
+ return this.adapter.size();
24
+ }
25
+ }
26
+ exports.HostAdapterBacklog = HostAdapterBacklog;
27
+ //# sourceMappingURL=host-adapter-backlog.js.map
@@ -0,0 +1,14 @@
1
+ import { BacklogAdapter, BacklogItem, UsageEventItem } from '../../types';
2
+ export declare class JournalBacklog implements BacklogAdapter {
3
+ private readonly filePath;
4
+ private queue;
5
+ private isCompacting;
6
+ constructor(filePath?: string);
7
+ private recoverSync;
8
+ enqueue(event: UsageEventItem): Promise<void>;
9
+ peek(limit: number): Promise<BacklogItem[]>;
10
+ ack(ids: string[]): Promise<void>;
11
+ nack(ids: string[]): Promise<void>;
12
+ size(): Promise<number>;
13
+ private compact;
14
+ }
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.JournalBacklog = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ class JournalBacklog {
40
+ filePath;
41
+ queue = [];
42
+ isCompacting = false;
43
+ constructor(filePath) {
44
+ this.filePath = filePath || path.resolve(process.cwd(), '.shiprev-usage-journal.log');
45
+ this.recoverSync();
46
+ }
47
+ recoverSync() {
48
+ try {
49
+ if (!fs.existsSync(this.filePath)) {
50
+ return;
51
+ }
52
+ const content = fs.readFileSync(this.filePath, 'utf-8');
53
+ const lines = content.split('\n').filter((l) => l.trim().length > 0);
54
+ for (const line of lines) {
55
+ try {
56
+ const parsed = JSON.parse(line);
57
+ if (parsed.event && parsed.id) {
58
+ this.queue.push({
59
+ id: parsed.id,
60
+ event: parsed.event,
61
+ enqueued_at: parsed.enqueued_at ?? Date.now(),
62
+ attempts: parsed.attempts ?? 0,
63
+ });
64
+ }
65
+ }
66
+ catch {
67
+ // Ignore corrupt line to protect rest of journal
68
+ }
69
+ }
70
+ }
71
+ catch {
72
+ // Ignore recovery read error
73
+ }
74
+ }
75
+ async enqueue(event) {
76
+ const item = {
77
+ id: event.usage_event_id,
78
+ event,
79
+ enqueued_at: Date.now(),
80
+ attempts: 0,
81
+ };
82
+ this.queue.push(item);
83
+ // Atomically append line to journal file
84
+ try {
85
+ const line = JSON.stringify(item) + '\n';
86
+ await fs.promises.appendFile(this.filePath, line, 'utf-8');
87
+ }
88
+ catch {
89
+ // In-memory queue remains even if disk append encounters temporary issue
90
+ }
91
+ }
92
+ async peek(limit) {
93
+ return this.queue.slice(0, limit);
94
+ }
95
+ async ack(ids) {
96
+ const idSet = new Set(ids);
97
+ this.queue = this.queue.filter((item) => !idSet.has(item.id));
98
+ await this.compact();
99
+ }
100
+ async nack(ids) {
101
+ const idSet = new Set(ids);
102
+ for (const item of this.queue) {
103
+ if (idSet.has(item.id)) {
104
+ item.attempts += 1;
105
+ }
106
+ }
107
+ }
108
+ async size() {
109
+ return this.queue.length;
110
+ }
111
+ async compact() {
112
+ if (this.isCompacting)
113
+ return;
114
+ this.isCompacting = true;
115
+ try {
116
+ if (this.queue.length === 0) {
117
+ if (fs.existsSync(this.filePath)) {
118
+ await fs.promises.writeFile(this.filePath, '', 'utf-8');
119
+ }
120
+ }
121
+ else {
122
+ const lines = this.queue.map((item) => JSON.stringify(item)).join('\n') + '\n';
123
+ await fs.promises.writeFile(this.filePath, lines, 'utf-8');
124
+ }
125
+ }
126
+ catch {
127
+ // Best-effort compaction
128
+ }
129
+ finally {
130
+ this.isCompacting = false;
131
+ }
132
+ }
133
+ }
134
+ exports.JournalBacklog = JournalBacklog;
135
+ //# sourceMappingURL=journal-backlog.js.map
@@ -0,0 +1,9 @@
1
+ import { BacklogAdapter, BacklogItem, UsageEventItem } from '../../types';
2
+ export declare class MemoryBacklog implements BacklogAdapter {
3
+ private queue;
4
+ enqueue(event: UsageEventItem): Promise<void>;
5
+ peek(limit: number): Promise<BacklogItem[]>;
6
+ ack(ids: string[]): Promise<void>;
7
+ nack(ids: string[]): Promise<void>;
8
+ size(): Promise<number>;
9
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryBacklog = void 0;
4
+ class MemoryBacklog {
5
+ queue = [];
6
+ async enqueue(event) {
7
+ this.queue.push({
8
+ id: event.usage_event_id,
9
+ event,
10
+ enqueued_at: Date.now(),
11
+ attempts: 0,
12
+ });
13
+ }
14
+ async peek(limit) {
15
+ return this.queue.slice(0, limit);
16
+ }
17
+ async ack(ids) {
18
+ const idSet = new Set(ids);
19
+ this.queue = this.queue.filter((item) => !idSet.has(item.id));
20
+ }
21
+ async nack(ids) {
22
+ const idSet = new Set(ids);
23
+ for (const item of this.queue) {
24
+ if (idSet.has(item.id)) {
25
+ item.attempts += 1;
26
+ }
27
+ }
28
+ }
29
+ async size() {
30
+ return this.queue.length;
31
+ }
32
+ }
33
+ exports.MemoryBacklog = MemoryBacklog;
34
+ //# sourceMappingURL=memory-backlog.js.map
@@ -0,0 +1,59 @@
1
+ import { HttpTransport } from '../transport/http.transport';
2
+ import { BacklogConfig, RejectedEvent, UsageBufferConfig, UsageEventItem, UsageEventsResponse } from '../types';
3
+ export interface UsageBufferOptions {
4
+ transport: HttpTransport;
5
+ getToken: () => string | null;
6
+ isOnline: () => boolean;
7
+ /**
8
+ * Delivers a batch; defaults to `transport.reportUsageEvents(events, getToken())`. The client
9
+ * passes a sender that renews the token once on a 401.
10
+ */
11
+ send?: (events: UsageEventItem[]) => Promise<UsageEventsResponse>;
12
+ bufferConfig?: UsageBufferConfig;
13
+ backlogConfig?: BacklogConfig;
14
+ onFlushFailure?: (events: UsageEventItem[], error: Error) => void;
15
+ onRejected?: (rejected: RejectedEvent[]) => void;
16
+ }
17
+ export declare class UsageBuffer {
18
+ private readonly transport;
19
+ private readonly getToken;
20
+ private readonly isOnline;
21
+ private readonly send?;
22
+ private readonly batchSize;
23
+ private readonly flushIntervalMs;
24
+ private readonly backlog;
25
+ private readonly onFlushFailure?;
26
+ private readonly onRejected?;
27
+ private buffer;
28
+ private timer;
29
+ /** The pass in flight, if any; at most one runs at a time. */
30
+ private inFlight;
31
+ private isStopped;
32
+ constructor(options: UsageBufferOptions);
33
+ /**
34
+ * Appends an event to the buffer. If buffer reaches batchSize, triggers flush.
35
+ */
36
+ reportUsage(event: UsageEventItem): Promise<void>;
37
+ /**
38
+ * Delivers every usage event reported before this call: the backlog first, in batches, then the
39
+ * in-memory buffer. A flush already in flight is awaited first, then this call runs its own pass.
40
+ * Resolves once each of those events was accepted, refused (`onRejected`), or moved to the
41
+ * backlog because it could not be delivered (offline, no token, or a failed request).
42
+ */
43
+ flush(): Promise<void>;
44
+ private runPass;
45
+ /**
46
+ * One pass. With `drainBacklog` the backlog is sent in batches until it is empty or a batch
47
+ * fails; without it (timer ticks) at most one backlog batch is sent.
48
+ */
49
+ private pass;
50
+ private flushBacklog;
51
+ private deliver;
52
+ private startTimer;
53
+ /**
54
+ * Gracefully stops the buffer flusher and performs final drain.
55
+ */
56
+ stop(): Promise<void>;
57
+ getBufferSize(): number;
58
+ getBacklogSize(): Promise<number>;
59
+ }