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.
- package/README.md +174 -2
- package/dist/client.d.ts +29 -0
- package/dist/client.js +206 -0
- package/dist/common/coercion.d.ts +4 -0
- package/dist/common/coercion.js +32 -0
- package/dist/common/config.d.ts +8 -0
- package/dist/common/config.js +172 -0
- package/dist/common/json.d.ts +7 -0
- package/dist/common/json.js +59 -0
- package/dist/core/config-snapshot.d.ts +15 -0
- package/dist/core/config-snapshot.js +58 -0
- package/dist/core/http-transport.d.ts +13 -0
- package/dist/core/http-transport.js +181 -0
- package/dist/core/request-operation.d.ts +16 -0
- package/dist/core/request-operation.js +73 -0
- package/dist/core/request-scheduler.d.ts +16 -0
- package/dist/core/request-scheduler.js +111 -0
- package/dist/core/request.d.ts +10 -0
- package/dist/core/request.js +72 -0
- package/dist/core/response.d.ts +5 -0
- package/dist/core/response.js +198 -0
- package/dist/core/result-cache.d.ts +14 -0
- package/dist/core/result-cache.js +70 -0
- package/dist/core/retry.d.ts +6 -0
- package/dist/core/retry.js +49 -0
- package/dist/core/stream-decoder.d.ts +10 -0
- package/dist/core/stream-decoder.js +29 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +16 -0
- package/dist/evaluation.d.ts +10 -0
- package/dist/evaluation.js +9 -0
- package/dist/feature.d.ts +31 -0
- package/dist/feature.js +57 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +11 -0
- package/dist/report/event.d.ts +3 -0
- package/dist/report/event.js +86 -0
- package/dist/report/expose-reporter.d.ts +40 -0
- package/dist/report/expose-reporter.js +285 -0
- package/dist/report/exposure-dedup.d.ts +16 -0
- package/dist/report/exposure-dedup.js +45 -0
- package/dist/report/owned-collection-adapter.d.ts +22 -0
- package/dist/report/owned-collection-adapter.js +243 -0
- package/dist/report/thinking-data-adapter.d.ts +11 -0
- package/dist/report/thinking-data-adapter.js +54 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.js +9 -0
- package/npm-shrinkwrap.json +665 -0
- package/package.json +46 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isObject = isObject;
|
|
4
|
+
exports.stringValue = stringValue;
|
|
5
|
+
exports.freezeJson = freezeJson;
|
|
6
|
+
exports.snapshotJson = snapshotJson;
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function stringValue(value) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
14
|
+
}
|
|
15
|
+
// Only values recursively copied and frozen here may be reused. Object.isFrozen
|
|
16
|
+
// alone cannot prove that an externally supplied object's children are immutable.
|
|
17
|
+
const frozenJson = new WeakSet();
|
|
18
|
+
/** Freeze a detached JSON copy once, then reuse SDK-owned immutable values. */
|
|
19
|
+
function freezeJson(value) {
|
|
20
|
+
if (value !== null && typeof value === 'object' && frozenJson.has(value))
|
|
21
|
+
return value;
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
const result = Object.freeze(value.map(freezeJson));
|
|
24
|
+
frozenJson.add(result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
if (isObject(value)) {
|
|
28
|
+
const result = {};
|
|
29
|
+
Object.keys(value).forEach(function (key) {
|
|
30
|
+
Object.defineProperty(result, key, { value: freezeJson(value[key]), enumerable: true });
|
|
31
|
+
});
|
|
32
|
+
Object.freeze(result);
|
|
33
|
+
frozenJson.add(result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/** Snapshot a caller's JSON data without invoking toJSON hooks or losing reserved keys. */
|
|
39
|
+
function snapshotJson(value, ancestors = []) {
|
|
40
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
41
|
+
return value;
|
|
42
|
+
if (typeof value === 'number' && isFinite(value))
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof value !== 'object' || ancestors.indexOf(value) >= 0)
|
|
45
|
+
throw new Error('Expected acyclic JSON values');
|
|
46
|
+
const next = ancestors.concat([value]);
|
|
47
|
+
if (Array.isArray(value))
|
|
48
|
+
return value.map(function (item) {
|
|
49
|
+
return snapshotJson(item, next);
|
|
50
|
+
});
|
|
51
|
+
const prototype = Object.getPrototypeOf(value);
|
|
52
|
+
if (prototype !== null && prototype !== Object.prototype)
|
|
53
|
+
throw new Error('Expected a JSON object');
|
|
54
|
+
const result = Object.create(null);
|
|
55
|
+
Object.keys(value).forEach(function (key) {
|
|
56
|
+
result[key] = snapshotJson(value[key], next);
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Evaluation } from '../evaluation';
|
|
2
|
+
import { Feature } from '../feature';
|
|
3
|
+
import type { ExperimentInfo, JsonValue } from '../types';
|
|
4
|
+
export interface FeatureSnapshot {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly value: JsonValue;
|
|
7
|
+
readonly experimentInfo: ExperimentInfo | null;
|
|
8
|
+
}
|
|
9
|
+
export interface ConfigSnapshot {
|
|
10
|
+
readonly features: ReadonlyArray<FeatureSnapshot>;
|
|
11
|
+
readonly expirationPeriod: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function makeSnapshot(features: FeatureSnapshot[], expirationPeriod: number): ConfigSnapshot;
|
|
14
|
+
/** Fresh results/context on every call; experiment copies are shared within one result. */
|
|
15
|
+
export declare function bindSnapshot(snapshot: ConfigSnapshot, accountId: string | null, distinctId: string | null, automaticExposure?: (feature: Feature) => void): Evaluation;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeSnapshot = makeSnapshot;
|
|
4
|
+
exports.bindSnapshot = bindSnapshot;
|
|
5
|
+
const evaluation_1 = require("../evaluation");
|
|
6
|
+
const feature_1 = require("../feature");
|
|
7
|
+
const json_1 = require("../common/json");
|
|
8
|
+
function makeSnapshot(features, expirationPeriod) {
|
|
9
|
+
const entries = features.map(function (feature) {
|
|
10
|
+
return Object.freeze({ ...feature, value: (0, json_1.freezeJson)(feature.value) });
|
|
11
|
+
});
|
|
12
|
+
return Object.freeze({ features: Object.freeze(entries), expirationPeriod });
|
|
13
|
+
}
|
|
14
|
+
function copyInfo(info) {
|
|
15
|
+
const bucket = info.bucketId
|
|
16
|
+
? Object.create(null)
|
|
17
|
+
: null;
|
|
18
|
+
if (bucket)
|
|
19
|
+
Object.keys(info.bucketId).forEach(function (key) {
|
|
20
|
+
bucket[key] = info.bucketId[key];
|
|
21
|
+
});
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
experimentId: info.experimentId,
|
|
24
|
+
experimentGroupId: info.experimentGroupId,
|
|
25
|
+
isControlGroup: info.isControlGroup,
|
|
26
|
+
bucketId: bucket ? Object.freeze(bucket) : null,
|
|
27
|
+
featureList: info.featureList ? Object.freeze(info.featureList.slice()) : null,
|
|
28
|
+
featureKey: info.featureKey,
|
|
29
|
+
featureVersion: info.featureVersion,
|
|
30
|
+
splitMode: info.splitMode,
|
|
31
|
+
assignmentSource: info.assignmentSource,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Fresh results/context on every call; experiment copies are shared within one result. */
|
|
35
|
+
function bindSnapshot(snapshot, accountId, distinctId, automaticExposure) {
|
|
36
|
+
// An experiment may cover many features. Copy its full feature list only once
|
|
37
|
+
// per evaluation, without retaining metadata or read state across fetches.
|
|
38
|
+
const experiments = new Map();
|
|
39
|
+
return (0, evaluation_1.createEvaluation)(snapshot.features.map(function (feature) {
|
|
40
|
+
const source = feature.experimentInfo;
|
|
41
|
+
let experiment = null;
|
|
42
|
+
if (source) {
|
|
43
|
+
experiment = experiments.get(source) || null;
|
|
44
|
+
if (!experiment) {
|
|
45
|
+
experiment = copyInfo(source);
|
|
46
|
+
experiments.set(source, experiment);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return (0, feature_1.createFeature)({
|
|
50
|
+
key: feature.key,
|
|
51
|
+
value: feature.value,
|
|
52
|
+
accountId,
|
|
53
|
+
distinctId,
|
|
54
|
+
experiment: experiment,
|
|
55
|
+
onRead: automaticExposure,
|
|
56
|
+
});
|
|
57
|
+
}), snapshot.expirationPeriod);
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedClientOptions } from '../types';
|
|
2
|
+
import { BuiltRequest } from './request';
|
|
3
|
+
import { Operation } from './request-operation';
|
|
4
|
+
export declare class HttpTransport {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly httpAgent;
|
|
7
|
+
private readonly httpsAgent;
|
|
8
|
+
private readonly active;
|
|
9
|
+
private closed;
|
|
10
|
+
constructor(config: Readonly<ResolvedClientOptions>);
|
|
11
|
+
send(built: BuiltRequest, operation: Operation): Promise<string>;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpTransport = void 0;
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const zlib = require("zlib");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const config_1 = require("../common/config");
|
|
9
|
+
const request_1 = require("./request");
|
|
10
|
+
const retry_1 = require("./retry");
|
|
11
|
+
const stream_decoder_1 = require("./stream-decoder");
|
|
12
|
+
class HttpTransport {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.active = new Set();
|
|
16
|
+
this.closed = false;
|
|
17
|
+
const options = {
|
|
18
|
+
keepAlive: true,
|
|
19
|
+
maxSockets: config.maxHttpConnections,
|
|
20
|
+
maxFreeSockets: config.maxHttpConnections,
|
|
21
|
+
};
|
|
22
|
+
this.httpAgent = new http.Agent(options);
|
|
23
|
+
this.httpsAgent = new https.Agent(Object.assign({}, options, { ca: config.tlsCa }));
|
|
24
|
+
}
|
|
25
|
+
send(built, operation) {
|
|
26
|
+
if (this.closed)
|
|
27
|
+
return Promise.reject(new errors_1.FeatureError('Client is closed', 'CLIENT_CLOSED'));
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let request;
|
|
30
|
+
let response;
|
|
31
|
+
let decoder;
|
|
32
|
+
let settled = false;
|
|
33
|
+
let unsubscribe = function () { };
|
|
34
|
+
const fail = (error) => {
|
|
35
|
+
if (settled)
|
|
36
|
+
return;
|
|
37
|
+
settled = true;
|
|
38
|
+
cleanup();
|
|
39
|
+
// Already-closing native resources must not prevent promise settlement.
|
|
40
|
+
try {
|
|
41
|
+
if (request)
|
|
42
|
+
request.abort();
|
|
43
|
+
}
|
|
44
|
+
catch (_) { }
|
|
45
|
+
try {
|
|
46
|
+
if (response)
|
|
47
|
+
response.destroy();
|
|
48
|
+
}
|
|
49
|
+
catch (_) { }
|
|
50
|
+
try {
|
|
51
|
+
if (decoder)
|
|
52
|
+
decoder.stop();
|
|
53
|
+
}
|
|
54
|
+
catch (_) { }
|
|
55
|
+
reject(error);
|
|
56
|
+
};
|
|
57
|
+
operation.check();
|
|
58
|
+
const timer = setTimeout(() => fail(new errors_1.FeatureError('Remote request timed out', 'TIMEOUT')), this.config.timeout);
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
unsubscribe();
|
|
62
|
+
this.active.delete(fail);
|
|
63
|
+
};
|
|
64
|
+
this.active.add(fail);
|
|
65
|
+
unsubscribe = operation.onCancel(fail);
|
|
66
|
+
if (settled)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const location = (0, config_1.requestLocation)(this.config.serverUrl);
|
|
70
|
+
const secure = location.protocol === 'https:';
|
|
71
|
+
const timestamp = Date.now();
|
|
72
|
+
request = (secure ? https : http).request({
|
|
73
|
+
hostname: location.hostname,
|
|
74
|
+
port: location.port || (secure ? 443 : 80),
|
|
75
|
+
path: location.path,
|
|
76
|
+
method: 'POST',
|
|
77
|
+
agent: secure ? this.httpsAgent : this.httpAgent,
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
'Content-Length': Buffer.byteLength(built.body, 'utf8'),
|
|
81
|
+
appid: headerValue(this.config.appId),
|
|
82
|
+
template_code: 'ta_feature',
|
|
83
|
+
timestamp: String(timestamp),
|
|
84
|
+
sign: (0, request_1.calculateSign)(timestamp, built.body),
|
|
85
|
+
device_id: headerValue(built.deviceId),
|
|
86
|
+
},
|
|
87
|
+
}, (incoming) => {
|
|
88
|
+
response = incoming;
|
|
89
|
+
let rawEnded = false;
|
|
90
|
+
let decodedBytes = 0;
|
|
91
|
+
const chunks = [];
|
|
92
|
+
incoming.on('error', (error) => fail(new errors_1.FeatureError('Response stream failed', 'NETWORK_ERROR', { cause: error })));
|
|
93
|
+
incoming.on('aborted', () => fail(new errors_1.FeatureError('Remote response was aborted', 'NETWORK_ERROR')));
|
|
94
|
+
incoming.on('end', () => {
|
|
95
|
+
rawEnded = true;
|
|
96
|
+
});
|
|
97
|
+
incoming.on('close', () => {
|
|
98
|
+
if (!rawEnded)
|
|
99
|
+
fail(new errors_1.FeatureError('Remote response ended prematurely', 'NETWORK_ERROR'));
|
|
100
|
+
});
|
|
101
|
+
const encoding = String(incoming.headers['content-encoding'] || 'identity').toLowerCase();
|
|
102
|
+
let stream = incoming;
|
|
103
|
+
if (encoding === 'gzip' || encoding === 'deflate') {
|
|
104
|
+
decoder = new stream_decoder_1.Decoder(encoding === 'gzip' ? zlib.createGunzip() : zlib.createInflate());
|
|
105
|
+
decoder.stream.on('error', (error) => fail(new errors_1.FeatureError('Invalid HTTP compression', 'INVALID_RESPONSE', { cause: error })));
|
|
106
|
+
stream = decoder.stream;
|
|
107
|
+
incoming.on('data', (chunk) => {
|
|
108
|
+
if (settled)
|
|
109
|
+
return;
|
|
110
|
+
incoming.pause();
|
|
111
|
+
decoder.feed(chunk, () => {
|
|
112
|
+
if (!settled)
|
|
113
|
+
incoming.resume();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
incoming.on('end', () => {
|
|
117
|
+
if (!settled)
|
|
118
|
+
decoder.end();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else if (encoding !== 'identity') {
|
|
122
|
+
fail(new errors_1.FeatureError('Unsupported HTTP content encoding: ' + encoding, 'INVALID_RESPONSE'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
stream.on('data', (chunk) => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
decodedBytes += chunk.length;
|
|
129
|
+
chunks.push(chunk);
|
|
130
|
+
});
|
|
131
|
+
stream.on('end', () => {
|
|
132
|
+
if (settled)
|
|
133
|
+
return;
|
|
134
|
+
const status = incoming.statusCode || 0;
|
|
135
|
+
if (status < 200 || status >= 300) {
|
|
136
|
+
fail(new errors_1.FeatureError('Remote Config Center returned HTTP ' + status, 'HTTP_ERROR', {
|
|
137
|
+
httpStatus: status,
|
|
138
|
+
retryAfterMillis: (0, retry_1.parseRetryAfter)(incoming.headers['retry-after']),
|
|
139
|
+
}));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const body = Buffer.concat(chunks, decodedBytes).toString('utf8');
|
|
144
|
+
settled = true;
|
|
145
|
+
cleanup();
|
|
146
|
+
resolve(body);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
fail(new errors_1.FeatureError('Failed to read remote response', 'INVALID_RESPONSE', {
|
|
150
|
+
cause: error,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
request.on('error', (error) => fail(new errors_1.FeatureError('Failed to call Remote Config Center', 'NETWORK_ERROR', {
|
|
156
|
+
cause: error,
|
|
157
|
+
})));
|
|
158
|
+
// A Buffer keeps UTF-8 body bytes separate from Node's Latin-1 header encoding.
|
|
159
|
+
request.end(Buffer.from(built.body, 'utf8'));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
fail(new errors_1.FeatureError('Failed to create remote request', 'INVALID_REQUEST', {
|
|
163
|
+
cause: error,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
close() {
|
|
169
|
+
this.closed = true;
|
|
170
|
+
Array.from(this.active).forEach(function (fail) {
|
|
171
|
+
fail(new errors_1.FeatureError('Client is closed', 'CLIENT_CLOSED'));
|
|
172
|
+
});
|
|
173
|
+
this.httpAgent.destroy();
|
|
174
|
+
this.httpsAgent.destroy();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.HttpTransport = HttpTransport;
|
|
178
|
+
/** Apache's default header encoder is ISO-8859-1; verified by Java wire fixtures. */
|
|
179
|
+
function headerValue(value) {
|
|
180
|
+
return value.replace(/[^\x00-\xff]/g, '?');
|
|
181
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AbortSignalLike } from '../types';
|
|
2
|
+
import { FeatureError } from '../errors';
|
|
3
|
+
export declare function monotonicNow(): number;
|
|
4
|
+
/** One caller's cancellation scope, shared by every stage of its fetch. */
|
|
5
|
+
export declare class Operation {
|
|
6
|
+
private readonly signal?;
|
|
7
|
+
private readonly listeners;
|
|
8
|
+
private readonly abortListener;
|
|
9
|
+
private reason;
|
|
10
|
+
constructor(signal?: AbortSignalLike | undefined);
|
|
11
|
+
check(): void;
|
|
12
|
+
cancel(error: FeatureError): void;
|
|
13
|
+
onCancel(listener: (error: FeatureError) => void): () => void;
|
|
14
|
+
sleep(milliseconds: number): Promise<void>;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Operation = void 0;
|
|
4
|
+
exports.monotonicNow = monotonicNow;
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
function monotonicNow() {
|
|
7
|
+
const time = process.hrtime();
|
|
8
|
+
return time[0] * 1000 + time[1] / 1000000;
|
|
9
|
+
}
|
|
10
|
+
/** One caller's cancellation scope, shared by every stage of its fetch. */
|
|
11
|
+
class Operation {
|
|
12
|
+
constructor(signal) {
|
|
13
|
+
this.signal = signal;
|
|
14
|
+
this.listeners = new Set();
|
|
15
|
+
this.abortListener = () => this.cancel(new errors_1.FeatureError('Fetch was aborted by its caller', 'ABORTED'));
|
|
16
|
+
if (signal) {
|
|
17
|
+
signal.addEventListener('abort', this.abortListener);
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
this.abortListener();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
check() {
|
|
23
|
+
if (this.reason)
|
|
24
|
+
throw this.reason;
|
|
25
|
+
}
|
|
26
|
+
cancel(error) {
|
|
27
|
+
if (this.reason)
|
|
28
|
+
return;
|
|
29
|
+
this.reason = error;
|
|
30
|
+
const callbacks = Array.from(this.listeners);
|
|
31
|
+
this.listeners.clear();
|
|
32
|
+
callbacks.forEach(function (callback) {
|
|
33
|
+
callback(error);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
onCancel(listener) {
|
|
37
|
+
if (this.reason) {
|
|
38
|
+
listener(this.reason);
|
|
39
|
+
return function () { };
|
|
40
|
+
}
|
|
41
|
+
this.listeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.listeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
sleep(milliseconds) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
this.check();
|
|
49
|
+
let unsubscribe = function () { };
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
unsubscribe();
|
|
52
|
+
try {
|
|
53
|
+
this.check();
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
reject(error);
|
|
58
|
+
}
|
|
59
|
+
}, Math.max(0, Math.ceil(milliseconds)));
|
|
60
|
+
unsubscribe = this.onCancel((error) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
unsubscribe();
|
|
63
|
+
reject(error);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
if (this.signal)
|
|
69
|
+
this.signal.removeEventListener('abort', this.abortListener);
|
|
70
|
+
this.listeners.clear();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.Operation = Operation;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Operation } from './request-operation';
|
|
2
|
+
/** FIFO admission; an active job keeps its slot through decoding and retry backoff. */
|
|
3
|
+
export declare class Scheduler {
|
|
4
|
+
private readonly concurrency;
|
|
5
|
+
private readonly capacity;
|
|
6
|
+
private active;
|
|
7
|
+
private waiting;
|
|
8
|
+
private head;
|
|
9
|
+
private tail;
|
|
10
|
+
private closed;
|
|
11
|
+
constructor(concurrency: number, capacity: number);
|
|
12
|
+
submit<T>(operation: Operation, run: () => Promise<T>): Promise<T>;
|
|
13
|
+
private remove;
|
|
14
|
+
private pump;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Scheduler = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
/** FIFO admission; an active job keeps its slot through decoding and retry backoff. */
|
|
6
|
+
class Scheduler {
|
|
7
|
+
constructor(concurrency, capacity) {
|
|
8
|
+
this.concurrency = concurrency;
|
|
9
|
+
this.capacity = capacity;
|
|
10
|
+
this.active = 0;
|
|
11
|
+
this.waiting = 0;
|
|
12
|
+
this.head = null;
|
|
13
|
+
this.tail = null;
|
|
14
|
+
this.closed = false;
|
|
15
|
+
}
|
|
16
|
+
submit(operation, run) {
|
|
17
|
+
operation.check();
|
|
18
|
+
if (this.closed)
|
|
19
|
+
throw new errors_1.FeatureError('Client is closed', 'CLIENT_CLOSED');
|
|
20
|
+
if ((this.active >= this.concurrency || this.head) && this.waiting >= this.capacity) {
|
|
21
|
+
throw new errors_1.FeatureError('Fetch waiting queue is full', 'QUEUE_FULL');
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const job = {
|
|
25
|
+
operation: operation,
|
|
26
|
+
reject: reject,
|
|
27
|
+
queued: false,
|
|
28
|
+
previous: null,
|
|
29
|
+
next: null,
|
|
30
|
+
unsubscribe: function () { },
|
|
31
|
+
start: () => {
|
|
32
|
+
job.unsubscribe();
|
|
33
|
+
this.active++;
|
|
34
|
+
Promise.resolve()
|
|
35
|
+
.then(() => {
|
|
36
|
+
operation.check();
|
|
37
|
+
return run();
|
|
38
|
+
})
|
|
39
|
+
.then((value) => {
|
|
40
|
+
this.active--;
|
|
41
|
+
resolve(value);
|
|
42
|
+
this.pump();
|
|
43
|
+
}, (error) => {
|
|
44
|
+
this.active--;
|
|
45
|
+
reject(error);
|
|
46
|
+
this.pump();
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
if (this.active < this.concurrency && !this.head) {
|
|
51
|
+
job.start();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
job.queued = true;
|
|
55
|
+
job.previous = this.tail;
|
|
56
|
+
if (this.tail)
|
|
57
|
+
this.tail.next = job;
|
|
58
|
+
else
|
|
59
|
+
this.head = job;
|
|
60
|
+
this.tail = job;
|
|
61
|
+
this.waiting++;
|
|
62
|
+
job.unsubscribe = operation.onCancel((error) => {
|
|
63
|
+
if (!job.queued)
|
|
64
|
+
return;
|
|
65
|
+
this.remove(job);
|
|
66
|
+
job.unsubscribe();
|
|
67
|
+
reject(error);
|
|
68
|
+
this.pump();
|
|
69
|
+
});
|
|
70
|
+
this.pump();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
remove(job) {
|
|
74
|
+
if (job.previous)
|
|
75
|
+
job.previous.next = job.next;
|
|
76
|
+
else
|
|
77
|
+
this.head = job.next;
|
|
78
|
+
if (job.next)
|
|
79
|
+
job.next.previous = job.previous;
|
|
80
|
+
else
|
|
81
|
+
this.tail = job.previous;
|
|
82
|
+
job.previous = null;
|
|
83
|
+
job.next = null;
|
|
84
|
+
job.queued = false;
|
|
85
|
+
this.waiting--;
|
|
86
|
+
}
|
|
87
|
+
pump() {
|
|
88
|
+
while (!this.closed && this.active < this.concurrency && this.head) {
|
|
89
|
+
const job = this.head;
|
|
90
|
+
this.remove(job);
|
|
91
|
+
job.unsubscribe();
|
|
92
|
+
try {
|
|
93
|
+
job.operation.check();
|
|
94
|
+
job.start();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
job.reject(error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.closed = true;
|
|
103
|
+
while (this.head) {
|
|
104
|
+
const job = this.head;
|
|
105
|
+
this.remove(job);
|
|
106
|
+
job.unsubscribe();
|
|
107
|
+
job.reject(new errors_1.FeatureError('Client is closed', 'CLIENT_CLOSED'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.Scheduler = Scheduler;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FeatureRequest } from '../types';
|
|
2
|
+
export declare const RCC_SIGN_KEY = "thinkingdata-remote-config";
|
|
3
|
+
export interface BuiltRequest {
|
|
4
|
+
body: string;
|
|
5
|
+
accountId: string | null;
|
|
6
|
+
distinctId: string | null;
|
|
7
|
+
deviceId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function calculateSign(timestamp: number | string, body: string): string;
|
|
10
|
+
export declare function buildRequest(appId: string, request: FeatureRequest): BuiltRequest;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RCC_SIGN_KEY = void 0;
|
|
4
|
+
exports.calculateSign = calculateSign;
|
|
5
|
+
exports.buildRequest = buildRequest;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const json_1 = require("../common/json");
|
|
8
|
+
const errors_1 = require("../errors");
|
|
9
|
+
exports.RCC_SIGN_KEY = 'thinkingdata-remote-config';
|
|
10
|
+
function calculateSign(timestamp, body) {
|
|
11
|
+
return (0, crypto_1.createHash)('sha256')
|
|
12
|
+
.update(String(timestamp) + body + exports.RCC_SIGN_KEY, 'utf8')
|
|
13
|
+
.digest('base64');
|
|
14
|
+
}
|
|
15
|
+
function buildRequest(appId, request) {
|
|
16
|
+
try {
|
|
17
|
+
if (!request || typeof request !== 'object' || Array.isArray(request))
|
|
18
|
+
throw new Error('FeatureRequest is required');
|
|
19
|
+
const accountId = request.accountId === undefined ? null : request.accountId;
|
|
20
|
+
const distinctId = request.distinctId === undefined ? null : request.distinctId;
|
|
21
|
+
[accountId, distinctId].forEach(function (id) {
|
|
22
|
+
if (id !== null && typeof id !== 'string')
|
|
23
|
+
throw new Error('Identifiers must be strings');
|
|
24
|
+
if (id !== null && /[\x00-\x1f\x7f]/.test(id))
|
|
25
|
+
throw new Error('Identifiers cannot contain control characters');
|
|
26
|
+
});
|
|
27
|
+
if (!accountId && !distinctId)
|
|
28
|
+
throw new Error('accountId or distinctId is required');
|
|
29
|
+
const custom = request.customParams;
|
|
30
|
+
if (custom !== undefined &&
|
|
31
|
+
custom !== null &&
|
|
32
|
+
(typeof custom !== 'object' || Array.isArray(custom))) {
|
|
33
|
+
throw new Error('customParams must be a JSON object');
|
|
34
|
+
}
|
|
35
|
+
const params = custom == null ? Object.create(null) : (0, json_1.snapshotJson)(custom);
|
|
36
|
+
params['#account_id'] = accountId;
|
|
37
|
+
params['#distinct_id'] = distinctId;
|
|
38
|
+
if (accountId === null)
|
|
39
|
+
delete params['#account_id'];
|
|
40
|
+
if (distinctId === null)
|
|
41
|
+
delete params['#distinct_id'];
|
|
42
|
+
params['#lib'] = 'node';
|
|
43
|
+
if (request.featureKeys !== undefined &&
|
|
44
|
+
request.featureKeys !== null &&
|
|
45
|
+
!Array.isArray(request.featureKeys)) {
|
|
46
|
+
throw new Error('featureKeys must be an array');
|
|
47
|
+
}
|
|
48
|
+
if (request.featureKeys && request.featureKeys.length) {
|
|
49
|
+
params['#feature_key'] = request.featureKeys.map(function (key) {
|
|
50
|
+
if (typeof key !== 'string')
|
|
51
|
+
throw new Error('Feature keys must be strings');
|
|
52
|
+
return key;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
delete params['#feature_key'];
|
|
57
|
+
const body = JSON.stringify({
|
|
58
|
+
appid: appId,
|
|
59
|
+
template_code: 'ta_feature',
|
|
60
|
+
request_params: params,
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
body: body,
|
|
64
|
+
accountId: accountId,
|
|
65
|
+
distinctId: distinctId,
|
|
66
|
+
deviceId: accountId || distinctId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new errors_1.FeatureError(error.message, 'INVALID_REQUEST', { cause: error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Evaluation } from '../evaluation';
|
|
2
|
+
import { ConfigSnapshot } from './config-snapshot';
|
|
3
|
+
import { Operation } from './request-operation';
|
|
4
|
+
export declare function parseResponse(text: string, limit: number, accountId: string | null, distinctId: string | null): Promise<Evaluation>;
|
|
5
|
+
export declare function parseSnapshot(text: string, limit: number, operation?: Operation): Promise<ConfigSnapshot>;
|