td-feature-node 0.0.0-stage → 0.0.2

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 (49) hide show
  1. package/README.md +174 -2
  2. package/dist/client.d.ts +29 -0
  3. package/dist/client.js +206 -0
  4. package/dist/common/coercion.d.ts +4 -0
  5. package/dist/common/coercion.js +32 -0
  6. package/dist/common/config.d.ts +8 -0
  7. package/dist/common/config.js +172 -0
  8. package/dist/common/json.d.ts +7 -0
  9. package/dist/common/json.js +59 -0
  10. package/dist/core/config-snapshot.d.ts +15 -0
  11. package/dist/core/config-snapshot.js +58 -0
  12. package/dist/core/http-transport.d.ts +13 -0
  13. package/dist/core/http-transport.js +181 -0
  14. package/dist/core/request-operation.d.ts +16 -0
  15. package/dist/core/request-operation.js +73 -0
  16. package/dist/core/request-scheduler.d.ts +16 -0
  17. package/dist/core/request-scheduler.js +111 -0
  18. package/dist/core/request.d.ts +10 -0
  19. package/dist/core/request.js +72 -0
  20. package/dist/core/response.d.ts +5 -0
  21. package/dist/core/response.js +198 -0
  22. package/dist/core/result-cache.d.ts +14 -0
  23. package/dist/core/result-cache.js +70 -0
  24. package/dist/core/retry.d.ts +6 -0
  25. package/dist/core/retry.js +49 -0
  26. package/dist/core/stream-decoder.d.ts +10 -0
  27. package/dist/core/stream-decoder.js +29 -0
  28. package/dist/errors.d.ts +17 -0
  29. package/dist/errors.js +16 -0
  30. package/dist/evaluation.d.ts +10 -0
  31. package/dist/evaluation.js +9 -0
  32. package/dist/feature.d.ts +31 -0
  33. package/dist/feature.js +57 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +11 -0
  36. package/dist/report/event.d.ts +3 -0
  37. package/dist/report/event.js +86 -0
  38. package/dist/report/expose-reporter.d.ts +40 -0
  39. package/dist/report/expose-reporter.js +285 -0
  40. package/dist/report/exposure-dedup.d.ts +16 -0
  41. package/dist/report/exposure-dedup.js +45 -0
  42. package/dist/report/owned-collection-adapter.d.ts +22 -0
  43. package/dist/report/owned-collection-adapter.js +243 -0
  44. package/dist/report/thinking-data-adapter.d.ts +11 -0
  45. package/dist/report/thinking-data-adapter.js +54 -0
  46. package/dist/types.d.ts +134 -0
  47. package/dist/types.js +9 -0
  48. package/npm-shrinkwrap.json +665 -0
  49. package/package.json +46 -4
@@ -0,0 +1,40 @@
1
+ import type { ExposureAdapter, ExposureProperties, ResolvedClientOptions } from '../types';
2
+ import { Feature } from '../feature';
3
+ /** One asynchronous worker, a bounded FIFO, and one pending flush control slot. */
4
+ export declare class ExposeReporter {
5
+ private readonly adapter;
6
+ private readonly ownsAdapter;
7
+ private readonly config;
8
+ private readonly dedup;
9
+ private head;
10
+ private tail;
11
+ private queueSize;
12
+ private active;
13
+ private scheduled;
14
+ private remainingInTurn;
15
+ private barrier;
16
+ private activeBarrier;
17
+ private sequence;
18
+ private completed;
19
+ private closing;
20
+ private stopped;
21
+ private finalizing;
22
+ private adapterClosed;
23
+ private closePromise;
24
+ private closeTimer;
25
+ private closeDeadline;
26
+ private finishClose;
27
+ constructor(adapter: ExposureAdapter, ownsAdapter: boolean, config: Readonly<ResolvedClientOptions>);
28
+ report(feature: Feature | null | undefined, custom?: ExposureProperties | null): boolean;
29
+ private schedule;
30
+ private pump;
31
+ private unmarkTask;
32
+ private completeTask;
33
+ flush(): Promise<void>;
34
+ private settleBarrier;
35
+ private completeBarrier;
36
+ close(): Promise<void>;
37
+ private closeAdapter;
38
+ private finalize;
39
+ private stop;
40
+ }
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExposeReporter = void 0;
4
+ const errors_1 = require("../errors");
5
+ const exposure_dedup_1 = require("./exposure-dedup");
6
+ const event_1 = require("./event");
7
+ const request_operation_1 = require("../core/request-operation");
8
+ /** One asynchronous worker, a bounded FIFO, and one pending flush control slot. */
9
+ class ExposeReporter {
10
+ constructor(adapter, ownsAdapter, config) {
11
+ this.adapter = adapter;
12
+ this.ownsAdapter = ownsAdapter;
13
+ this.config = config;
14
+ this.head = null;
15
+ this.tail = null;
16
+ this.queueSize = 0;
17
+ this.active = null;
18
+ this.remainingInTurn = 0;
19
+ this.barrier = null;
20
+ this.activeBarrier = null;
21
+ this.sequence = 0;
22
+ this.completed = 0;
23
+ this.closing = false;
24
+ this.stopped = false;
25
+ this.finalizing = false;
26
+ this.adapterClosed = false;
27
+ this.closePromise = null;
28
+ this.closeDeadline = 0;
29
+ this.finishClose = function () { };
30
+ this.dedup = new exposure_dedup_1.ExposureDedup(config.exposureDedupMaxSize, config.exposureDedupExpireSeconds);
31
+ }
32
+ report(feature, custom) {
33
+ if (!feature || !feature.experiment)
34
+ return false;
35
+ const source = feature.experiment;
36
+ const bucket = source.bucketId;
37
+ // Capture metadata before copying caller properties, which may have getters.
38
+ const info = Object.assign({}, source, {
39
+ bucketId: bucket ? Object.assign(Object.create(null), bucket) : null,
40
+ });
41
+ if (!info.experimentId || !info.experimentGroupId)
42
+ return false;
43
+ if (this.closing || this.stopped) {
44
+ return false;
45
+ }
46
+ const subject = (0, event_1.exposureSubject)(info, feature.accountId, feature.distinctId);
47
+ const key = (0, exposure_dedup_1.exposureKey)(subject, info.experimentId, info.experimentGroupId);
48
+ const mark = this.dedup.mark(key);
49
+ if (!mark) {
50
+ return false;
51
+ }
52
+ let accepted = false;
53
+ try {
54
+ if (this.queueSize >= this.config.exposureQueueCapacity) {
55
+ return false;
56
+ }
57
+ const event = (0, event_1.exposureEvent)(feature.accountId, feature.distinctId, info, custom);
58
+ // Caller property getters can re-enter exposure() or close() while copying.
59
+ if (this.closing || this.stopped || this.queueSize >= this.config.exposureQueueCapacity) {
60
+ return false;
61
+ }
62
+ const task = {
63
+ event: event,
64
+ key: key,
65
+ mark: mark,
66
+ sequence: ++this.sequence,
67
+ next: null,
68
+ };
69
+ if (this.tail)
70
+ this.tail.next = task;
71
+ else
72
+ this.head = task;
73
+ this.tail = task;
74
+ this.queueSize++;
75
+ accepted = true;
76
+ this.schedule();
77
+ return true;
78
+ }
79
+ finally {
80
+ if (!accepted)
81
+ this.dedup.unmark(key, mark);
82
+ }
83
+ }
84
+ schedule() {
85
+ if (this.stopped || this.scheduled || this.active || this.activeBarrier)
86
+ return;
87
+ this.scheduled = setImmediate(() => {
88
+ this.scheduled = undefined;
89
+ this.remainingInTurn = 64;
90
+ this.pump();
91
+ });
92
+ }
93
+ pump() {
94
+ if (this.stopped || this.active || this.activeBarrier)
95
+ return;
96
+ if (this.barrier && this.completed >= this.barrier.target) {
97
+ const barrier = this.barrier;
98
+ this.barrier = null;
99
+ this.activeBarrier = barrier;
100
+ if ((0, request_operation_1.monotonicNow)() >= barrier.deadline) {
101
+ this.completeBarrier(barrier);
102
+ return;
103
+ }
104
+ Promise.resolve()
105
+ .then(() => this.adapter.flush())
106
+ .then(() => this.completeBarrier(barrier), (error) => this.completeBarrier(barrier, error));
107
+ return;
108
+ }
109
+ const task = this.head;
110
+ if (!task) {
111
+ if (this.closing)
112
+ this.finalize();
113
+ return;
114
+ }
115
+ this.head = task.next;
116
+ if (!this.head)
117
+ this.tail = null;
118
+ task.next = null;
119
+ this.queueSize--;
120
+ this.active = task;
121
+ Promise.resolve()
122
+ .then(() => this.adapter.track(task.event, () => this.unmarkTask(task)))
123
+ .then((accepted) => {
124
+ if (accepted === false)
125
+ throw new Error('Exposure adapter rejected submission');
126
+ })
127
+ .then(() => this.completeTask(task), () => {
128
+ this.unmarkTask(task);
129
+ this.completeTask(task);
130
+ });
131
+ }
132
+ unmarkTask(task) {
133
+ this.dedup.unmark(task.key, task.mark);
134
+ }
135
+ completeTask(task) {
136
+ if (this.active !== task)
137
+ return;
138
+ this.active = null;
139
+ this.completed = task.sequence;
140
+ // Drain a bounded group per turn instead of paying one event-loop turn per
141
+ // event. Yield after 64 so a full queue cannot monopolize microtasks.
142
+ if (--this.remainingInTurn > 0)
143
+ this.pump();
144
+ else
145
+ this.schedule();
146
+ }
147
+ flush() {
148
+ if (this.closing || this.stopped)
149
+ return this.closePromise || Promise.resolve();
150
+ const current = this.barrier || this.activeBarrier;
151
+ if (current) {
152
+ if (this.sequence <= current.target)
153
+ return current.promise;
154
+ return Promise.reject(new errors_1.FeatureError('Another exposure flush barrier is pending', 'QUEUE_FULL'));
155
+ }
156
+ let resolve, reject;
157
+ const promise = new Promise((yes, no) => {
158
+ resolve = yes;
159
+ reject = no;
160
+ });
161
+ const barrier = {
162
+ target: this.sequence,
163
+ promise: promise,
164
+ resolve: resolve,
165
+ reject: reject,
166
+ settled: false,
167
+ deadline: (0, request_operation_1.monotonicNow)() + this.config.exposureFlushTimeoutMillis,
168
+ timer: setTimeout(() => {
169
+ this.settleBarrier(barrier, new errors_1.FeatureError('Exposure flush exceeded its wait budget', 'TIMEOUT'));
170
+ if (this.barrier === barrier)
171
+ this.barrier = null;
172
+ this.schedule();
173
+ }, this.config.exposureFlushTimeoutMillis),
174
+ };
175
+ this.barrier = barrier;
176
+ this.schedule();
177
+ return promise;
178
+ }
179
+ settleBarrier(barrier, error) {
180
+ if (barrier.settled)
181
+ return;
182
+ barrier.settled = true;
183
+ clearTimeout(barrier.timer);
184
+ if (error) {
185
+ barrier.reject(error instanceof errors_1.FeatureError
186
+ ? error
187
+ : new errors_1.FeatureError('Collection flush failed', 'EXPOSURE_ERROR', { cause: error }));
188
+ }
189
+ else
190
+ barrier.resolve();
191
+ }
192
+ completeBarrier(barrier, error) {
193
+ if (!error && (0, request_operation_1.monotonicNow)() >= barrier.deadline)
194
+ error = new errors_1.FeatureError('Exposure flush exceeded its wait budget', 'TIMEOUT');
195
+ this.settleBarrier(barrier, error);
196
+ if (this.activeBarrier === barrier)
197
+ this.activeBarrier = null;
198
+ this.schedule();
199
+ }
200
+ close() {
201
+ if (this.closePromise)
202
+ return this.closePromise;
203
+ this.closing = true;
204
+ this.closeDeadline = (0, request_operation_1.monotonicNow)() + this.config.exposureFlushTimeoutMillis;
205
+ this.closePromise = new Promise((resolve) => {
206
+ this.finishClose = resolve;
207
+ });
208
+ this.closeTimer = setTimeout(() => this.stop(true), this.config.exposureFlushTimeoutMillis);
209
+ this.schedule();
210
+ return this.closePromise;
211
+ }
212
+ closeAdapter() {
213
+ if (!this.ownsAdapter || this.adapterClosed)
214
+ return;
215
+ this.adapterClosed = true;
216
+ if (this.adapter.close)
217
+ return this.adapter.close();
218
+ }
219
+ finalize() {
220
+ if (this.finalizing || this.stopped)
221
+ return;
222
+ if ((0, request_operation_1.monotonicNow)() >= this.closeDeadline) {
223
+ this.stop(true);
224
+ return;
225
+ }
226
+ this.finalizing = true;
227
+ Promise.resolve()
228
+ .then(() => {
229
+ if (this.ownsAdapter && !this.stopped)
230
+ return this.adapter.flush();
231
+ })
232
+ .then(() => {
233
+ if (this.stopped)
234
+ return;
235
+ if ((0, request_operation_1.monotonicNow)() >= this.closeDeadline) {
236
+ this.stop(true);
237
+ return;
238
+ }
239
+ return this.closeAdapter();
240
+ })
241
+ .then(() => this.stop((0, request_operation_1.monotonicNow)() >= this.closeDeadline), () => {
242
+ this.stop(true);
243
+ });
244
+ }
245
+ stop(timedOut) {
246
+ if (this.stopped)
247
+ return;
248
+ this.stopped = true;
249
+ if (this.closeTimer)
250
+ clearTimeout(this.closeTimer);
251
+ if (this.scheduled) {
252
+ clearImmediate(this.scheduled);
253
+ this.scheduled = undefined;
254
+ }
255
+ this.head = null;
256
+ this.tail = null;
257
+ this.queueSize = 0;
258
+ this.active = null;
259
+ const closed = new errors_1.FeatureError('Client closed before exposure flush completed', 'CLIENT_CLOSED');
260
+ if (this.barrier) {
261
+ this.settleBarrier(this.barrier, closed);
262
+ this.barrier = null;
263
+ }
264
+ if (this.activeBarrier) {
265
+ this.settleBarrier(this.activeBarrier, closed);
266
+ this.activeBarrier = null;
267
+ }
268
+ if (timedOut) {
269
+ // Only SDK-owned adapters may be closed or aborted here.
270
+ try {
271
+ Promise.resolve(this.closeAdapter()).then(function () { }, function () { });
272
+ }
273
+ catch (_) { }
274
+ const owned = this.adapter;
275
+ try {
276
+ if (this.ownsAdapter && owned.abort)
277
+ owned.abort();
278
+ }
279
+ catch (_) { }
280
+ }
281
+ this.dedup.clear();
282
+ this.finishClose();
283
+ }
284
+ }
285
+ exports.ExposeReporter = ExposeReporter;
@@ -0,0 +1,16 @@
1
+ export declare function exposureKey(subject: string, experiment: string, group: string): string;
2
+ export interface ExposureMark {
3
+ readonly expires: number;
4
+ }
5
+ /** Bounded, non-sliding TTL. Tokens keep an old failure from deleting a newer reservation. */
6
+ export declare class ExposureDedup {
7
+ private readonly capacity;
8
+ private readonly seconds;
9
+ private readonly now;
10
+ private readonly entries;
11
+ constructor(capacity: number, seconds: number, now?: () => number);
12
+ mark(key: string): ExposureMark | null;
13
+ unmark(key: string, mark: ExposureMark): void;
14
+ clear(): void;
15
+ get size(): number;
16
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExposureDedup = void 0;
4
+ exports.exposureKey = exposureKey;
5
+ const crypto_1 = require("crypto");
6
+ const request_operation_1 = require("../core/request-operation");
7
+ function exposureKey(subject, experiment, group) {
8
+ return (0, crypto_1.createHash)('sha256')
9
+ .update(JSON.stringify([subject, experiment, group]), 'utf8')
10
+ .digest('hex');
11
+ }
12
+ /** Bounded, non-sliding TTL. Tokens keep an old failure from deleting a newer reservation. */
13
+ class ExposureDedup {
14
+ constructor(capacity, seconds, now = request_operation_1.monotonicNow) {
15
+ this.capacity = capacity;
16
+ this.seconds = seconds;
17
+ this.now = now;
18
+ this.entries = new Map();
19
+ }
20
+ mark(key) {
21
+ const existing = this.entries.get(key);
22
+ if (existing && existing.expires > this.now()) {
23
+ this.entries.delete(key);
24
+ this.entries.set(key, existing);
25
+ return null;
26
+ }
27
+ this.entries.delete(key);
28
+ while (this.entries.size >= this.capacity)
29
+ this.entries.delete(this.entries.keys().next().value);
30
+ const mark = { expires: this.now() + this.seconds * 1000 };
31
+ this.entries.set(key, mark);
32
+ return mark;
33
+ }
34
+ unmark(key, mark) {
35
+ if (this.entries.get(key) === mark)
36
+ this.entries.delete(key);
37
+ }
38
+ clear() {
39
+ this.entries.clear();
40
+ }
41
+ get size() {
42
+ return this.entries.size;
43
+ }
44
+ }
45
+ exports.ExposureDedup = ExposureDedup;
@@ -0,0 +1,22 @@
1
+ import type { ExposureAdapter, ExposureEvent, ResolvedClientOptions } from '../types';
2
+ /** Official npm Batch collector with instance-owned, cancellable transport. */
3
+ export declare class OwnedCollectionAdapter implements ExposureAdapter {
4
+ private readonly config;
5
+ private analytics;
6
+ private buffered;
7
+ private sending;
8
+ private flushTimer;
9
+ private readonly requests;
10
+ private readonly agent;
11
+ private closed;
12
+ constructor(config: Readonly<ResolvedClientOptions>);
13
+ track(event: ExposureEvent, onFailure?: (error: Error) => void): void | Promise<void>;
14
+ private newBatch;
15
+ private startBatch;
16
+ private completeBatch;
17
+ private request;
18
+ private failedRequest;
19
+ flush(): Promise<void>;
20
+ close(): void;
21
+ abort(): void;
22
+ }
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OwnedCollectionAdapter = void 0;
4
+ const http = require("http");
5
+ const https = require("https");
6
+ const events_1 = require("events");
7
+ /** Official npm Batch collector with instance-owned, cancellable transport. */
8
+ class OwnedCollectionAdapter {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.analytics = null;
12
+ this.buffered = null;
13
+ this.sending = null;
14
+ this.requests = new Map();
15
+ this.closed = false;
16
+ this.agent = /^https:/.test(config.serverUrl)
17
+ ? new https.Agent({
18
+ keepAlive: true,
19
+ maxSockets: 1,
20
+ ca: config.tlsCa,
21
+ })
22
+ : new http.Agent({ keepAlive: true, maxSockets: 1 });
23
+ }
24
+ track(event, onFailure) {
25
+ if (this.closed)
26
+ throw new Error('Collection adapter is closed');
27
+ // Keep one batch in flight, including compression and HTTP. The reporter
28
+ // retains subsequent events in its bounded queue while this promise waits.
29
+ if (this.sending)
30
+ return this.sending.promise.then(() => this.track(event, onFailure));
31
+ if (!this.analytics) {
32
+ const module = require('thinkingdata-node');
33
+ const analytics = module.initWithBatchMode(this.config.appId, this.config.serverUrl, {
34
+ batchSize: this.config.exposureBatchSize,
35
+ compress: true,
36
+ });
37
+ // The pinned official 1.5.0 Batch consumer exposes its per-instance server
38
+ // config. Adapt only this instance; never patch the package or global HTTP.
39
+ const transport = analytics.consumer && analytics.consumer.config;
40
+ if (!transport || !transport.requestLib || typeof transport.requestLib.request !== 'function')
41
+ throw new Error('Unsupported official thinkingdata-node Batch transport');
42
+ transport.hostname = new URL(this.config.serverUrl).hostname.replace(/^\[|\]$/g, '');
43
+ transport.requestLib = { request: this.request.bind(this) };
44
+ this.analytics = analytics;
45
+ }
46
+ const batch = this.buffered || (this.buffered = this.newBatch());
47
+ let calling = true;
48
+ let localError;
49
+ const params = {
50
+ event: event.event,
51
+ properties: event.properties,
52
+ callback: (error) => {
53
+ if (calling)
54
+ localError = error || undefined;
55
+ else
56
+ this.completeBatch(batch, error || undefined);
57
+ },
58
+ };
59
+ if (event.accountId !== null)
60
+ params.accountId = event.accountId;
61
+ if (event.distinctId !== null)
62
+ params.distinctId = event.distinctId;
63
+ try {
64
+ this.analytics.track(params);
65
+ }
66
+ catch (error) {
67
+ if (!batch.failures.length)
68
+ this.buffered = null;
69
+ throw error;
70
+ }
71
+ calling = false;
72
+ if (localError) {
73
+ if (!batch.failures.length)
74
+ this.buffered = null;
75
+ throw localError;
76
+ }
77
+ batch.failures.push(onFailure);
78
+ if (batch.failures.length >= this.config.exposureBatchSize) {
79
+ // The threshold event has already triggered the native Batch flush.
80
+ this.startBatch(batch);
81
+ return batch.promise;
82
+ }
83
+ if (!this.flushTimer) {
84
+ this.flushTimer = setTimeout(() => {
85
+ this.flushTimer = undefined;
86
+ this.flush();
87
+ }, this.config.exposureFlushIntervalMillis);
88
+ this.flushTimer.unref();
89
+ }
90
+ }
91
+ newBatch() {
92
+ let resolve;
93
+ const promise = new Promise((done) => {
94
+ resolve = done;
95
+ });
96
+ return { failures: [], promise, resolve, settled: false };
97
+ }
98
+ startBatch(batch) {
99
+ if (this.flushTimer)
100
+ clearTimeout(this.flushTimer);
101
+ this.flushTimer = undefined;
102
+ this.buffered = null;
103
+ this.sending = batch;
104
+ }
105
+ completeBatch(batch, error) {
106
+ if (batch.settled)
107
+ return;
108
+ batch.settled = true;
109
+ if (this.buffered === batch)
110
+ this.buffered = null;
111
+ if (this.sending === batch)
112
+ this.sending = null;
113
+ if (error) {
114
+ for (const notify of batch.failures)
115
+ if (notify)
116
+ notify(error);
117
+ }
118
+ batch.failures.length = 0;
119
+ // Individual submission failures roll back dedup, as for custom adapters;
120
+ // flush waits for settlement and does not assert successful ingestion.
121
+ batch.resolve();
122
+ }
123
+ request(options, callback) {
124
+ if (this.closed) {
125
+ // Upstream gzip is asynchronous and may finish after close. Never start late I/O.
126
+ return this.failedRequest(new Error('Collection adapter is closed'));
127
+ }
128
+ const native = /^https:/.test(this.config.serverUrl) ? https : http;
129
+ options.agent = this.agent;
130
+ let response;
131
+ let request;
132
+ try {
133
+ request = native.request(options, (incoming) => {
134
+ response = incoming;
135
+ let bytes = 0, ended = false;
136
+ incoming.on('data', (chunk) => {
137
+ bytes += chunk.length;
138
+ if (bytes > 1024 * 1024)
139
+ fail(new Error('Collection response exceeds 1 MiB'));
140
+ });
141
+ incoming.on('end', () => {
142
+ ended = true;
143
+ cleanup();
144
+ });
145
+ incoming.on('error', fail);
146
+ incoming.on('aborted', () => fail(new Error('Collection response aborted')));
147
+ incoming.on('close', () => {
148
+ if (!ended)
149
+ fail(new Error('Collection response ended prematurely'));
150
+ });
151
+ if (incoming.statusCode === 200)
152
+ callback(incoming);
153
+ else {
154
+ // The official consumer reports non-200 responses at headers. Drain
155
+ // their bounded body first so the next batch cannot queue behind it.
156
+ incoming.once('end', () => callback(incoming));
157
+ incoming.resume();
158
+ }
159
+ });
160
+ }
161
+ catch (error) {
162
+ // This call can run in upstream's gzip callback. Convert sync native
163
+ // errors into its error event instead of throwing out of that callback.
164
+ return this.failedRequest(error);
165
+ }
166
+ let failed = false;
167
+ const cleanup = () => {
168
+ clearTimeout(timer);
169
+ this.requests.delete(request);
170
+ };
171
+ const fail = (error) => {
172
+ if (failed)
173
+ return;
174
+ failed = true;
175
+ cleanup();
176
+ // The upstream Batch callback listens on ClientRequest.error.
177
+ request.emit('error', error);
178
+ request.abort();
179
+ if (response)
180
+ response.destroy();
181
+ };
182
+ const timer = setTimeout(() => fail(new Error('Collection request timed out')), this.config.exposureFlushTimeoutMillis);
183
+ this.requests.set(request, () => fail(new Error('Collection adapter closed')));
184
+ request.on('error', () => {
185
+ failed = true;
186
+ cleanup();
187
+ });
188
+ return request;
189
+ }
190
+ failedRequest(error) {
191
+ const dead = Object.assign(new events_1.EventEmitter(), {
192
+ write: function () {
193
+ return false;
194
+ },
195
+ end: function () { },
196
+ abort: function () { },
197
+ });
198
+ process.nextTick(() => dead.emit('error', error));
199
+ return dead;
200
+ }
201
+ flush() {
202
+ if (this.closed)
203
+ return Promise.resolve();
204
+ if (this.sending)
205
+ return this.sending.promise.then(() => this.flush());
206
+ const batch = this.buffered;
207
+ if (!batch)
208
+ return Promise.resolve();
209
+ this.startBatch(batch);
210
+ try {
211
+ this.analytics.flush((error) => this.completeBatch(batch, error || undefined));
212
+ }
213
+ catch (error) {
214
+ this.completeBatch(batch, error);
215
+ }
216
+ return batch.promise;
217
+ }
218
+ close() {
219
+ if (this.closed)
220
+ return;
221
+ this.closed = true;
222
+ if (this.analytics)
223
+ this.analytics.close();
224
+ this.abort();
225
+ }
226
+ abort() {
227
+ this.closed = true;
228
+ if (this.flushTimer)
229
+ clearTimeout(this.flushTimer);
230
+ this.flushTimer = undefined;
231
+ Array.from(this.requests.values()).forEach(function (cancel) {
232
+ cancel();
233
+ });
234
+ this.requests.clear();
235
+ this.agent.destroy();
236
+ const error = new Error('Collection adapter closed');
237
+ if (this.buffered)
238
+ this.completeBatch(this.buffered, error);
239
+ if (this.sending)
240
+ this.completeBatch(this.sending, error);
241
+ }
242
+ }
243
+ exports.OwnedCollectionAdapter = OwnedCollectionAdapter;
@@ -0,0 +1,11 @@
1
+ import type { ExposureAdapter, ExposureEvent, ExposureSubmission, ThinkingDataAnalytics } from '../types';
2
+ /** Converts SDK-owned exposure fields to the official Node track({ ... }) API. */
3
+ export declare class ThinkingDataAdapter implements ExposureAdapter {
4
+ private readonly analytics;
5
+ constructor(analytics: ThinkingDataAnalytics);
6
+ track(event: ExposureEvent, onFailure?: (error: Error) => void): ExposureSubmission;
7
+ flush(): void | PromiseLike<void>;
8
+ close(): void | PromiseLike<void>;
9
+ }
10
+ /** Adapt an existing collection SDK instance; its lifecycle stays caller-owned. */
11
+ export declare function createThinkingDataAdapter(analytics: ThinkingDataAnalytics): ExposureAdapter;