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,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseResponse = parseResponse;
4
+ exports.parseSnapshot = parseSnapshot;
5
+ const zlib_1 = require("zlib");
6
+ const errors_1 = require("../errors");
7
+ const json_1 = require("../common/json");
8
+ const config_snapshot_1 = require("./config-snapshot");
9
+ const stream_decoder_1 = require("./stream-decoder");
10
+ function invalid(message, cause) {
11
+ return new errors_1.FeatureError(message, 'INVALID_RESPONSE', { errorCode: 5000, cause: cause });
12
+ }
13
+ function integer(value, fallback) {
14
+ if (value === undefined || value === null || value === '')
15
+ return fallback;
16
+ if (typeof value === 'boolean')
17
+ return value ? 1 : 0;
18
+ if (typeof value === 'string' && /^[+-]?\d+$/.test(value))
19
+ value = Number(value);
20
+ if (typeof value !== 'number' || !isFinite(value) || Math.floor(value) !== value)
21
+ throw invalid('Expected an integer response field');
22
+ return value;
23
+ }
24
+ function expiration(outer, decoded) {
25
+ const info = decoded && (0, json_1.isObject)(decoded.templateInfo)
26
+ ? decoded.templateInfo
27
+ : (0, json_1.isObject)(outer.templateInfo)
28
+ ? outer.templateInfo
29
+ : null;
30
+ return info ? integer(info.expirationPeriod, 0) : 86400;
31
+ }
32
+ function control(value) {
33
+ if (value === undefined ||
34
+ value === null ||
35
+ value === '' ||
36
+ value === false ||
37
+ value === 'false' ||
38
+ value === '0')
39
+ return false;
40
+ if (value === true || value === 'true' || value === '1')
41
+ return true;
42
+ if (typeof value === 'number')
43
+ return value === 1;
44
+ throw invalid('Invalid #is_control_group');
45
+ }
46
+ function experiment(value) {
47
+ if (!(0, json_1.isObject)(value))
48
+ throw invalid('experiment_detail entries must be objects');
49
+ let bucket = null;
50
+ if (value.bucket_id !== undefined && value.bucket_id !== null) {
51
+ if (!(0, json_1.isObject)(value.bucket_id))
52
+ throw invalid('bucket_id must be an object');
53
+ bucket = Object.create(null);
54
+ Object.keys(value.bucket_id).forEach(function (key) {
55
+ bucket[key] = (0, json_1.stringValue)(value.bucket_id[key]);
56
+ });
57
+ Object.freeze(bucket);
58
+ }
59
+ let list = null;
60
+ if (value['#feature_list'] !== undefined && value['#feature_list'] !== null) {
61
+ if (!Array.isArray(value['#feature_list']))
62
+ throw invalid('#feature_list must be an array');
63
+ list = Object.freeze(value['#feature_list'].map(json_1.stringValue));
64
+ }
65
+ return Object.freeze({
66
+ experimentId: (0, json_1.stringValue)(value['#experiment_id']),
67
+ experimentGroupId: (0, json_1.stringValue)(value['#experiment_group_id']),
68
+ isControlGroup: control(value['#is_control_group']),
69
+ bucketId: bucket,
70
+ featureList: list,
71
+ });
72
+ }
73
+ function features(outer, decoded) {
74
+ const container = (0, json_1.isObject)(decoded.config) ? decoded.config : decoded;
75
+ const source = container.ta_feature;
76
+ const ttl = expiration(outer, decoded);
77
+ if (source === undefined || source === null)
78
+ return (0, config_snapshot_1.makeSnapshot)([], ttl);
79
+ if (!(0, json_1.isObject)(source))
80
+ throw invalid('ta_feature must be an object');
81
+ const byFeature = new Map();
82
+ const detail = source.experiment_detail;
83
+ if (detail !== undefined && detail !== null) {
84
+ if (!Array.isArray(detail))
85
+ throw invalid('experiment_detail must be an array');
86
+ detail.forEach(function (entry) {
87
+ const info = experiment(entry);
88
+ if (info.featureList)
89
+ info.featureList.forEach(function (key) {
90
+ if (key !== null && !byFeature.has(key))
91
+ byFeature.set(key, info);
92
+ });
93
+ });
94
+ }
95
+ return (0, config_snapshot_1.makeSnapshot)(Object.keys(source)
96
+ .filter(function (key) {
97
+ return key !== 'experiment_detail';
98
+ })
99
+ .map(function (key) {
100
+ return { key: key, value: source[key], experimentInfo: byFeature.get(key) || null };
101
+ }), ttl);
102
+ }
103
+ function parseResponse(text, limit, accountId, distinctId) {
104
+ return parseSnapshot(text, limit).then(function (snapshot) {
105
+ return (0, config_snapshot_1.bindSnapshot)(snapshot, accountId, distinctId);
106
+ });
107
+ }
108
+ function parseSnapshot(text, limit, operation) {
109
+ let outer;
110
+ let compressed;
111
+ try {
112
+ if (operation)
113
+ operation.check();
114
+ const parsed = JSON.parse(text);
115
+ if (operation)
116
+ operation.check();
117
+ if (!(0, json_1.isObject)(parsed))
118
+ throw invalid('Remote response must be a JSON object');
119
+ outer = parsed;
120
+ const code = integer(outer.code, 0);
121
+ if (code !== 0) {
122
+ const known = [1001, 1002, 1003, 1004, 2001, 2002, 5000].indexOf(code) >= 0 ? code : 5000;
123
+ throw new errors_1.FeatureError((0, json_1.stringValue)(outer.message) || (0, json_1.stringValue)(outer.msg) || 'Remote Config error ' + code, 'REMOTE_ERROR', { remoteCode: code, errorCode: known });
124
+ }
125
+ compressed = (0, json_1.stringValue)(outer.data);
126
+ if (compressed === null || compressed === '')
127
+ return Promise.resolve((0, config_snapshot_1.makeSnapshot)([], expiration(outer)));
128
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compressed) ||
129
+ compressed.length % 4 === 1 ||
130
+ (compressed.indexOf('=') >= 0 && compressed.length % 4 !== 0))
131
+ throw invalid('Invalid Base64 response data');
132
+ }
133
+ catch (error) {
134
+ return Promise.reject(error instanceof errors_1.FeatureError ? error : invalid('Invalid response envelope', error));
135
+ }
136
+ return new Promise((resolve, reject) => {
137
+ const decoder = new stream_decoder_1.Decoder((0, zlib_1.createGunzip)());
138
+ const gunzip = decoder.stream;
139
+ let settled = false;
140
+ let size = 0;
141
+ const chunks = [];
142
+ let unsubscribe = function () { };
143
+ const fail = (error) => {
144
+ if (settled)
145
+ return;
146
+ settled = true;
147
+ unsubscribe();
148
+ decoder.stop();
149
+ reject(error);
150
+ };
151
+ gunzip.on('error', (error) => fail(invalid('Failed to decompress response data', error)));
152
+ gunzip.on('data', (chunk) => {
153
+ if (settled)
154
+ return;
155
+ try {
156
+ if (operation)
157
+ operation.check();
158
+ }
159
+ catch (error) {
160
+ fail(error);
161
+ return;
162
+ }
163
+ size += chunk.length;
164
+ if (size > limit)
165
+ fail(new errors_1.FeatureError('Decompressed response exceeds maxDecompressedResponseBytes', 'RESPONSE_TOO_LARGE', { errorCode: 5000 }));
166
+ else
167
+ chunks.push(chunk);
168
+ });
169
+ gunzip.on('end', () => {
170
+ if (settled)
171
+ return;
172
+ try {
173
+ if (operation)
174
+ operation.check();
175
+ const decoded = JSON.parse(Buffer.concat(chunks, size).toString('utf8'));
176
+ if (operation)
177
+ operation.check();
178
+ if (!(0, json_1.isObject)(decoded))
179
+ throw invalid('Decoded response must be a JSON object');
180
+ const result = features(outer, decoded);
181
+ if (operation)
182
+ operation.check();
183
+ settled = true;
184
+ unsubscribe();
185
+ resolve(result);
186
+ }
187
+ catch (error) {
188
+ fail(error instanceof errors_1.FeatureError
189
+ ? error
190
+ : invalid('Invalid decoded configuration', error));
191
+ }
192
+ });
193
+ if (operation)
194
+ unsubscribe = operation.onCancel(fail);
195
+ if (!settled)
196
+ decoder.feed(Buffer.from(compressed, 'base64'), () => decoder.end());
197
+ });
198
+ }
@@ -0,0 +1,14 @@
1
+ import { ConfigSnapshot } from './config-snapshot';
2
+ export declare function configCacheKey(body: string): string;
3
+ export declare class ResultCache {
4
+ private readonly maxEntries;
5
+ private readonly fallbackSeconds;
6
+ private readonly now;
7
+ private readonly entries;
8
+ constructor(maxEntries: number, fallbackSeconds: number, now?: () => number);
9
+ get(key: string): ConfigSnapshot | null;
10
+ set(key: string, snapshot: ConfigSnapshot): boolean;
11
+ removeIfSame(key: string, snapshot: ConfigSnapshot): void;
12
+ clear(): void;
13
+ get size(): number;
14
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResultCache = void 0;
4
+ exports.configCacheKey = configCacheKey;
5
+ const crypto_1 = require("crypto");
6
+ const request_operation_1 = require("./request-operation");
7
+ function canonical(value) {
8
+ if (Array.isArray(value))
9
+ return '[' + value.map(canonical).join(',') + ']';
10
+ if (value !== null && typeof value === 'object')
11
+ return ('{' +
12
+ Object.keys(value)
13
+ .sort()
14
+ .map(function (key) {
15
+ return JSON.stringify(key) + ':' + canonical(value[key]);
16
+ })
17
+ .join(',') +
18
+ '}');
19
+ return JSON.stringify(value);
20
+ }
21
+ function configCacheKey(body) {
22
+ const request = JSON.parse(body);
23
+ const keys = request.request_params['#feature_key'];
24
+ if (Array.isArray(keys))
25
+ request.request_params['#feature_key'] = keys.slice().sort();
26
+ return (0, crypto_1.createHash)('sha256').update(canonical(request), 'utf8').digest('hex');
27
+ }
28
+ class ResultCache {
29
+ constructor(maxEntries, fallbackSeconds, now = request_operation_1.monotonicNow) {
30
+ this.maxEntries = maxEntries;
31
+ this.fallbackSeconds = fallbackSeconds;
32
+ this.now = now;
33
+ this.entries = new Map();
34
+ }
35
+ get(key) {
36
+ const entry = this.entries.get(key);
37
+ if (!entry)
38
+ return null;
39
+ if (entry.expires <= this.now()) {
40
+ this.entries.delete(key);
41
+ return null;
42
+ }
43
+ this.entries.delete(key);
44
+ this.entries.set(key, entry);
45
+ return entry.snapshot;
46
+ }
47
+ set(key, snapshot) {
48
+ this.entries.delete(key);
49
+ const seconds = snapshot.expirationPeriod > 0 ? snapshot.expirationPeriod : this.fallbackSeconds;
50
+ if (this.maxEntries === 0 || seconds <= 0)
51
+ return false;
52
+ while (this.entries.size >= this.maxEntries) {
53
+ this.entries.delete(this.entries.keys().next().value);
54
+ }
55
+ this.entries.set(key, { snapshot, expires: this.now() + seconds * 1000 });
56
+ return true;
57
+ }
58
+ removeIfSame(key, snapshot) {
59
+ const entry = this.entries.get(key);
60
+ if (entry && entry.snapshot === snapshot)
61
+ this.entries.delete(key);
62
+ }
63
+ clear() {
64
+ this.entries.clear();
65
+ }
66
+ get size() {
67
+ return this.entries.size;
68
+ }
69
+ }
70
+ exports.ResultCache = ResultCache;
@@ -0,0 +1,6 @@
1
+ import { FeatureError } from '../errors';
2
+ import type { ResolvedClientOptions } from '../types';
3
+ export declare function retryable(error: Error): boolean;
4
+ /** null means the server's minimum wait exceeds the configured retry limit. */
5
+ export declare function retryDelay(retryIndex: number, error: FeatureError, config: Readonly<ResolvedClientOptions>): number | null;
6
+ export declare function parseRetryAfter(value: string | string[] | undefined, now?: number): number | undefined;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.retryable = retryable;
4
+ exports.retryDelay = retryDelay;
5
+ exports.parseRetryAfter = parseRetryAfter;
6
+ const errors_1 = require("../errors");
7
+ function retryable(error) {
8
+ if (!(error instanceof errors_1.FeatureError))
9
+ return false;
10
+ if (error.code === 'TIMEOUT')
11
+ return true;
12
+ if (error.code === 'HTTP_ERROR')
13
+ return [408, 429, 500, 502, 503, 504].indexOf(error.httpStatus || 0) >= 0;
14
+ if (error.code !== 'NETWORK_ERROR')
15
+ return false;
16
+ const cause = error.cause;
17
+ const code = cause && cause.code;
18
+ return (code === undefined ||
19
+ [
20
+ 'ECONNRESET',
21
+ 'ECONNREFUSED',
22
+ 'EPIPE',
23
+ 'ETIMEDOUT',
24
+ 'ESOCKETTIMEDOUT',
25
+ 'EAI_AGAIN',
26
+ 'ECONNABORTED',
27
+ 'ERR_SOCKET_CLOSED',
28
+ 'ERR_STREAM_PREMATURE_CLOSE',
29
+ ].indexOf(code) >= 0);
30
+ }
31
+ /** null means the server's minimum wait exceeds the configured retry limit. */
32
+ function retryDelay(retryIndex, error, config) {
33
+ const retryAfter = error.retryAfterMillis || 0;
34
+ if (retryAfter > config.maxRetryInterval)
35
+ return null;
36
+ const interval = config.retryInterval *
37
+ (config.retryStrategy === 'EXPONENTIAL_BACKOFF' ? Math.pow(2, retryIndex) : 1);
38
+ const capped = Math.min(config.maxRetryInterval, interval);
39
+ const backoff = config.retryJitter ? Math.floor(Math.random() * (capped + 1)) : capped;
40
+ return Math.max(backoff, retryAfter);
41
+ }
42
+ function parseRetryAfter(value, now = Date.now()) {
43
+ if (typeof value !== 'string')
44
+ return undefined;
45
+ if (/^\d+$/.test(value))
46
+ return Math.min(2147483647, Number(value) * 1000);
47
+ const date = Date.parse(value);
48
+ return isFinite(date) ? Math.max(0, Math.min(2147483647, date - now)) : undefined;
49
+ }
@@ -0,0 +1,10 @@
1
+ import { Gunzip, Inflate } from 'zlib';
2
+ /** Backpressure and cancellation for native HTTP and payload decompression. */
3
+ export declare class Decoder {
4
+ readonly stream: Gunzip | Inflate;
5
+ private stopped;
6
+ constructor(stream: Gunzip | Inflate);
7
+ feed(input: Buffer, done: () => void): void;
8
+ end(): void;
9
+ stop(): void;
10
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Decoder = void 0;
4
+ /** Backpressure and cancellation for native HTTP and payload decompression. */
5
+ class Decoder {
6
+ constructor(stream) {
7
+ this.stream = stream;
8
+ this.stopped = false;
9
+ }
10
+ feed(input, done) {
11
+ if (this.stopped)
12
+ return;
13
+ this.stream.write(input, () => {
14
+ if (!this.stopped)
15
+ done();
16
+ });
17
+ }
18
+ end() {
19
+ if (!this.stopped)
20
+ this.stream.end();
21
+ }
22
+ stop() {
23
+ if (this.stopped)
24
+ return;
25
+ this.stopped = true;
26
+ this.stream.destroy();
27
+ }
28
+ }
29
+ exports.Decoder = Decoder;
@@ -0,0 +1,17 @@
1
+ export type FeatureErrorCode = 'INVALID_CONFIG' | 'INVALID_REQUEST' | 'NETWORK_ERROR' | 'HTTP_ERROR' | 'REMOTE_ERROR' | 'INVALID_RESPONSE' | 'RESPONSE_TOO_LARGE' | 'TIMEOUT' | 'CLIENT_CLOSED' | 'QUEUE_FULL' | 'ABORTED' | 'EXPOSURE_ERROR';
2
+ export interface FeatureErrorDetails {
3
+ httpStatus?: number;
4
+ remoteCode?: number;
5
+ errorCode?: number;
6
+ cause?: Error;
7
+ retryAfterMillis?: number;
8
+ }
9
+ export declare class FeatureError extends Error {
10
+ readonly code: FeatureErrorCode;
11
+ readonly httpStatus: number | undefined;
12
+ readonly remoteCode: number | undefined;
13
+ readonly errorCode: number;
14
+ readonly cause: Error | undefined;
15
+ readonly retryAfterMillis: number | undefined;
16
+ constructor(message: string, code?: FeatureErrorCode, details?: FeatureErrorDetails);
17
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureError = void 0;
4
+ class FeatureError extends Error {
5
+ constructor(message, code = 'INVALID_CONFIG', details = {}) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = 'FeatureError';
9
+ this.httpStatus = details.httpStatus;
10
+ this.remoteCode = details.remoteCode;
11
+ this.errorCode = details.errorCode === undefined ? -1 : details.errorCode;
12
+ this.cause = details.cause;
13
+ this.retryAfterMillis = details.retryAfterMillis;
14
+ }
15
+ }
16
+ exports.FeatureError = FeatureError;
@@ -0,0 +1,10 @@
1
+ import { Feature } from './feature';
2
+ export interface Evaluation {
3
+ /** Missing keys resolve to undefined; reserved object keys are safe to look up. */
4
+ readonly features: Readonly<{
5
+ [key: string]: Feature | undefined;
6
+ }>;
7
+ /** Original RCC TTL in seconds, not the remaining cache lifetime. */
8
+ readonly ttl: number;
9
+ }
10
+ export declare function createEvaluation(results: Feature[], ttl: number): Evaluation;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEvaluation = createEvaluation;
4
+ function createEvaluation(results, ttl) {
5
+ const features = Object.create(null);
6
+ for (const feature of results)
7
+ features[feature.key] = feature;
8
+ return Object.freeze({ features: Object.freeze(features), ttl });
9
+ }
@@ -0,0 +1,31 @@
1
+ import type { ExperimentInfo, JsonObject, JsonValue } from './types';
2
+ interface ValueReader<T> {
3
+ (): T | null;
4
+ (fallback: T): T;
5
+ (fallback: T | null): T | null;
6
+ }
7
+ /** A request-scoped result. Reading a value may trigger opt-in automatic exposure. */
8
+ export interface Feature {
9
+ readonly key: string;
10
+ readonly value: JsonValue;
11
+ readonly accountId: string | null;
12
+ readonly distinctId: string | null;
13
+ readonly experiment: ExperimentInfo | null;
14
+ readonly hasValue: boolean;
15
+ readonly asString: ValueReader<string>;
16
+ readonly asNumber: ValueReader<number>;
17
+ readonly asBoolean: ValueReader<boolean>;
18
+ asJson(fallback?: JsonObject | null): JsonObject | null;
19
+ }
20
+ interface FeatureOptions {
21
+ key: string;
22
+ value: JsonValue;
23
+ accountId: string | null;
24
+ distinctId: string | null;
25
+ experiment: ExperimentInfo | null;
26
+ onRead?: (feature: Feature) => void;
27
+ }
28
+ /** Internal factory: consumers obtain trusted results through client.fetch(). */
29
+ export declare function createFeature(options: FeatureOptions): Feature;
30
+ export declare function isFeature(value: unknown): value is Feature;
31
+ export {};
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFeature = createFeature;
4
+ exports.isFeature = isFeature;
5
+ const json_1 = require("./common/json");
6
+ const coercion_1 = require("./common/coercion");
7
+ const results = new WeakSet();
8
+ /** Internal factory: consumers obtain trusted results through client.fetch(). */
9
+ function createFeature(options) {
10
+ const { key, value, accountId, distinctId, experiment, onRead } = options;
11
+ const raw = (0, json_1.freezeJson)(value);
12
+ let triggered = false;
13
+ function read(result) {
14
+ if (onRead && experiment && !triggered) {
15
+ triggered = true;
16
+ // Collection failures never change a business value or its fallback.
17
+ try {
18
+ onRead(feature);
19
+ }
20
+ catch (_) { }
21
+ }
22
+ return result;
23
+ }
24
+ function asString(fallback = null) {
25
+ return read(value === null ? fallback : (0, json_1.stringValue)(value));
26
+ }
27
+ function asNumber(fallback = null) {
28
+ return read((0, coercion_1.toNumber)(value, fallback));
29
+ }
30
+ function asBoolean(fallback = null) {
31
+ return read((0, coercion_1.toBoolean)(value, fallback));
32
+ }
33
+ const feature = Object.freeze({
34
+ key,
35
+ get value() {
36
+ return read(raw);
37
+ },
38
+ accountId,
39
+ distinctId,
40
+ experiment,
41
+ hasValue: raw !== null,
42
+ asString,
43
+ asNumber,
44
+ asBoolean,
45
+ asJson(fallback = null) {
46
+ // Reuse the same immutable JSON value on repeated reads.
47
+ return read(raw !== null && typeof raw === 'object' && !Array.isArray(raw)
48
+ ? raw
49
+ : (0, coercion_1.toJson)(value, fallback));
50
+ },
51
+ });
52
+ results.add(feature);
53
+ return feature;
54
+ }
55
+ function isFeature(value) {
56
+ return value !== null && typeof value === 'object' && results.has(value);
57
+ }
@@ -0,0 +1,9 @@
1
+ export { createClient } from './client';
2
+ export { SplitMode } from './types';
3
+ export type { Client } from './client';
4
+ export { FeatureError } from './errors';
5
+ export type { FeatureErrorCode } from './errors';
6
+ export { createThinkingDataAdapter } from './report/thinking-data-adapter';
7
+ export type { Evaluation } from './evaluation';
8
+ export type { Feature } from './feature';
9
+ export type { ClientOptions, FeatureRequest, FetchOptions, AbortSignalLike, ExperimentInfo, JsonValue, JsonObject, ExposureAdapter, ExposureEvent, ExposureProperties, ExposureValue, ExposureSubmission, ThinkingDataAnalytics, ThinkingDataTrackEvent, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createThinkingDataAdapter = exports.FeatureError = exports.SplitMode = exports.createClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
6
+ var types_1 = require("./types");
7
+ Object.defineProperty(exports, "SplitMode", { enumerable: true, get: function () { return types_1.SplitMode; } });
8
+ var errors_1 = require("./errors");
9
+ Object.defineProperty(exports, "FeatureError", { enumerable: true, get: function () { return errors_1.FeatureError; } });
10
+ var thinking_data_adapter_1 = require("./report/thinking-data-adapter");
11
+ Object.defineProperty(exports, "createThinkingDataAdapter", { enumerable: true, get: function () { return thinking_data_adapter_1.createThinkingDataAdapter; } });
@@ -0,0 +1,3 @@
1
+ import type { ExperimentInfo, ExposureEvent, ExposureProperties } from '../types';
2
+ export declare function exposureSubject(info: ExperimentInfo, account: string | null, distinct: string | null): string;
3
+ export declare function exposureEvent(accountId: string | null, distinctId: string | null, info: ExperimentInfo, custom?: ExposureProperties | null): ExposureEvent;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exposureSubject = exposureSubject;
4
+ exports.exposureEvent = exposureEvent;
5
+ const errors_1 = require("../errors");
6
+ function exposureSubject(info, account, distinct) {
7
+ const bucket = info.bucketId;
8
+ if (bucket) {
9
+ const keys = Object.keys(bucket);
10
+ for (let i = 0; i < keys.length; i++) {
11
+ const value = bucket[keys[i]];
12
+ if (value !== null && value !== '')
13
+ return keys[i] + '=' + value;
14
+ }
15
+ }
16
+ if (account)
17
+ return '#account_id=' + account;
18
+ if (distinct)
19
+ return '#distinct_id=' + distinct;
20
+ throw new errors_1.FeatureError('accountId or distinctId is required for exposure', 'INVALID_REQUEST');
21
+ }
22
+ function copy(value, parents = []) {
23
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
24
+ return value;
25
+ if (typeof value === 'number' && isFinite(value))
26
+ return value;
27
+ if (!value || typeof value !== 'object' || parents.indexOf(value) >= 0)
28
+ throw new Error('Expected acyclic exposure properties');
29
+ if (value instanceof Date) {
30
+ if (!isFinite(value.getTime()))
31
+ throw new Error('Invalid exposure Date');
32
+ return new Date(value.getTime());
33
+ }
34
+ const path = parents.concat([value]);
35
+ if (Array.isArray(value))
36
+ return value.map(function (item) {
37
+ return copy(item, path);
38
+ });
39
+ const prototype = Object.getPrototypeOf(value);
40
+ if (prototype !== null && prototype !== Object.prototype)
41
+ throw new Error('Expected exposure JSON objects or Dates');
42
+ const result = Object.create(null);
43
+ Object.keys(value).forEach(function (key) {
44
+ result[key] = copy(value[key], path);
45
+ });
46
+ return result;
47
+ }
48
+ function exposureEvent(accountId, distinctId, info, custom) {
49
+ const properties = Object.create(null);
50
+ if (custom !== undefined && custom !== null) {
51
+ if (typeof custom !== 'object' || Array.isArray(custom) || custom instanceof Date)
52
+ throw new errors_1.FeatureError('Exposure properties must be an object', 'INVALID_REQUEST');
53
+ try {
54
+ Object.keys(custom).forEach(function (key) {
55
+ if (key.charAt(0) !== '#')
56
+ properties[key] = copy(custom[key]);
57
+ });
58
+ }
59
+ catch (error) {
60
+ throw new errors_1.FeatureError('Invalid exposure properties', 'INVALID_REQUEST', {
61
+ cause: error,
62
+ });
63
+ }
64
+ }
65
+ properties['#experiment_id'] = info.experimentId;
66
+ properties['#experiment_group_id'] = info.experimentGroupId;
67
+ properties['#is_control_group'] = info.isControlGroup;
68
+ properties['#rule_type'] = 'experiment';
69
+ const fields = {
70
+ '#feature_key': info.featureKey,
71
+ '#feature_version': info.featureVersion,
72
+ '#split_mode': info.splitMode,
73
+ '#assignment_source': info.assignmentSource,
74
+ };
75
+ Object.keys(fields).forEach(function (key) {
76
+ const value = fields[key];
77
+ if (value !== null && value !== undefined && value !== '')
78
+ properties[key] = value;
79
+ });
80
+ return {
81
+ accountId: accountId,
82
+ distinctId: distinctId,
83
+ event: 'te_experiment_exposure',
84
+ properties: properties,
85
+ };
86
+ }