@featbit/js-client-sdk 3.0.11 → 3.0.13
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.map +1 -1
- package/dist/esm/FbClientCore.js +1 -1
- package/dist/esm/FbClientCore.js.map +1 -1
- package/dist/esm/data-sources/DataSourceUpdates.d.ts +2 -2
- package/dist/esm/data-sources/DataSourceUpdates.d.ts.map +1 -1
- package/dist/esm/data-sources/DataSourceUpdates.js +55 -51
- package/dist/esm/data-sources/DataSourceUpdates.js.map +1 -1
- package/dist/esm/data-sources/createStreamListeners.d.ts +0 -9
- package/dist/esm/data-sources/createStreamListeners.d.ts.map +1 -1
- package/dist/esm/data-sources/createStreamListeners.js +10 -10
- package/dist/esm/data-sources/createStreamListeners.js.map +1 -1
- package/dist/esm/data-sync/IDataSynchronizer.d.ts +1 -1
- package/dist/esm/data-sync/IDataSynchronizer.d.ts.map +1 -1
- package/dist/esm/data-sync/NullDataSynchronizer.d.ts +1 -1
- package/dist/esm/data-sync/NullDataSynchronizer.d.ts.map +1 -1
- package/dist/esm/data-sync/NullDataSynchronizer.js +11 -0
- package/dist/esm/data-sync/NullDataSynchronizer.js.map +1 -1
- package/dist/esm/data-sync/PollingDataSynchronizer.d.ts +1 -1
- package/dist/esm/data-sync/PollingDataSynchronizer.d.ts.map +1 -1
- package/dist/esm/data-sync/PollingDataSynchronizer.js +42 -17
- package/dist/esm/data-sync/PollingDataSynchronizer.js.map +1 -1
- package/dist/esm/data-sync/WebSocketDataSynchronizer.d.ts +2 -1
- package/dist/esm/data-sync/WebSocketDataSynchronizer.d.ts.map +1 -1
- package/dist/esm/data-sync/WebSocketDataSynchronizer.js +20 -6
- package/dist/esm/data-sync/WebSocketDataSynchronizer.js.map +1 -1
- package/dist/esm/data-sync/types.d.ts +1 -1
- package/dist/esm/data-sync/types.d.ts.map +1 -1
- package/dist/esm/integrations/test_data/TestDataSynchronizer.d.ts +1 -1
- package/dist/esm/integrations/test_data/TestDataSynchronizer.d.ts.map +1 -1
- package/dist/esm/integrations/test_data/TestDataSynchronizer.js +3 -1
- package/dist/esm/integrations/test_data/TestDataSynchronizer.js.map +1 -1
- package/dist/esm/platform/browser/BrowserWebSocket.d.ts.map +1 -1
- package/dist/esm/platform/browser/BrowserWebSocket.js +4 -0
- package/dist/esm/platform/browser/BrowserWebSocket.js.map +1 -1
- package/dist/esm/store/IDataSourceUpdates.d.ts +2 -2
- package/dist/esm/store/IDataSourceUpdates.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/{featbit-js-client-sdk-3.0.11.js → featbit-js-client-sdk-3.0.13.js} +2 -2
- package/dist/umd/featbit-js-client-sdk-3.0.13.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 +405 -401
- package/src/IContextProperty.ts +3 -3
- package/src/IDataKind.ts +11 -11
- package/src/IFbClient.ts +29 -29
- package/src/IFbClientCore.ts +290 -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 -66
- 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 -111
- package/src/data-sync/Requestor.ts +61 -61
- package/src/data-sync/WebSocketDataSynchronizer.ts +77 -73
- 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 -142
- 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.11.js.map +0 -1
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import { DeliveryStatus, IEventSender, IEventSenderResult } from "./IEventSender";
|
|
2
|
-
import ClientContext from "../options/ClientContext";
|
|
3
|
-
import { defaultHeaders, httpErrorMessage } from "../utils/http";
|
|
4
|
-
import { IRequests } from "../platform/requests";
|
|
5
|
-
import { isHttpRecoverable, UnexpectedResponseError } from "../errors";
|
|
6
|
-
import sleep from "../utils/sleep";
|
|
7
|
-
|
|
8
|
-
export class DefaultEventSender implements IEventSender {
|
|
9
|
-
private readonly defaultHeaders: {
|
|
10
|
-
[key: string]: string;
|
|
11
|
-
};
|
|
12
|
-
private readonly eventsUri: string;
|
|
13
|
-
private requests: IRequests;
|
|
14
|
-
|
|
15
|
-
constructor(clientContext: ClientContext) {
|
|
16
|
-
const {
|
|
17
|
-
sdkKey,
|
|
18
|
-
eventsUri,
|
|
19
|
-
platform
|
|
20
|
-
} = clientContext;
|
|
21
|
-
|
|
22
|
-
const {info, requests} = platform;
|
|
23
|
-
this.defaultHeaders = defaultHeaders(sdkKey, info);
|
|
24
|
-
this.eventsUri = eventsUri;
|
|
25
|
-
this.requests = requests;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async send(payload: string, retry: boolean): Promise<IEventSenderResult> {
|
|
29
|
-
const res: IEventSenderResult = {
|
|
30
|
-
status: DeliveryStatus.Succeeded,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const headers: Record<string, string> = {
|
|
34
|
-
...this.defaultHeaders,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let error;
|
|
38
|
-
try {
|
|
39
|
-
const {status} = await this.requests.fetch(this.eventsUri, {
|
|
40
|
-
headers,
|
|
41
|
-
body: payload,
|
|
42
|
-
method: 'POST',
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (status >= 200 && status <= 299) {
|
|
46
|
-
return res;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
error = new UnexpectedResponseError(
|
|
50
|
-
httpErrorMessage({status, message: 'some events were dropped'}, 'event posting'),
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
if (!isHttpRecoverable(status)) {
|
|
54
|
-
res.status = DeliveryStatus.FailedAndMustShutDown;
|
|
55
|
-
res.error = error;
|
|
56
|
-
return res;
|
|
57
|
-
}
|
|
58
|
-
} catch (err) {
|
|
59
|
-
error = err;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// recoverable but not retrying
|
|
63
|
-
if (error && !retry) {
|
|
64
|
-
res.status = DeliveryStatus.Failed;
|
|
65
|
-
res.error = error;
|
|
66
|
-
return res;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// wait 1 second before retrying
|
|
70
|
-
await sleep();
|
|
71
|
-
|
|
72
|
-
return this.send(payload, false);
|
|
73
|
-
}
|
|
1
|
+
import { DeliveryStatus, IEventSender, IEventSenderResult } from "./IEventSender";
|
|
2
|
+
import ClientContext from "../options/ClientContext";
|
|
3
|
+
import { defaultHeaders, httpErrorMessage } from "../utils/http";
|
|
4
|
+
import { IRequests } from "../platform/requests";
|
|
5
|
+
import { isHttpRecoverable, UnexpectedResponseError } from "../errors";
|
|
6
|
+
import sleep from "../utils/sleep";
|
|
7
|
+
|
|
8
|
+
export class DefaultEventSender implements IEventSender {
|
|
9
|
+
private readonly defaultHeaders: {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
};
|
|
12
|
+
private readonly eventsUri: string;
|
|
13
|
+
private requests: IRequests;
|
|
14
|
+
|
|
15
|
+
constructor(clientContext: ClientContext) {
|
|
16
|
+
const {
|
|
17
|
+
sdkKey,
|
|
18
|
+
eventsUri,
|
|
19
|
+
platform
|
|
20
|
+
} = clientContext;
|
|
21
|
+
|
|
22
|
+
const {info, requests} = platform;
|
|
23
|
+
this.defaultHeaders = defaultHeaders(sdkKey, info);
|
|
24
|
+
this.eventsUri = eventsUri;
|
|
25
|
+
this.requests = requests;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async send(payload: string, retry: boolean): Promise<IEventSenderResult> {
|
|
29
|
+
const res: IEventSenderResult = {
|
|
30
|
+
status: DeliveryStatus.Succeeded,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const headers: Record<string, string> = {
|
|
34
|
+
...this.defaultHeaders,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let error;
|
|
38
|
+
try {
|
|
39
|
+
const {status} = await this.requests.fetch(this.eventsUri, {
|
|
40
|
+
headers,
|
|
41
|
+
body: payload,
|
|
42
|
+
method: 'POST',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (status >= 200 && status <= 299) {
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
error = new UnexpectedResponseError(
|
|
50
|
+
httpErrorMessage({status, message: 'some events were dropped'}, 'event posting'),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!isHttpRecoverable(status)) {
|
|
54
|
+
res.status = DeliveryStatus.FailedAndMustShutDown;
|
|
55
|
+
res.error = error;
|
|
56
|
+
return res;
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
error = err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// recoverable but not retrying
|
|
63
|
+
if (error && !retry) {
|
|
64
|
+
res.status = DeliveryStatus.Failed;
|
|
65
|
+
res.error = error;
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// wait 1 second before retrying
|
|
70
|
+
await sleep();
|
|
71
|
+
|
|
72
|
+
return this.send(payload, false);
|
|
73
|
+
}
|
|
74
74
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { IEventSerializer } from "./EventSerializer";
|
|
2
|
-
import { EvalEvent, IEvent, MetricEvent } from "./event";
|
|
3
|
-
|
|
4
|
-
export class DefaultEventSerializer implements IEventSerializer {
|
|
5
|
-
serialize(events: IEvent[]): string {
|
|
6
|
-
const payload = events
|
|
7
|
-
.map(event => event instanceof EvalEvent || event instanceof MetricEvent ? event.toPayload() : null)
|
|
8
|
-
.filter(event => event !== null);
|
|
9
|
-
|
|
10
|
-
return JSON.stringify(payload);
|
|
11
|
-
}
|
|
1
|
+
import { IEventSerializer } from "./EventSerializer";
|
|
2
|
+
import { EvalEvent, IEvent, MetricEvent } from "./event";
|
|
3
|
+
|
|
4
|
+
export class DefaultEventSerializer implements IEventSerializer {
|
|
5
|
+
serialize(events: IEvent[]): string {
|
|
6
|
+
const payload = events
|
|
7
|
+
.map(event => event instanceof EvalEvent || event instanceof MetricEvent ? event.toPayload() : null)
|
|
8
|
+
.filter(event => event !== null);
|
|
9
|
+
|
|
10
|
+
return JSON.stringify(payload);
|
|
11
|
+
}
|
|
12
12
|
}
|
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
import { ILogger } from "../logging/ILogger";
|
|
2
|
-
import ClientContext from "../options/ClientContext";
|
|
3
|
-
import { DeliveryStatus, IEventSender } from "./IEventSender";
|
|
4
|
-
import { IEventQueue } from "./IEventQueue";
|
|
5
|
-
import { DefaultEventQueue } from "./DefaultEventQueue";
|
|
6
|
-
import { DefaultEventSender } from "./DefaultEventSender";
|
|
7
|
-
import { AsyncEvent, FlushEvent, IEvent, PayloadEvent, ShutdownEvent } from "./event";
|
|
8
|
-
import { IEventSerializer } from "./EventSerializer";
|
|
9
|
-
import { DefaultEventSerializer } from "./DefaultEventSerializer";
|
|
10
|
-
import sleep from "../utils/sleep";
|
|
11
|
-
|
|
12
|
-
export class EventDispatcher {
|
|
13
|
-
private readonly logger: ILogger;
|
|
14
|
-
private sender: IEventSender;
|
|
15
|
-
private buffer: IEventQueue;
|
|
16
|
-
private serializer: IEventSerializer;
|
|
17
|
-
|
|
18
|
-
private maxEventPerRequest = 50;
|
|
19
|
-
private stopped: boolean = false;
|
|
20
|
-
|
|
21
|
-
constructor(clientContext: ClientContext, queue: IEventQueue) {
|
|
22
|
-
const {logger, maxEventsInQueue} = clientContext;
|
|
23
|
-
this.logger = logger!;
|
|
24
|
-
|
|
25
|
-
this.buffer = new DefaultEventQueue(maxEventsInQueue, this.logger);
|
|
26
|
-
this.sender = new DefaultEventSender(clientContext);
|
|
27
|
-
this.serializer = new DefaultEventSerializer();
|
|
28
|
-
|
|
29
|
-
this.dispatchLoop(queue).then();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private async dispatchLoop(queue: IEventQueue) {
|
|
33
|
-
this.logger.debug('Start dispatch loop.');
|
|
34
|
-
|
|
35
|
-
let running = true;
|
|
36
|
-
while (running) {
|
|
37
|
-
try {
|
|
38
|
-
const event = queue.shift();
|
|
39
|
-
|
|
40
|
-
if (event === undefined) {
|
|
41
|
-
await sleep(1000);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (event instanceof PayloadEvent) {
|
|
46
|
-
this.addEventToBuffer(event);
|
|
47
|
-
} else if (event instanceof FlushEvent) {
|
|
48
|
-
await this.triggerFlush(event);
|
|
49
|
-
} else if (event instanceof ShutdownEvent) {
|
|
50
|
-
await this.triggerFlush(event);
|
|
51
|
-
this.stopped = true;
|
|
52
|
-
running = false;
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
this.logger.error('Unexpected error in event dispatcher.', err);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.logger.debug('Finish dispatch loop.');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private addEventToBuffer(event: IEvent) {
|
|
63
|
-
if (this.stopped) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (this.buffer.addEvent(event)) {
|
|
68
|
-
this.logger.debug('Added event to buffer.');
|
|
69
|
-
} else {
|
|
70
|
-
this.logger.warn('Exceeded event queue capacity, event will be dropped. Increase capacity to avoid dropping events.');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private async triggerFlush(event: AsyncEvent) {
|
|
75
|
-
if (this.stopped) {
|
|
76
|
-
event.complete();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (this.buffer.isEmpty) {
|
|
81
|
-
event.complete();
|
|
82
|
-
this.logger.debug('Flush empty buffer.');
|
|
83
|
-
// There are no events to flush. If we don't complete the message, then the async task may never
|
|
84
|
-
// complete (if it had a non-zero positive timeout, then it would complete after the timeout).
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const snapshot = this.buffer.eventsSnapshot;
|
|
89
|
-
this.buffer.clear();
|
|
90
|
-
try {
|
|
91
|
-
await this.flushEvents(snapshot);
|
|
92
|
-
this.logger.debug(`${ snapshot.length } events has been flushed.`);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
this.logger.warn('Exception happened when flushing events', err);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
event.complete();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private async flushEvents(events: IEvent[]) {
|
|
101
|
-
events = this.getUniqueEvents(events);
|
|
102
|
-
const total = events.length;
|
|
103
|
-
for (let i = 0; i < total; i += this.maxEventPerRequest) {
|
|
104
|
-
const length = Math.min(this.maxEventPerRequest, total - i);
|
|
105
|
-
const slice = events.slice(i, i + length);
|
|
106
|
-
const payload = this.serializer.serialize(slice);
|
|
107
|
-
|
|
108
|
-
const {status} = await this.sender.send(payload, true);
|
|
109
|
-
if (status === DeliveryStatus.FailedAndMustShutDown) {
|
|
110
|
-
this.stopped = true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private getUniqueEvents(events: IEvent[]): IEvent[] {
|
|
116
|
-
const uniqueEvents: IEvent[] = [];
|
|
117
|
-
const hashes: string[] = [];
|
|
118
|
-
|
|
119
|
-
for (const event of events) {
|
|
120
|
-
if (!hashes.includes(event.hash)) {
|
|
121
|
-
uniqueEvents.push(event);
|
|
122
|
-
hashes.push(event.hash);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return uniqueEvents;
|
|
127
|
-
}
|
|
1
|
+
import { ILogger } from "../logging/ILogger";
|
|
2
|
+
import ClientContext from "../options/ClientContext";
|
|
3
|
+
import { DeliveryStatus, IEventSender } from "./IEventSender";
|
|
4
|
+
import { IEventQueue } from "./IEventQueue";
|
|
5
|
+
import { DefaultEventQueue } from "./DefaultEventQueue";
|
|
6
|
+
import { DefaultEventSender } from "./DefaultEventSender";
|
|
7
|
+
import { AsyncEvent, FlushEvent, IEvent, PayloadEvent, ShutdownEvent } from "./event";
|
|
8
|
+
import { IEventSerializer } from "./EventSerializer";
|
|
9
|
+
import { DefaultEventSerializer } from "./DefaultEventSerializer";
|
|
10
|
+
import sleep from "../utils/sleep";
|
|
11
|
+
|
|
12
|
+
export class EventDispatcher {
|
|
13
|
+
private readonly logger: ILogger;
|
|
14
|
+
private sender: IEventSender;
|
|
15
|
+
private buffer: IEventQueue;
|
|
16
|
+
private serializer: IEventSerializer;
|
|
17
|
+
|
|
18
|
+
private maxEventPerRequest = 50;
|
|
19
|
+
private stopped: boolean = false;
|
|
20
|
+
|
|
21
|
+
constructor(clientContext: ClientContext, queue: IEventQueue) {
|
|
22
|
+
const {logger, maxEventsInQueue} = clientContext;
|
|
23
|
+
this.logger = logger!;
|
|
24
|
+
|
|
25
|
+
this.buffer = new DefaultEventQueue(maxEventsInQueue, this.logger);
|
|
26
|
+
this.sender = new DefaultEventSender(clientContext);
|
|
27
|
+
this.serializer = new DefaultEventSerializer();
|
|
28
|
+
|
|
29
|
+
this.dispatchLoop(queue).then();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async dispatchLoop(queue: IEventQueue) {
|
|
33
|
+
this.logger.debug('Start dispatch loop.');
|
|
34
|
+
|
|
35
|
+
let running = true;
|
|
36
|
+
while (running) {
|
|
37
|
+
try {
|
|
38
|
+
const event = queue.shift();
|
|
39
|
+
|
|
40
|
+
if (event === undefined) {
|
|
41
|
+
await sleep(1000);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (event instanceof PayloadEvent) {
|
|
46
|
+
this.addEventToBuffer(event);
|
|
47
|
+
} else if (event instanceof FlushEvent) {
|
|
48
|
+
await this.triggerFlush(event);
|
|
49
|
+
} else if (event instanceof ShutdownEvent) {
|
|
50
|
+
await this.triggerFlush(event);
|
|
51
|
+
this.stopped = true;
|
|
52
|
+
running = false;
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
this.logger.error('Unexpected error in event dispatcher.', err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.logger.debug('Finish dispatch loop.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private addEventToBuffer(event: IEvent) {
|
|
63
|
+
if (this.stopped) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.buffer.addEvent(event)) {
|
|
68
|
+
this.logger.debug('Added event to buffer.');
|
|
69
|
+
} else {
|
|
70
|
+
this.logger.warn('Exceeded event queue capacity, event will be dropped. Increase capacity to avoid dropping events.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async triggerFlush(event: AsyncEvent) {
|
|
75
|
+
if (this.stopped) {
|
|
76
|
+
event.complete();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.buffer.isEmpty) {
|
|
81
|
+
event.complete();
|
|
82
|
+
this.logger.debug('Flush empty buffer.');
|
|
83
|
+
// There are no events to flush. If we don't complete the message, then the async task may never
|
|
84
|
+
// complete (if it had a non-zero positive timeout, then it would complete after the timeout).
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const snapshot = this.buffer.eventsSnapshot;
|
|
89
|
+
this.buffer.clear();
|
|
90
|
+
try {
|
|
91
|
+
await this.flushEvents(snapshot);
|
|
92
|
+
this.logger.debug(`${ snapshot.length } events has been flushed.`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.logger.warn('Exception happened when flushing events', err);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
event.complete();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async flushEvents(events: IEvent[]) {
|
|
101
|
+
events = this.getUniqueEvents(events);
|
|
102
|
+
const total = events.length;
|
|
103
|
+
for (let i = 0; i < total; i += this.maxEventPerRequest) {
|
|
104
|
+
const length = Math.min(this.maxEventPerRequest, total - i);
|
|
105
|
+
const slice = events.slice(i, i + length);
|
|
106
|
+
const payload = this.serializer.serialize(slice);
|
|
107
|
+
|
|
108
|
+
const {status} = await this.sender.send(payload, true);
|
|
109
|
+
if (status === DeliveryStatus.FailedAndMustShutDown) {
|
|
110
|
+
this.stopped = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private getUniqueEvents(events: IEvent[]): IEvent[] {
|
|
116
|
+
const uniqueEvents: IEvent[] = [];
|
|
117
|
+
const hashes: string[] = [];
|
|
118
|
+
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
if (!hashes.includes(event.hash)) {
|
|
121
|
+
uniqueEvents.push(event);
|
|
122
|
+
hashes.push(event.hash);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return uniqueEvents;
|
|
127
|
+
}
|
|
128
128
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IEvent } from "./event";
|
|
2
|
-
|
|
3
|
-
export interface IEventSerializer {
|
|
4
|
-
serialize(events: IEvent[]): string;
|
|
1
|
+
import { IEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export interface IEventSerializer {
|
|
4
|
+
serialize(events: IEvent[]): string;
|
|
5
5
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { IEvent } from "./event";
|
|
2
|
-
|
|
3
|
-
export interface IEventProcessor {
|
|
4
|
-
close(): Promise<void>;
|
|
5
|
-
|
|
6
|
-
flush(): Promise<void>;
|
|
7
|
-
|
|
8
|
-
record(event: IEvent | null): boolean;
|
|
1
|
+
import { IEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export interface IEventProcessor {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
|
|
6
|
+
flush(): Promise<void>;
|
|
7
|
+
|
|
8
|
+
record(event: IEvent | null): boolean;
|
|
9
9
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { IEvent } from "./event";
|
|
2
|
-
|
|
3
|
-
export interface IEventQueue {
|
|
4
|
-
addEvent(event: IEvent): boolean;
|
|
5
|
-
|
|
6
|
-
clear(): void;
|
|
7
|
-
|
|
8
|
-
shift(): IEvent | undefined;
|
|
9
|
-
|
|
10
|
-
close(): void;
|
|
11
|
-
|
|
12
|
-
get eventsSnapshot(): IEvent[];
|
|
13
|
-
|
|
14
|
-
get length(): number;
|
|
15
|
-
|
|
16
|
-
get isEmpty(): boolean;
|
|
1
|
+
import { IEvent } from "./event";
|
|
2
|
+
|
|
3
|
+
export interface IEventQueue {
|
|
4
|
+
addEvent(event: IEvent): boolean;
|
|
5
|
+
|
|
6
|
+
clear(): void;
|
|
7
|
+
|
|
8
|
+
shift(): IEvent | undefined;
|
|
9
|
+
|
|
10
|
+
close(): void;
|
|
11
|
+
|
|
12
|
+
get eventsSnapshot(): IEvent[];
|
|
13
|
+
|
|
14
|
+
get length(): number;
|
|
15
|
+
|
|
16
|
+
get isEmpty(): boolean;
|
|
17
17
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export enum DeliveryStatus {
|
|
2
|
-
Succeeded,
|
|
3
|
-
Failed,
|
|
4
|
-
FailedAndMustShutDown
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface IEventSenderResult {
|
|
8
|
-
status: DeliveryStatus,
|
|
9
|
-
error?: any
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface IEventSender {
|
|
13
|
-
send(payload: string, retry: boolean): Promise<IEventSenderResult>;
|
|
1
|
+
export enum DeliveryStatus {
|
|
2
|
+
Succeeded,
|
|
3
|
+
Failed,
|
|
4
|
+
FailedAndMustShutDown
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface IEventSenderResult {
|
|
8
|
+
status: DeliveryStatus,
|
|
9
|
+
error?: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IEventSender {
|
|
13
|
+
send(payload: string, retry: boolean): Promise<IEventSenderResult>;
|
|
14
14
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { IEventProcessor } from "./IEventProcessor";
|
|
2
|
-
import { IEvent } from "./event";
|
|
3
|
-
|
|
4
|
-
export class NullEventProcessor implements IEventProcessor {
|
|
5
|
-
flush(): Promise<void> {
|
|
6
|
-
return Promise.resolve();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
close(): Promise<void> {
|
|
10
|
-
return Promise.resolve();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
record(event: IEvent | null): boolean {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
1
|
+
import { IEventProcessor } from "./IEventProcessor";
|
|
2
|
+
import { IEvent } from "./event";
|
|
3
|
+
|
|
4
|
+
export class NullEventProcessor implements IEventProcessor {
|
|
5
|
+
flush(): Promise<void> {
|
|
6
|
+
return Promise.resolve();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
close(): Promise<void> {
|
|
10
|
+
return Promise.resolve();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
record(event: IEvent | null): boolean {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
16
|
}
|