@featbit/js-client-sdk 3.0.13 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +301 -301
- package/dist/esm/FbClientCore.d.ts +6 -5
- package/dist/esm/FbClientCore.d.ts.map +1 -1
- package/dist/esm/FbClientCore.js +27 -4
- package/dist/esm/FbClientCore.js.map +1 -1
- package/dist/esm/IFbClientCore.d.ts +8 -7
- package/dist/esm/IFbClientCore.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.d.ts.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/umd/featbit-js-client-sdk-4.0.0.js +2 -0
- package/dist/umd/featbit-js-client-sdk-4.0.0.js.map +1 -0
- package/dist/umd/featbit-js-client-sdk.js +1 -1
- package/dist/umd/featbit-js-client-sdk.js.map +1 -1
- package/package.json +46 -46
- package/src/Configuration.ts +232 -232
- package/src/Context.ts +61 -61
- package/src/FbClientBuilder.ts +167 -167
- package/src/FbClientCore.ts +428 -405
- package/src/IContextProperty.ts +3 -3
- package/src/IDataKind.ts +11 -11
- package/src/IFbClient.ts +29 -29
- package/src/IFbClientCore.ts +291 -290
- package/src/IVersionedData.ts +18 -18
- package/src/bootstrap/IBootstrapProvider.ts +4 -4
- package/src/bootstrap/JsonBootstrapProvider.ts +34 -34
- package/src/bootstrap/NullBootstrapProvider.ts +20 -20
- package/src/bootstrap/index.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/data-sources/DataSourceUpdates.ts +116 -116
- package/src/data-sources/createStreamListeners.ts +67 -67
- package/src/data-sources/index.ts +1 -1
- package/src/data-sync/DataSyncMode.ts +3 -3
- package/src/data-sync/IDataSynchronizer.ts +15 -15
- package/src/data-sync/IRequestor.ts +10 -10
- package/src/data-sync/NullDataSynchronizer.ts +14 -14
- package/src/data-sync/PollingDataSynchronizer.ts +125 -125
- package/src/data-sync/Requestor.ts +61 -61
- package/src/data-sync/WebSocketDataSynchronizer.ts +77 -77
- package/src/data-sync/index.ts +8 -8
- package/src/data-sync/types.ts +19 -19
- package/src/data-sync/utils.ts +31 -31
- package/src/errors.ts +47 -47
- package/src/evaluation/EvalResult.ts +35 -35
- package/src/evaluation/Evaluator.ts +26 -26
- package/src/evaluation/IEvalDetail.ts +23 -23
- package/src/evaluation/ReasonKinds.ts +9 -9
- package/src/evaluation/data/IFlag.ts +29 -29
- package/src/evaluation/index.ts +4 -4
- package/src/events/DefaultEventProcessor.ts +83 -83
- package/src/events/DefaultEventQueue.ts +49 -49
- package/src/events/DefaultEventSender.ts +73 -73
- package/src/events/DefaultEventSerializer.ts +11 -11
- package/src/events/EventDispatcher.ts +127 -127
- package/src/events/EventSerializer.ts +4 -4
- package/src/events/IEventProcessor.ts +8 -8
- package/src/events/IEventQueue.ts +16 -16
- package/src/events/IEventSender.ts +13 -13
- package/src/events/NullEventProcessor.ts +15 -15
- package/src/events/event.ts +129 -129
- package/src/events/index.ts +11 -11
- package/src/index.ts +21 -21
- package/src/integrations/TestLogger.ts +24 -24
- package/src/integrations/index.ts +1 -1
- package/src/integrations/test_data/FlagBuilder.ts +59 -59
- package/src/integrations/test_data/TestData.ts +57 -57
- package/src/integrations/test_data/TestDataSynchronizer.ts +49 -49
- package/src/integrations/test_data/index.ts +4 -4
- package/src/logging/BasicLogger.ts +108 -108
- package/src/logging/IBasicLoggerOptions.ts +46 -46
- package/src/logging/ILogger.ts +49 -49
- package/src/logging/LogLevel.ts +8 -8
- package/src/logging/SafeLogger.ts +69 -69
- package/src/logging/format.ts +154 -154
- package/src/logging/index.ts +5 -5
- package/src/options/ClientContext.ts +39 -39
- package/src/options/IClientContext.ts +53 -53
- package/src/options/IOptions.ts +123 -123
- package/src/options/IUser.ts +6 -6
- package/src/options/IValidatedOptions.ts +29 -29
- package/src/options/OptionMessages.ts +35 -35
- package/src/options/UserBuilder.ts +35 -35
- package/src/options/Validators.ts +300 -300
- package/src/options/index.ts +7 -7
- package/src/platform/IInfo.ts +102 -102
- package/src/platform/IPlatform.ts +20 -20
- package/src/platform/IStore.ts +112 -112
- package/src/platform/IWebSocket.ts +22 -22
- package/src/platform/browser/BrowserInfo.ts +24 -24
- package/src/platform/browser/BrowserPlatform.ts +19 -19
- package/src/platform/browser/BrowserRequests.ts +6 -6
- package/src/platform/browser/BrowserWebSocket.ts +147 -147
- package/src/platform/browser/FbClient.ts +65 -65
- package/src/platform/browser/LocalStorageStore.ts +59 -59
- package/src/platform/index.ts +11 -11
- package/src/platform/requests.ts +76 -76
- package/src/store/BaseStore.ts +125 -125
- package/src/store/DataKinds.ts +6 -6
- package/src/store/IDataSourceUpdates.ts +68 -68
- package/src/store/InMemoryStore.ts +36 -36
- package/src/store/index.ts +5 -5
- package/src/store/serialization.ts +52 -52
- package/src/store/store.ts +37 -37
- package/src/utils/Emits.ts +75 -75
- package/src/utils/EventEmitter.ts +128 -128
- package/src/utils/IEventEmitter.ts +14 -14
- package/src/utils/Regex.ts +21 -21
- package/src/utils/ValueConverters.ts +55 -55
- package/src/utils/canonicalizeUri.ts +3 -3
- package/src/utils/debounce.ts +33 -33
- package/src/utils/http.ts +40 -40
- package/src/utils/index.ts +5 -5
- package/src/utils/isNullOrUndefined.ts +2 -2
- package/src/utils/serializeUser.ts +27 -27
- package/src/utils/sleep.ts +5 -5
- package/src/version.ts +1 -1
- package/dist/umd/featbit-js-client-sdk-3.0.13.js +0 -2
- package/dist/umd/featbit-js-client-sdk-3.0.13.js.map +0 -1
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
import { isHttpRecoverable, PollingError } from "../errors";
|
|
2
|
-
import { IDataSynchronizer } from "./IDataSynchronizer";
|
|
3
|
-
import { ILogger } from "../logging/ILogger";
|
|
4
|
-
import Configuration from "../Configuration";
|
|
5
|
-
import { EventName, PollingErrorHandler, ProcessStreamResponse, StreamResponseEventType } from "./types";
|
|
6
|
-
import Requestor from "./Requestor";
|
|
7
|
-
import { httpErrorMessage } from "../utils/http";
|
|
8
|
-
import { IUser } from "../options/IUser";
|
|
9
|
-
|
|
10
|
-
export default class PollingDataSynchronizer implements IDataSynchronizer {
|
|
11
|
-
private stopped = false;
|
|
12
|
-
|
|
13
|
-
private logger?: ILogger;
|
|
14
|
-
|
|
15
|
-
private pollingInterval: number;
|
|
16
|
-
|
|
17
|
-
private user: IUser | undefined;
|
|
18
|
-
|
|
19
|
-
private timeoutHandle: any;
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
22
|
-
config: Configuration,
|
|
23
|
-
private readonly requestor: Requestor,
|
|
24
|
-
private readonly getStoreTimestamp: () => number,
|
|
25
|
-
private readonly listeners: Map<EventName, ProcessStreamResponse>,
|
|
26
|
-
private readonly errorHandler?: PollingErrorHandler,
|
|
27
|
-
) {
|
|
28
|
-
this.logger = config.logger;
|
|
29
|
-
this.pollingInterval = config.pollingInterval;
|
|
30
|
-
this.user = config.user;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private poll(resolve?: () => void, reject?: () => void) {
|
|
34
|
-
if (this.stopped) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const startTime = Date.now();
|
|
39
|
-
this.logger?.debug('Polling for feature flag and segments updates');
|
|
40
|
-
this.requestor.requestData(this.getStoreTimestamp(), this.user, async (err, body) => {
|
|
41
|
-
const elapsed = Date.now() - startTime;
|
|
42
|
-
const sleepFor = Math.max(this.pollingInterval - elapsed, 0);
|
|
43
|
-
|
|
44
|
-
this.logger?.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
|
|
45
|
-
if (err) {
|
|
46
|
-
const {status} = err;
|
|
47
|
-
if (status && !isHttpRecoverable(status)) {
|
|
48
|
-
const message = httpErrorMessage(err, 'polling request');
|
|
49
|
-
this.logger?.error(message);
|
|
50
|
-
this.errorHandler?.(new PollingError(message, status));
|
|
51
|
-
// It is not recoverable, return and do not trigger another
|
|
52
|
-
// poll.
|
|
53
|
-
reject?.();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
this.logger?.warn(httpErrorMessage(err, 'polling request', 'will retry'));
|
|
57
|
-
// Falling through, there was some type of error, we need to trigger
|
|
58
|
-
// a new poll.
|
|
59
|
-
this.timeoutHandle = setTimeout(() => {
|
|
60
|
-
this.poll(resolve, reject);
|
|
61
|
-
}, sleepFor);
|
|
62
|
-
} else {
|
|
63
|
-
let featureFlags = [];
|
|
64
|
-
let userKeyId = this.user?.keyId!;
|
|
65
|
-
let processStreamResponse: ProcessStreamResponse | undefined = this.listeners.get('patch');
|
|
66
|
-
|
|
67
|
-
if (body) {
|
|
68
|
-
const message = JSON.parse(body);
|
|
69
|
-
if (message.messageType === 'data-sync') {
|
|
70
|
-
switch (message.data.eventType) {
|
|
71
|
-
case StreamResponseEventType.patch:
|
|
72
|
-
processStreamResponse = this.listeners.get('patch');
|
|
73
|
-
break;
|
|
74
|
-
case StreamResponseEventType.full:
|
|
75
|
-
processStreamResponse = this.listeners.get('put');
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
({featureFlags, userKeyId} = message.data);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const data = processStreamResponse?.deserializeData?.(featureFlags);
|
|
84
|
-
await processStreamResponse?.processJson?.(userKeyId, data);
|
|
85
|
-
resolve?.();
|
|
86
|
-
// Falling through, there was some type of error, we need to trigger
|
|
87
|
-
// a new poll.
|
|
88
|
-
this.timeoutHandle = setTimeout(() => {
|
|
89
|
-
this.poll();
|
|
90
|
-
}, sleepFor);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async identify(user: IUser): Promise<void> {
|
|
96
|
-
this.user = {...user};
|
|
97
|
-
if (this.timeoutHandle) {
|
|
98
|
-
clearTimeout(this.timeoutHandle);
|
|
99
|
-
this.timeoutHandle = undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
this.poll(resolve, reject);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
close(): void {
|
|
108
|
-
this.stop();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
start(): void {
|
|
112
|
-
this.poll();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
stop(): void {
|
|
116
|
-
if (this.timeoutHandle) {
|
|
117
|
-
clearTimeout(this.timeoutHandle);
|
|
118
|
-
this.timeoutHandle = undefined;
|
|
119
|
-
}
|
|
120
|
-
this.stopped = true;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1
|
+
import { isHttpRecoverable, PollingError } from "../errors";
|
|
2
|
+
import { IDataSynchronizer } from "./IDataSynchronizer";
|
|
3
|
+
import { ILogger } from "../logging/ILogger";
|
|
4
|
+
import Configuration from "../Configuration";
|
|
5
|
+
import { EventName, PollingErrorHandler, ProcessStreamResponse, StreamResponseEventType } from "./types";
|
|
6
|
+
import Requestor from "./Requestor";
|
|
7
|
+
import { httpErrorMessage } from "../utils/http";
|
|
8
|
+
import { IUser } from "../options/IUser";
|
|
9
|
+
|
|
10
|
+
export default class PollingDataSynchronizer implements IDataSynchronizer {
|
|
11
|
+
private stopped = false;
|
|
12
|
+
|
|
13
|
+
private logger?: ILogger;
|
|
14
|
+
|
|
15
|
+
private pollingInterval: number;
|
|
16
|
+
|
|
17
|
+
private user: IUser | undefined;
|
|
18
|
+
|
|
19
|
+
private timeoutHandle: any;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
config: Configuration,
|
|
23
|
+
private readonly requestor: Requestor,
|
|
24
|
+
private readonly getStoreTimestamp: () => number,
|
|
25
|
+
private readonly listeners: Map<EventName, ProcessStreamResponse>,
|
|
26
|
+
private readonly errorHandler?: PollingErrorHandler,
|
|
27
|
+
) {
|
|
28
|
+
this.logger = config.logger;
|
|
29
|
+
this.pollingInterval = config.pollingInterval;
|
|
30
|
+
this.user = config.user;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private poll(resolve?: () => void, reject?: () => void) {
|
|
34
|
+
if (this.stopped) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
this.logger?.debug('Polling for feature flag and segments updates');
|
|
40
|
+
this.requestor.requestData(this.getStoreTimestamp(), this.user, async (err, body) => {
|
|
41
|
+
const elapsed = Date.now() - startTime;
|
|
42
|
+
const sleepFor = Math.max(this.pollingInterval - elapsed, 0);
|
|
43
|
+
|
|
44
|
+
this.logger?.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
|
|
45
|
+
if (err) {
|
|
46
|
+
const {status} = err;
|
|
47
|
+
if (status && !isHttpRecoverable(status)) {
|
|
48
|
+
const message = httpErrorMessage(err, 'polling request');
|
|
49
|
+
this.logger?.error(message);
|
|
50
|
+
this.errorHandler?.(new PollingError(message, status));
|
|
51
|
+
// It is not recoverable, return and do not trigger another
|
|
52
|
+
// poll.
|
|
53
|
+
reject?.();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.logger?.warn(httpErrorMessage(err, 'polling request', 'will retry'));
|
|
57
|
+
// Falling through, there was some type of error, we need to trigger
|
|
58
|
+
// a new poll.
|
|
59
|
+
this.timeoutHandle = setTimeout(() => {
|
|
60
|
+
this.poll(resolve, reject);
|
|
61
|
+
}, sleepFor);
|
|
62
|
+
} else {
|
|
63
|
+
let featureFlags = [];
|
|
64
|
+
let userKeyId = this.user?.keyId!;
|
|
65
|
+
let processStreamResponse: ProcessStreamResponse | undefined = this.listeners.get('patch');
|
|
66
|
+
|
|
67
|
+
if (body) {
|
|
68
|
+
const message = JSON.parse(body);
|
|
69
|
+
if (message.messageType === 'data-sync') {
|
|
70
|
+
switch (message.data.eventType) {
|
|
71
|
+
case StreamResponseEventType.patch:
|
|
72
|
+
processStreamResponse = this.listeners.get('patch');
|
|
73
|
+
break;
|
|
74
|
+
case StreamResponseEventType.full:
|
|
75
|
+
processStreamResponse = this.listeners.get('put');
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
({featureFlags, userKeyId} = message.data);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = processStreamResponse?.deserializeData?.(featureFlags);
|
|
84
|
+
await processStreamResponse?.processJson?.(userKeyId, data);
|
|
85
|
+
resolve?.();
|
|
86
|
+
// Falling through, there was some type of error, we need to trigger
|
|
87
|
+
// a new poll.
|
|
88
|
+
this.timeoutHandle = setTimeout(() => {
|
|
89
|
+
this.poll();
|
|
90
|
+
}, sleepFor);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async identify(user: IUser): Promise<void> {
|
|
96
|
+
this.user = {...user};
|
|
97
|
+
if (this.timeoutHandle) {
|
|
98
|
+
clearTimeout(this.timeoutHandle);
|
|
99
|
+
this.timeoutHandle = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.poll(resolve, reject);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
close(): void {
|
|
108
|
+
this.stop();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
start(): void {
|
|
112
|
+
this.poll();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stop(): void {
|
|
116
|
+
if (this.timeoutHandle) {
|
|
117
|
+
clearTimeout(this.timeoutHandle);
|
|
118
|
+
this.timeoutHandle = undefined;
|
|
119
|
+
}
|
|
120
|
+
this.stopped = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { IRequestor } from "./IRequestor";
|
|
2
|
-
import Configuration from "../Configuration";
|
|
3
|
-
import { IInfo } from "../platform/IInfo";
|
|
4
|
-
import { IRequestOptions, IRequests, IResponse } from "../platform/requests";
|
|
5
|
-
import { StreamingError } from "../errors";
|
|
6
|
-
import { defaultHeaders } from "../utils/http";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @internal
|
|
10
|
-
*/
|
|
11
|
-
export default class Requestor implements IRequestor {
|
|
12
|
-
private readonly headers: Record<string, string>;
|
|
13
|
-
|
|
14
|
-
private readonly uri: string;
|
|
15
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
sdkKey: string,
|
|
18
|
-
config: Configuration,
|
|
19
|
-
info: IInfo,
|
|
20
|
-
private readonly requests: IRequests,
|
|
21
|
-
) {
|
|
22
|
-
this.headers = defaultHeaders(sdkKey, info);
|
|
23
|
-
this.uri = config.pollingUri;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Perform a request and utilize the ETag cache. The ETags are cached in the
|
|
28
|
-
* requestor instance.
|
|
29
|
-
*/
|
|
30
|
-
private async request(
|
|
31
|
-
requestUrl: string,
|
|
32
|
-
options: IRequestOptions,
|
|
33
|
-
): Promise<{
|
|
34
|
-
res: IResponse;
|
|
35
|
-
body: string;
|
|
36
|
-
}> {
|
|
37
|
-
const res = await this.requests.fetch(requestUrl, options);
|
|
38
|
-
|
|
39
|
-
const body = await res.text();
|
|
40
|
-
|
|
41
|
-
return {res, body};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async requestData(timestamp: number, payload: any, cb: (err: any, body: any) => void) {
|
|
45
|
-
const options: IRequestOptions = {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: this.headers,
|
|
48
|
-
body: JSON.stringify(payload)
|
|
49
|
-
};
|
|
50
|
-
try {
|
|
51
|
-
const {res, body} = await this.request(`${ this.uri }?timestamp=${ timestamp ?? 0 }`, options);
|
|
52
|
-
if (res.status !== 200 && res.status !== 304) {
|
|
53
|
-
const err = new StreamingError(`Unexpected status code: ${ res.status }`, res.status);
|
|
54
|
-
return cb(err, undefined);
|
|
55
|
-
}
|
|
56
|
-
return cb(undefined, res.status === 304 ? null : body);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
return cb(err, undefined);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
import { IRequestor } from "./IRequestor";
|
|
2
|
+
import Configuration from "../Configuration";
|
|
3
|
+
import { IInfo } from "../platform/IInfo";
|
|
4
|
+
import { IRequestOptions, IRequests, IResponse } from "../platform/requests";
|
|
5
|
+
import { StreamingError } from "../errors";
|
|
6
|
+
import { defaultHeaders } from "../utils/http";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export default class Requestor implements IRequestor {
|
|
12
|
+
private readonly headers: Record<string, string>;
|
|
13
|
+
|
|
14
|
+
private readonly uri: string;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
sdkKey: string,
|
|
18
|
+
config: Configuration,
|
|
19
|
+
info: IInfo,
|
|
20
|
+
private readonly requests: IRequests,
|
|
21
|
+
) {
|
|
22
|
+
this.headers = defaultHeaders(sdkKey, info);
|
|
23
|
+
this.uri = config.pollingUri;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Perform a request and utilize the ETag cache. The ETags are cached in the
|
|
28
|
+
* requestor instance.
|
|
29
|
+
*/
|
|
30
|
+
private async request(
|
|
31
|
+
requestUrl: string,
|
|
32
|
+
options: IRequestOptions,
|
|
33
|
+
): Promise<{
|
|
34
|
+
res: IResponse;
|
|
35
|
+
body: string;
|
|
36
|
+
}> {
|
|
37
|
+
const res = await this.requests.fetch(requestUrl, options);
|
|
38
|
+
|
|
39
|
+
const body = await res.text();
|
|
40
|
+
|
|
41
|
+
return {res, body};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async requestData(timestamp: number, payload: any, cb: (err: any, body: any) => void) {
|
|
45
|
+
const options: IRequestOptions = {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: this.headers,
|
|
48
|
+
body: JSON.stringify(payload)
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const {res, body} = await this.request(`${ this.uri }?timestamp=${ timestamp ?? 0 }`, options);
|
|
52
|
+
if (res.status !== 200 && res.status !== 304) {
|
|
53
|
+
const err = new StreamingError(`Unexpected status code: ${ res.status }`, res.status);
|
|
54
|
+
return cb(err, undefined);
|
|
55
|
+
}
|
|
56
|
+
return cb(undefined, res.status === 304 ? null : body);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return cb(err, undefined);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { IDataSynchronizer } from "./IDataSynchronizer";
|
|
2
|
-
import ClientContext from "../options/ClientContext";
|
|
3
|
-
import { EventName, ProcessStreamResponse } from "./types";
|
|
4
|
-
import { ILogger } from "../logging/ILogger";
|
|
5
|
-
import { IWebSocketWithEvents } from "../platform/IWebSocket";
|
|
6
|
-
import { IUser } from "../options/IUser";
|
|
7
|
-
|
|
8
|
-
class WebSocketDataSynchronizer implements IDataSynchronizer {
|
|
9
|
-
private socket?: IWebSocketWithEvents;
|
|
10
|
-
private readonly logger?: ILogger;
|
|
11
|
-
private identifyResolve?: () => void;
|
|
12
|
-
|
|
13
|
-
private connectionAttemptStartTime?: number;
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
sdkKey: string,
|
|
17
|
-
user: IUser,
|
|
18
|
-
clientContext: ClientContext,
|
|
19
|
-
socket: IWebSocketWithEvents,
|
|
20
|
-
private readonly getStoreTimestamp: () => number,
|
|
21
|
-
private readonly listeners: Map<EventName, ProcessStreamResponse>,
|
|
22
|
-
webSocketPingInterval: number
|
|
23
|
-
) {
|
|
24
|
-
const {logger, streamingUri} = clientContext;
|
|
25
|
-
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
this.socket = socket;
|
|
28
|
-
this.socket.config({
|
|
29
|
-
sdkKey,
|
|
30
|
-
streamingUri,
|
|
31
|
-
pingInterval: webSocketPingInterval,
|
|
32
|
-
user,
|
|
33
|
-
logger,
|
|
34
|
-
getStoreTimestamp
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
this.listeners.forEach(({deserializeData, processJson}, eventName) => {
|
|
38
|
-
this.socket?.addListener(eventName, async (event) => {
|
|
39
|
-
this.logger?.debug(`Received ${ eventName } event`);
|
|
40
|
-
|
|
41
|
-
if (event?.data) {
|
|
42
|
-
const {featureFlags, userKeyId} = event.data;
|
|
43
|
-
const data = deserializeData(featureFlags);
|
|
44
|
-
await processJson(userKeyId, data);
|
|
45
|
-
this.identifyResolve?.();
|
|
46
|
-
this.identifyResolve = undefined;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async identify(user: IUser): Promise<void> {
|
|
53
|
-
this.socket?.identify(user);
|
|
54
|
-
return new Promise(resolve => this.identifyResolve = resolve);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
start(): void {
|
|
58
|
-
this.logConnectionStarted();
|
|
59
|
-
|
|
60
|
-
this.socket?.connect();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private logConnectionStarted() {
|
|
64
|
-
this.connectionAttemptStartTime = Date.now();
|
|
65
|
-
this.logger?.info(`Stream connection attempt StartTime ${ this.connectionAttemptStartTime }`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
close(): void {
|
|
69
|
-
this.stop();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
stop(): void {
|
|
73
|
-
this.socket?.close();
|
|
74
|
-
this.socket = undefined;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
1
|
+
import { IDataSynchronizer } from "./IDataSynchronizer";
|
|
2
|
+
import ClientContext from "../options/ClientContext";
|
|
3
|
+
import { EventName, ProcessStreamResponse } from "./types";
|
|
4
|
+
import { ILogger } from "../logging/ILogger";
|
|
5
|
+
import { IWebSocketWithEvents } from "../platform/IWebSocket";
|
|
6
|
+
import { IUser } from "../options/IUser";
|
|
7
|
+
|
|
8
|
+
class WebSocketDataSynchronizer implements IDataSynchronizer {
|
|
9
|
+
private socket?: IWebSocketWithEvents;
|
|
10
|
+
private readonly logger?: ILogger;
|
|
11
|
+
private identifyResolve?: () => void;
|
|
12
|
+
|
|
13
|
+
private connectionAttemptStartTime?: number;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
sdkKey: string,
|
|
17
|
+
user: IUser,
|
|
18
|
+
clientContext: ClientContext,
|
|
19
|
+
socket: IWebSocketWithEvents,
|
|
20
|
+
private readonly getStoreTimestamp: () => number,
|
|
21
|
+
private readonly listeners: Map<EventName, ProcessStreamResponse>,
|
|
22
|
+
webSocketPingInterval: number
|
|
23
|
+
) {
|
|
24
|
+
const {logger, streamingUri} = clientContext;
|
|
25
|
+
|
|
26
|
+
this.logger = logger;
|
|
27
|
+
this.socket = socket;
|
|
28
|
+
this.socket.config({
|
|
29
|
+
sdkKey,
|
|
30
|
+
streamingUri,
|
|
31
|
+
pingInterval: webSocketPingInterval,
|
|
32
|
+
user,
|
|
33
|
+
logger,
|
|
34
|
+
getStoreTimestamp
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.listeners.forEach(({deserializeData, processJson}, eventName) => {
|
|
38
|
+
this.socket?.addListener(eventName, async (event) => {
|
|
39
|
+
this.logger?.debug(`Received ${ eventName } event`);
|
|
40
|
+
|
|
41
|
+
if (event?.data) {
|
|
42
|
+
const {featureFlags, userKeyId} = event.data;
|
|
43
|
+
const data = deserializeData(featureFlags);
|
|
44
|
+
await processJson(userKeyId, data);
|
|
45
|
+
this.identifyResolve?.();
|
|
46
|
+
this.identifyResolve = undefined;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async identify(user: IUser): Promise<void> {
|
|
53
|
+
this.socket?.identify(user);
|
|
54
|
+
return new Promise(resolve => this.identifyResolve = resolve);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
start(): void {
|
|
58
|
+
this.logConnectionStarted();
|
|
59
|
+
|
|
60
|
+
this.socket?.connect();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private logConnectionStarted() {
|
|
64
|
+
this.connectionAttemptStartTime = Date.now();
|
|
65
|
+
this.logger?.info(`Stream connection attempt StartTime ${ this.connectionAttemptStartTime }`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
close(): void {
|
|
69
|
+
this.stop();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stop(): void {
|
|
73
|
+
this.socket?.close();
|
|
74
|
+
this.socket = undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
78
|
export default WebSocketDataSynchronizer;
|
package/src/data-sync/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export * from './DataSyncMode';
|
|
2
|
-
export * from './IDataSynchronizer';
|
|
3
|
-
export * from './IRequestor';
|
|
4
|
-
export * from './NullDataSynchronizer';
|
|
5
|
-
export * from './PollingDataSynchronizer';
|
|
6
|
-
export * from './Requestor';
|
|
7
|
-
export * from './types';
|
|
8
|
-
export * from './utils';
|
|
1
|
+
export * from './DataSyncMode';
|
|
2
|
+
export * from './IDataSynchronizer';
|
|
3
|
+
export * from './IRequestor';
|
|
4
|
+
export * from './NullDataSynchronizer';
|
|
5
|
+
export * from './PollingDataSynchronizer';
|
|
6
|
+
export * from './Requestor';
|
|
7
|
+
export * from './types';
|
|
8
|
+
export * from './utils';
|
|
9
9
|
export * from './WebSocketDataSynchronizer';
|
package/src/data-sync/types.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { PollingError } from "../errors";
|
|
2
|
-
import { IFlag } from "../evaluation/data/IFlag";
|
|
3
|
-
|
|
4
|
-
export type PollingErrorHandler = (err: PollingError) => void;
|
|
5
|
-
|
|
6
|
-
export enum StreamResponseEventType {
|
|
7
|
-
full = 'full',
|
|
8
|
-
patch = 'patch'
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface IStreamResponse {
|
|
12
|
-
eventType: StreamResponseEventType,
|
|
13
|
-
featureFlags: IFlag[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type EventName = 'delete' | 'patch' | 'ping' | 'put';
|
|
17
|
-
export type ProcessStreamResponse = {
|
|
18
|
-
deserializeData: (flags: IFlag[]) => any;
|
|
19
|
-
processJson: (userKeyId: string, json: any) => Promise<void>;
|
|
1
|
+
import { PollingError } from "../errors";
|
|
2
|
+
import { IFlag } from "../evaluation/data/IFlag";
|
|
3
|
+
|
|
4
|
+
export type PollingErrorHandler = (err: PollingError) => void;
|
|
5
|
+
|
|
6
|
+
export enum StreamResponseEventType {
|
|
7
|
+
full = 'full',
|
|
8
|
+
patch = 'patch'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IStreamResponse {
|
|
12
|
+
eventType: StreamResponseEventType,
|
|
13
|
+
featureFlags: IFlag[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type EventName = 'delete' | 'patch' | 'ping' | 'put';
|
|
17
|
+
export type ProcessStreamResponse = {
|
|
18
|
+
deserializeData: (flags: IFlag[]) => any;
|
|
19
|
+
processJson: (userKeyId: string, json: any) => Promise<void>;
|
|
20
20
|
};
|
package/src/data-sync/utils.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/********************** encode text begin *****************************/
|
|
2
|
-
const alphabet: Record<string, string> = {
|
|
3
|
-
"0": "Q",
|
|
4
|
-
"1": "B",
|
|
5
|
-
"2": "W",
|
|
6
|
-
"3": "S",
|
|
7
|
-
"4": "P",
|
|
8
|
-
"5": "H",
|
|
9
|
-
"6": "D",
|
|
10
|
-
"7": "X",
|
|
11
|
-
"8": "Z",
|
|
12
|
-
"9": "U",
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function encodeNumber(param: number, length: number): string {
|
|
16
|
-
var s = "000000000000" + param;
|
|
17
|
-
const numberWithLeadingZeros = s.slice(s.length - length);
|
|
18
|
-
return numberWithLeadingZeros.split('').map(n => alphabet[n]).join('');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// generate connection token
|
|
22
|
-
export function generateConnectionToken(text: string): string {
|
|
23
|
-
text = text.replace(/=*$/, '');
|
|
24
|
-
const timestamp = Date.now();
|
|
25
|
-
const timestampCode = encodeNumber(timestamp, timestamp.toString().length);
|
|
26
|
-
// get random number less than the length of the text as the start point, and it must be greater or equal to 2
|
|
27
|
-
const start = Math.max(Math.floor(Math.random() * text.length), 2);
|
|
28
|
-
|
|
29
|
-
return `${ encodeNumber(start, 3) }${ encodeNumber(timestampCode.length, 2) }${ text.slice(0, start) }${ timestampCode }${ text.slice(start) }`;
|
|
30
|
-
}
|
|
31
|
-
|
|
1
|
+
/********************** encode text begin *****************************/
|
|
2
|
+
const alphabet: Record<string, string> = {
|
|
3
|
+
"0": "Q",
|
|
4
|
+
"1": "B",
|
|
5
|
+
"2": "W",
|
|
6
|
+
"3": "S",
|
|
7
|
+
"4": "P",
|
|
8
|
+
"5": "H",
|
|
9
|
+
"6": "D",
|
|
10
|
+
"7": "X",
|
|
11
|
+
"8": "Z",
|
|
12
|
+
"9": "U",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function encodeNumber(param: number, length: number): string {
|
|
16
|
+
var s = "000000000000" + param;
|
|
17
|
+
const numberWithLeadingZeros = s.slice(s.length - length);
|
|
18
|
+
return numberWithLeadingZeros.split('').map(n => alphabet[n]).join('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// generate connection token
|
|
22
|
+
export function generateConnectionToken(text: string): string {
|
|
23
|
+
text = text.replace(/=*$/, '');
|
|
24
|
+
const timestamp = Date.now();
|
|
25
|
+
const timestampCode = encodeNumber(timestamp, timestamp.toString().length);
|
|
26
|
+
// get random number less than the length of the text as the start point, and it must be greater or equal to 2
|
|
27
|
+
const start = Math.max(Math.floor(Math.random() * text.length), 2);
|
|
28
|
+
|
|
29
|
+
return `${ encodeNumber(start, 3) }${ encodeNumber(timestampCode.length, 2) }${ text.slice(0, start) }${ timestampCode }${ text.slice(start) }`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
32
|
/********************** encode text end *****************************/
|