@servicetitan/log-service 27.1.1 → 27.2.1
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/dist/log-sender.d.ts +8 -9
- package/dist/log-sender.d.ts.map +1 -1
- package/dist/log-sender.js +63 -76
- package/dist/log-sender.js.map +1 -1
- package/dist/log-storage.d.ts +7 -1
- package/dist/log-storage.d.ts.map +1 -1
- package/dist/log-storage.js +82 -19
- package/dist/log-storage.js.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/__mocks__/broadcast-channel.ts +39 -0
- package/src/__tests__/__mocks__/index.ts +1 -0
- package/src/__tests__/log-sender.test.ts +18 -9
- package/src/__tests__/log-storage.test.ts +47 -4
- package/src/log-sender.ts +64 -61
- package/src/log-storage.ts +91 -18
package/dist/log-sender.d.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
+
import { Store } from '@servicetitan/react-ioc';
|
|
1
2
|
import { ConnectionService } from './connection-service';
|
|
2
3
|
import { LogConfig, Log } from './log';
|
|
3
4
|
import { LogStorage } from './log-storage';
|
|
4
|
-
export declare class LogSender {
|
|
5
|
+
export declare class LogSender extends Store {
|
|
5
6
|
readonly connectionService: ConnectionService;
|
|
6
7
|
readonly logStorage: LogStorage;
|
|
7
8
|
private readonly logConfig;
|
|
8
9
|
readonly log: Log;
|
|
10
|
+
private inSendFailedScenario;
|
|
9
11
|
private isSending;
|
|
10
|
-
private batchSize;
|
|
11
|
-
private initialDuration;
|
|
12
|
-
private initialFailDuration;
|
|
13
|
-
private failIntervalDuration;
|
|
14
|
-
private maxIntervalDuration;
|
|
15
|
-
private failSendScenarioEnabled;
|
|
16
|
-
private sendInterval?;
|
|
17
12
|
private onItemsChangedDisposer?;
|
|
13
|
+
private sendInterval;
|
|
14
|
+
private intervalId?;
|
|
18
15
|
constructor(connectionService: ConnectionService, logStorage: LogStorage, logConfig: LogConfig, log: Log);
|
|
16
|
+
dispose(): void;
|
|
19
17
|
private onItemsChanged;
|
|
20
18
|
private setSendInterval;
|
|
21
19
|
private send;
|
|
20
|
+
private sendEvents;
|
|
22
21
|
private setRegularSendScenario;
|
|
23
|
-
private
|
|
22
|
+
private setFailedSendScenario;
|
|
24
23
|
private increaseSendInterval;
|
|
25
24
|
}
|
|
26
25
|
//# sourceMappingURL=log-sender.d.ts.map
|
package/dist/log-sender.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-sender.d.ts","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"log-sender.d.ts","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAoB,GAAG,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,UAAU,EAAuB,MAAM,eAAe,CAAC;AAQhE,qBACa,SAAU,SAAQ,KAAK;IAQD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB;IACpD,QAAQ,CAAC,UAAU,EAAE,UAAU;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IACvC,QAAQ,CAAC,GAAG,EAAE,GAAG;IAVlC,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,sBAAsB,CAAC,CAAoB;IACnD,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,UAAU,CAAC,CAAS;gBAGY,iBAAiB,EAAE,iBAAiB,EAC3C,UAAU,EAAE,UAAU,EACR,SAAS,EAAE,SAAS,EACzC,GAAG,EAAE,GAAG;IAMlC,OAAO;IAIP,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,IAAI;YAuBE,UAAU;IA2BxB,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,oBAAoB;CAK/B"}
|
package/dist/log-sender.js
CHANGED
|
@@ -11,13 +11,19 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import axios from 'axios';
|
|
14
|
-
import { injectable, inject } from '@servicetitan/react-ioc';
|
|
14
|
+
import { injectable, inject, Store } from '@servicetitan/react-ioc';
|
|
15
15
|
import { reaction } from 'mobx';
|
|
16
16
|
import { ConnectionService } from './connection-service';
|
|
17
17
|
import { LOG_CONFIG_TOKEN, Log } from './log';
|
|
18
18
|
import { LogStorage, lastId } from './log-storage';
|
|
19
|
-
|
|
19
|
+
const SECOND = 1000;
|
|
20
|
+
const DEFAULT_SEND_INTERVAL = 60 * SECOND;
|
|
21
|
+
const INITIAL_RETRY_INTERVAL = SECOND;
|
|
22
|
+
const MAX_RETRY_INTERVAL = INITIAL_RETRY_INTERVAL * 512;
|
|
23
|
+
const BATCH_SIZE = 128;
|
|
24
|
+
let LogSender = class LogSender extends Store {
|
|
20
25
|
constructor(connectionService, logStorage, logConfig, log) {
|
|
26
|
+
super();
|
|
21
27
|
Object.defineProperty(this, "connectionService", {
|
|
22
28
|
enumerable: true,
|
|
23
29
|
configurable: true,
|
|
@@ -42,55 +48,31 @@ let LogSender = class LogSender {
|
|
|
42
48
|
writable: true,
|
|
43
49
|
value: log
|
|
44
50
|
});
|
|
45
|
-
Object.defineProperty(this, "
|
|
51
|
+
Object.defineProperty(this, "inSendFailedScenario", {
|
|
46
52
|
enumerable: true,
|
|
47
53
|
configurable: true,
|
|
48
54
|
writable: true,
|
|
49
55
|
value: false
|
|
50
56
|
});
|
|
51
|
-
Object.defineProperty(this, "
|
|
52
|
-
enumerable: true,
|
|
53
|
-
configurable: true,
|
|
54
|
-
writable: true,
|
|
55
|
-
value: 128
|
|
56
|
-
});
|
|
57
|
-
Object.defineProperty(this, "initialDuration", {
|
|
58
|
-
enumerable: true,
|
|
59
|
-
configurable: true,
|
|
60
|
-
writable: true,
|
|
61
|
-
value: 60 * 1000
|
|
62
|
-
});
|
|
63
|
-
Object.defineProperty(this, "initialFailDuration", {
|
|
64
|
-
enumerable: true,
|
|
65
|
-
configurable: true,
|
|
66
|
-
writable: true,
|
|
67
|
-
value: 1000
|
|
68
|
-
});
|
|
69
|
-
Object.defineProperty(this, "failIntervalDuration", {
|
|
70
|
-
enumerable: true,
|
|
71
|
-
configurable: true,
|
|
72
|
-
writable: true,
|
|
73
|
-
value: this.initialFailDuration
|
|
74
|
-
});
|
|
75
|
-
Object.defineProperty(this, "maxIntervalDuration", {
|
|
57
|
+
Object.defineProperty(this, "isSending", {
|
|
76
58
|
enumerable: true,
|
|
77
59
|
configurable: true,
|
|
78
60
|
writable: true,
|
|
79
|
-
value:
|
|
61
|
+
value: false
|
|
80
62
|
});
|
|
81
|
-
Object.defineProperty(this, "
|
|
63
|
+
Object.defineProperty(this, "onItemsChangedDisposer", {
|
|
82
64
|
enumerable: true,
|
|
83
65
|
configurable: true,
|
|
84
66
|
writable: true,
|
|
85
|
-
value:
|
|
67
|
+
value: void 0
|
|
86
68
|
});
|
|
87
69
|
Object.defineProperty(this, "sendInterval", {
|
|
88
70
|
enumerable: true,
|
|
89
71
|
configurable: true,
|
|
90
72
|
writable: true,
|
|
91
|
-
value:
|
|
73
|
+
value: DEFAULT_SEND_INTERVAL
|
|
92
74
|
});
|
|
93
|
-
Object.defineProperty(this, "
|
|
75
|
+
Object.defineProperty(this, "intervalId", {
|
|
94
76
|
enumerable: true,
|
|
95
77
|
configurable: true,
|
|
96
78
|
writable: true,
|
|
@@ -98,59 +80,63 @@ let LogSender = class LogSender {
|
|
|
98
80
|
});
|
|
99
81
|
this.setRegularSendScenario();
|
|
100
82
|
}
|
|
101
|
-
|
|
83
|
+
dispose() {
|
|
84
|
+
window.clearInterval(this.intervalId);
|
|
85
|
+
}
|
|
102
86
|
onItemsChanged(items) {
|
|
103
|
-
if (items.length >=
|
|
87
|
+
if (items.length >= BATCH_SIZE) {
|
|
104
88
|
this.send();
|
|
105
89
|
}
|
|
106
90
|
}
|
|
107
|
-
/* send periodically */
|
|
108
91
|
setSendInterval(interval) {
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
this.sendInterval = interval;
|
|
93
|
+
window.clearInterval(this.intervalId);
|
|
94
|
+
this.intervalId = window.setInterval(() => {
|
|
111
95
|
if (this.logStorage.items.length > 0) {
|
|
112
96
|
this.send();
|
|
113
97
|
}
|
|
114
98
|
}, interval);
|
|
115
99
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
category: 'Log Service',
|
|
120
|
-
code: 'LogSenderAlreadySending',
|
|
121
|
-
message: 'send_already_in_progress',
|
|
122
|
-
});
|
|
100
|
+
send() {
|
|
101
|
+
// If offline, punt
|
|
102
|
+
if (!this.connectionService.online) {
|
|
123
103
|
return;
|
|
124
104
|
}
|
|
125
|
-
/*
|
|
126
|
-
|
|
127
|
-
|
|
105
|
+
/*
|
|
106
|
+
* Using Web Locks API to prevent multiple tabs from simultaneously
|
|
107
|
+
* sending previously saved items. If the API isn't available, ¯\_(ツ)_/¯
|
|
108
|
+
*/
|
|
109
|
+
if (!('locks' in globalThis.navigator)) {
|
|
110
|
+
this.sendEvents();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// eslint-disable-next-line no-restricted-globals
|
|
114
|
+
navigator.locks.request(this.logConfig.storageKey, { ifAvailable: true }, async (lock) => {
|
|
115
|
+
if (lock) {
|
|
116
|
+
await this.sendEvents();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async sendEvents() {
|
|
121
|
+
if (this.isSending) {
|
|
122
|
+
return;
|
|
128
123
|
}
|
|
129
124
|
this.isSending = true;
|
|
130
125
|
try {
|
|
131
|
-
const logEvents = this.logStorage.loadBatch(
|
|
132
|
-
|
|
133
|
-
if (!logEvents.length) {
|
|
134
|
-
throw new Error('zero_log_events');
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
126
|
+
const logEvents = this.logStorage.loadBatch(BATCH_SIZE);
|
|
127
|
+
if (logEvents.length) {
|
|
137
128
|
await axios.post(this.logConfig.endpoint, logEvents);
|
|
138
129
|
this.logStorage.deleteToId(lastId(logEvents));
|
|
139
|
-
if (this.
|
|
130
|
+
if (this.inSendFailedScenario) {
|
|
140
131
|
this.setRegularSendScenario();
|
|
141
132
|
}
|
|
142
133
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
/* istanbul ignore next */
|
|
151
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
152
|
-
throw e;
|
|
153
|
-
}
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
this.setFailedSendScenario();
|
|
137
|
+
/* istanbul ignore next */
|
|
138
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
139
|
+
throw e;
|
|
154
140
|
}
|
|
155
141
|
}
|
|
156
142
|
finally {
|
|
@@ -158,23 +144,24 @@ let LogSender = class LogSender {
|
|
|
158
144
|
}
|
|
159
145
|
}
|
|
160
146
|
setRegularSendScenario() {
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
147
|
+
this.inSendFailedScenario = false;
|
|
148
|
+
this.setSendInterval(DEFAULT_SEND_INTERVAL);
|
|
163
149
|
this.onItemsChangedDisposer = reaction(() => this.logStorage.items, items => this.onItemsChanged(items));
|
|
164
|
-
this.setSendInterval(this.initialDuration);
|
|
165
150
|
}
|
|
166
|
-
|
|
151
|
+
setFailedSendScenario() {
|
|
167
152
|
var _a;
|
|
168
|
-
this.
|
|
153
|
+
if (this.inSendFailedScenario) {
|
|
154
|
+
this.increaseSendInterval();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.inSendFailedScenario = true;
|
|
158
|
+
this.setSendInterval(INITIAL_RETRY_INTERVAL);
|
|
169
159
|
(_a = this.onItemsChangedDisposer) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
170
|
-
this.setSendInterval(this.initialFailDuration);
|
|
171
160
|
}
|
|
172
161
|
increaseSendInterval() {
|
|
173
|
-
if (this.
|
|
174
|
-
|
|
162
|
+
if (this.sendInterval < MAX_RETRY_INTERVAL) {
|
|
163
|
+
this.setSendInterval(this.sendInterval * 2);
|
|
175
164
|
}
|
|
176
|
-
this.failIntervalDuration *= 2;
|
|
177
|
-
this.setSendInterval(this.failIntervalDuration);
|
|
178
165
|
}
|
|
179
166
|
};
|
|
180
167
|
LogSender = __decorate([
|
package/dist/log-sender.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"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"}
|
package/dist/log-storage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Store } from '@servicetitan/react-ioc';
|
|
1
2
|
import { EntryComplete, Nullable } from './log';
|
|
2
3
|
import { LogConfig } from './log-config';
|
|
3
4
|
export interface StorageItem {
|
|
@@ -9,17 +10,22 @@ export interface StorageItem {
|
|
|
9
10
|
data: string;
|
|
10
11
|
exception: string;
|
|
11
12
|
}
|
|
12
|
-
export declare class LogStorage {
|
|
13
|
+
export declare class LogStorage extends Store {
|
|
13
14
|
private readonly logConfig;
|
|
14
15
|
items: StorageItem[];
|
|
15
16
|
private readonly maxLogSize;
|
|
16
17
|
private readonly version;
|
|
17
18
|
private nextItemId;
|
|
19
|
+
private channel?;
|
|
18
20
|
constructor(logConfig: LogConfig);
|
|
21
|
+
dispose(): void;
|
|
19
22
|
get config(): LogConfig;
|
|
20
23
|
store(entry: EntryComplete): void;
|
|
21
24
|
deleteToId(id: number): void;
|
|
22
25
|
loadBatch(batchSize: number): StorageItem[];
|
|
26
|
+
private loadSavedItems;
|
|
27
|
+
private postDeleteEvent;
|
|
28
|
+
private handleDeleteEvent;
|
|
23
29
|
private setItems;
|
|
24
30
|
private get storageKey();
|
|
25
31
|
private get storageVersionKey();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-storage.d.ts","sourceRoot":"","sources":["../src/log-storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"log-storage.d.ts","sourceRoot":"","sources":["../src/log-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACrB;AAWD,qBACa,UAAW,SAAQ,KAAK;IAUK,OAAO,CAAC,QAAQ,CAAC,SAAS;IARhE,KAAK,EAAE,WAAW,EAAE,CAAM;IAE1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAK;IAE7B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,OAAO,CAAC,CAAmB;gBAEoB,SAAS,EAAE,SAAS;IA0B3E,OAAO;IAIP,IAAI,MAAM,IAAI,SAAS,CAEtB;IAED,KAAK,CAAC,KAAK,EAAE,aAAa;IAgB1B,UAAU,CAAC,EAAE,EAAE,MAAM;IAcrB,SAAS,CAAC,SAAS,EAAE,MAAM;IAI3B,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,iBAAiB;IAqBzB,OAAO,CAAC,QAAQ;IAKhB,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,iBAAiB,GAE5B;IAED,OAAO,CAAC,cAAc;CAIzB;AAMD,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAMnD"}
|
package/dist/log-storage.js
CHANGED
|
@@ -10,12 +10,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
-
import { inject, injectable } from '@servicetitan/react-ioc';
|
|
14
|
-
import { observable, action, makeObservable } from 'mobx';
|
|
13
|
+
import { inject, injectable, Store } from '@servicetitan/react-ioc';
|
|
14
|
+
import { observable, action, makeObservable, toJS } from 'mobx';
|
|
15
15
|
import { LOG_CONFIG_TOKEN } from './log-config';
|
|
16
|
-
|
|
16
|
+
var EventType;
|
|
17
|
+
(function (EventType) {
|
|
18
|
+
EventType["Delete"] = "delete";
|
|
19
|
+
})(EventType || (EventType = {}));
|
|
20
|
+
let LogStorage = class LogStorage extends Store {
|
|
17
21
|
constructor(logConfig) {
|
|
18
|
-
|
|
22
|
+
super();
|
|
19
23
|
Object.defineProperty(this, "logConfig", {
|
|
20
24
|
enumerable: true,
|
|
21
25
|
configurable: true,
|
|
@@ -46,27 +50,33 @@ let LogStorage = class LogStorage {
|
|
|
46
50
|
writable: true,
|
|
47
51
|
value: void 0
|
|
48
52
|
});
|
|
53
|
+
Object.defineProperty(this, "channel", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: void 0
|
|
58
|
+
});
|
|
49
59
|
makeObservable(this);
|
|
50
60
|
const version = window.parseInt(localStorage.getItem(this.storageVersionKey));
|
|
51
61
|
if (version !== this.version) {
|
|
52
62
|
localStorage.removeItem(this.storageKey);
|
|
53
63
|
localStorage.setItem(this.storageVersionKey, String(this.version));
|
|
54
64
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
+
this.loadSavedItems();
|
|
66
|
+
this.nextItemId = lastId(this.items) + 1;
|
|
67
|
+
/*
|
|
68
|
+
* Use the BroadcastChannel API to prevent different tabs from processing
|
|
69
|
+
* the saved items. The first one to process the items sends a "delete" event
|
|
70
|
+
* that causes all other instances to discard them from memory.
|
|
71
|
+
*/
|
|
72
|
+
if ('BroadcastChannel' in globalThis) {
|
|
73
|
+
this.channel = new BroadcastChannel(logConfig.storageKey);
|
|
74
|
+
this.channel.addEventListener('message', ({ data }) => this.handleDeleteEvent(data));
|
|
65
75
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
}
|
|
77
|
+
dispose() {
|
|
78
|
+
var _a;
|
|
79
|
+
(_a = this.channel) === null || _a === void 0 ? void 0 : _a.close();
|
|
70
80
|
}
|
|
71
81
|
get config() {
|
|
72
82
|
return this.logConfig;
|
|
@@ -86,12 +96,62 @@ let LogStorage = class LogStorage {
|
|
|
86
96
|
this.setItems(items);
|
|
87
97
|
}
|
|
88
98
|
deleteToId(id) {
|
|
89
|
-
const
|
|
99
|
+
const deletedItems = [];
|
|
100
|
+
const items = this.items.filter(item => {
|
|
101
|
+
if (item.id > id) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
deletedItems.push(item);
|
|
105
|
+
return false;
|
|
106
|
+
});
|
|
90
107
|
this.setItems(items);
|
|
108
|
+
this.postDeleteEvent(deletedItems);
|
|
91
109
|
}
|
|
92
110
|
loadBatch(batchSize) {
|
|
93
111
|
return this.items.slice(0, batchSize);
|
|
94
112
|
}
|
|
113
|
+
loadSavedItems() {
|
|
114
|
+
var _a;
|
|
115
|
+
try {
|
|
116
|
+
const items = (_a = JSON.parse(localStorage.getItem(this.storageKey))) !== null && _a !== void 0 ? _a : [];
|
|
117
|
+
/* migrate error -> exception */
|
|
118
|
+
items.forEach((item) => {
|
|
119
|
+
if (typeof item.error === 'string') {
|
|
120
|
+
item.exception = item.error;
|
|
121
|
+
delete item.error;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this.setItems(items);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
this.setItems([]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
postDeleteEvent(items) {
|
|
131
|
+
if (this.channel) {
|
|
132
|
+
const event = {
|
|
133
|
+
type: EventType.Delete,
|
|
134
|
+
items: items.map(item => toJS(item)),
|
|
135
|
+
};
|
|
136
|
+
this.channel.postMessage(event);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
handleDeleteEvent(event) {
|
|
140
|
+
/* istanbul ignore next: because there's only one event type */
|
|
141
|
+
if (event.type !== EventType.Delete) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Map item id to object for fast lookup
|
|
145
|
+
const map = new Map(event.items.map(item => [item.id, item]));
|
|
146
|
+
// Discard deleted items
|
|
147
|
+
const items = this.items.filter(item => !isEqual(item, map.get(item.id)));
|
|
148
|
+
this.setItems(items);
|
|
149
|
+
// If no more items, close channel; all saved items have been processed
|
|
150
|
+
if (items.length === 0 && this.channel) {
|
|
151
|
+
this.channel.close();
|
|
152
|
+
delete this.channel;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
95
155
|
setItems(items) {
|
|
96
156
|
this.items = [...items];
|
|
97
157
|
localStorage.setItem(this.storageKey, JSON.stringify(this.items));
|
|
@@ -123,6 +183,9 @@ LogStorage = __decorate([
|
|
|
123
183
|
__metadata("design:paramtypes", [Object])
|
|
124
184
|
], LogStorage);
|
|
125
185
|
export { LogStorage };
|
|
186
|
+
function isEqual(a, b) {
|
|
187
|
+
return b && Object.keys(a).every((key) => a[key] === b[key]);
|
|
188
|
+
}
|
|
126
189
|
export function lastId(items) {
|
|
127
190
|
if (!items.length) {
|
|
128
191
|
return 0;
|
package/dist/log-storage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-storage.js","sourceRoot":"","sources":["../src/log-storage.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"log-storage.js","sourceRoot":"","sources":["../src/log-storage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/log-service",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"homepage": "https://docs.st.dev/docs/frontend/log-service",
|
|
6
6
|
"repository": {
|
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
"src"
|
|
17
17
|
],
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@servicetitan/react-ioc": "27.
|
|
19
|
+
"@servicetitan/react-ioc": "27.2.1",
|
|
20
20
|
"@testing-library/dom": "^10.4.0",
|
|
21
|
-
"@testing-library/react": "^16.0.
|
|
21
|
+
"@testing-library/react": "^16.0.1",
|
|
22
22
|
"@types/lodash.throttle": "~4.1.6",
|
|
23
23
|
"axios": "~0.28.1",
|
|
24
|
-
"mobx": "~6.13.
|
|
24
|
+
"mobx": "~6.13.2",
|
|
25
25
|
"react": "~18.3.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@servicetitan/react-ioc": "27.
|
|
28
|
+
"@servicetitan/react-ioc": "27.2.1",
|
|
29
29
|
"axios": "~0.28.1",
|
|
30
|
-
"mobx": ">=6.13.
|
|
30
|
+
"mobx": ">=6.13.2",
|
|
31
31
|
"react": ">=17.0.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"cli": {
|
|
40
40
|
"webpack": false
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "0d9d923ebf34a808f42ad215f4538ef5920f5689"
|
|
43
43
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
const channels: Map<string, EventEmitter> = new Map();
|
|
4
|
+
|
|
5
|
+
export class MockBroadcastChannel implements BroadcastChannel {
|
|
6
|
+
onmessage = jest.fn();
|
|
7
|
+
onmessageerror = jest.fn();
|
|
8
|
+
removeEventListener = jest.fn();
|
|
9
|
+
dispatchEvent = jest.fn();
|
|
10
|
+
|
|
11
|
+
postMessage = jest.fn().mockImplementation(message => {
|
|
12
|
+
const event = { data: message, from: this };
|
|
13
|
+
this.emitter.emit('message', event);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
addEventListener = jest.fn().mockImplementation((_: string, listener: any) => {
|
|
17
|
+
this.emitter.on('message', ({ from, ...event }) => {
|
|
18
|
+
if (from !== this) {
|
|
19
|
+
listener(event);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
close = jest.fn().mockImplementation(() => {
|
|
25
|
+
this.emitter.removeAllListeners();
|
|
26
|
+
channels.delete(this.name);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
private emitter: EventEmitter;
|
|
30
|
+
|
|
31
|
+
constructor(public name: string) {
|
|
32
|
+
this.emitter = channels.get(name)!;
|
|
33
|
+
if (!this.emitter) {
|
|
34
|
+
const emitter = new EventEmitter();
|
|
35
|
+
channels.set(name, emitter);
|
|
36
|
+
this.emitter = emitter;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './broadcast-channel';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { ConnectionService } from '../connection-service';
|
|
3
3
|
import { Log, LogConfig, LogEntryType, LogInfo, LogWarning } from '../log';
|
|
4
|
-
import { LogStorage } from '../log-storage';
|
|
4
|
+
import { lastId, LogStorage } from '../log-storage';
|
|
5
5
|
|
|
6
6
|
import { LogSender } from '../log-sender';
|
|
7
7
|
|
|
@@ -35,13 +35,13 @@ describe(`[log-service] ${LogSender.name}`, () => {
|
|
|
35
35
|
let log: Pick<Log, 'info' | 'warning'>;
|
|
36
36
|
|
|
37
37
|
beforeAll(() => {
|
|
38
|
-
jest.spyOn(window, 'setInterval').mockImplementation(jest.fn());
|
|
39
38
|
process.env.NODE_ENV ??= 'test'; // workaround issue with Jest not setting NODE_ENV=test
|
|
39
|
+
jest.spyOn(window, 'setInterval').mockImplementation(jest.fn());
|
|
40
|
+
jest.spyOn(axios, 'post').mockImplementation(jest.fn());
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
beforeEach(() => {
|
|
43
44
|
jest.clearAllMocks();
|
|
44
|
-
jest.spyOn(axios, 'post').mockImplementation(jest.fn());
|
|
45
45
|
|
|
46
46
|
logStorage = new LogStorage(logConfig);
|
|
47
47
|
log = new MockLog(logStorage);
|
|
@@ -49,9 +49,11 @@ describe(`[log-service] ${LogSender.name}`, () => {
|
|
|
49
49
|
const logSender = new LogSender(connectionService, logStorage, logConfig, log as Log);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
afterEach(() => logStorage.deleteToId(lastId(logStorage.items)));
|
|
53
|
+
|
|
52
54
|
const subject = async () => {
|
|
53
55
|
getHandler().fn();
|
|
54
|
-
await
|
|
56
|
+
await Promise.resolve(); // resolve pending promises
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
function getHandler() {
|
|
@@ -114,14 +116,21 @@ describe(`[log-service] ${LogSender.name}`, () => {
|
|
|
114
116
|
})
|
|
115
117
|
);
|
|
116
118
|
|
|
117
|
-
test('
|
|
118
|
-
|
|
119
|
+
test('skips redundant post', async () => {
|
|
120
|
+
await subject();
|
|
121
|
+
|
|
122
|
+
expect(axios.post).toHaveBeenCalledTimes(1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('when offline', () => {
|
|
127
|
+
beforeEach(() => connectionService.setOnline(false));
|
|
128
|
+
afterEach(() => connectionService.setOnline(true));
|
|
119
129
|
|
|
130
|
+
test('does not post', async () => {
|
|
120
131
|
await subject();
|
|
121
132
|
|
|
122
|
-
expect(
|
|
123
|
-
expect.objectContaining({ message: 'send_already_in_progress' })
|
|
124
|
-
);
|
|
133
|
+
expect(axios.post).not.toHaveBeenCalled();
|
|
125
134
|
});
|
|
126
135
|
});
|
|
127
136
|
});
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { EntryComplete, LogEntryType } from '../log';
|
|
2
2
|
import { LogConfig } from '../log-config';
|
|
3
|
-
import { LogStorage } from '../log-storage';
|
|
3
|
+
import { lastId, LogStorage } from '../log-storage';
|
|
4
|
+
|
|
5
|
+
import { MockBroadcastChannel } from './__mocks__';
|
|
4
6
|
|
|
5
7
|
describe(`${LogStorage.name}`, () => {
|
|
6
8
|
const LOCAL_STORAGE_VERSION = '1';
|
|
7
9
|
const logConfig: LogConfig = { endpoint: '/Log/Foo', storageKey: 'LogStorage_Foo' };
|
|
10
|
+
let logStorage: LogStorage;
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
jest.spyOn(globalThis, 'BroadcastChannel').mockImplementation(
|
|
14
|
+
(name: string) => new MockBroadcastChannel(name)
|
|
15
|
+
);
|
|
16
|
+
});
|
|
8
17
|
|
|
9
18
|
beforeEach(() => window.localStorage.clear());
|
|
19
|
+
afterEach(() => logStorage.dispose());
|
|
10
20
|
|
|
11
21
|
const subject = (entryOrEntries: EntryComplete | EntryComplete[] = []) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
entries.forEach(entry => logStorage.store(entry));
|
|
22
|
+
logStorage = new LogStorage(logConfig);
|
|
23
|
+
[entryOrEntries].flat().forEach(entry => logStorage.store(entry));
|
|
15
24
|
return logStorage;
|
|
16
25
|
};
|
|
17
26
|
|
|
@@ -29,6 +38,7 @@ describe(`${LogStorage.name}`, () => {
|
|
|
29
38
|
beforeEach(() => {
|
|
30
39
|
const logStorage = new LogStorage(logConfig);
|
|
31
40
|
entries.map(entry => logStorage.store(entry));
|
|
41
|
+
logStorage.dispose();
|
|
32
42
|
});
|
|
33
43
|
|
|
34
44
|
function itLoadsSavedItems() {
|
|
@@ -71,6 +81,39 @@ describe(`${LogStorage.name}`, () => {
|
|
|
71
81
|
|
|
72
82
|
itLoadsSavedItems();
|
|
73
83
|
});
|
|
84
|
+
|
|
85
|
+
describe('when multiple instances are running simultaneously', () => {
|
|
86
|
+
let otherLogStorage: LogStorage;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => (otherLogStorage = new LogStorage(logConfig)));
|
|
89
|
+
afterEach(() => otherLogStorage.dispose());
|
|
90
|
+
|
|
91
|
+
test('deleteToId removes items from both instances', () => {
|
|
92
|
+
const logStorage = subject();
|
|
93
|
+
|
|
94
|
+
expect(logStorage.items.length).toEqual(entries.length);
|
|
95
|
+
expect(otherLogStorage.items.length).toEqual(entries.length);
|
|
96
|
+
|
|
97
|
+
logStorage.deleteToId(lastId(logStorage.items));
|
|
98
|
+
|
|
99
|
+
expect(logStorage.items).toEqual([]);
|
|
100
|
+
expect(otherLogStorage.items).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('deleteToId does not remove private items', () => {
|
|
104
|
+
const logStorage = subject();
|
|
105
|
+
|
|
106
|
+
const entry = createEntry({ message: 'foo' });
|
|
107
|
+
otherLogStorage.store(entry);
|
|
108
|
+
|
|
109
|
+
logStorage.deleteToId(lastId(logStorage.items));
|
|
110
|
+
|
|
111
|
+
expect(logStorage.items).toEqual([]);
|
|
112
|
+
expect(otherLogStorage.items).toEqual(
|
|
113
|
+
buildItems([entry]).map(expect.objectContaining)
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
74
117
|
});
|
|
75
118
|
|
|
76
119
|
test('stores items with unique ids', () => {
|
package/src/log-sender.ts
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
|
|
3
|
-
import { injectable, inject } from '@servicetitan/react-ioc';
|
|
3
|
+
import { injectable, inject, Store } from '@servicetitan/react-ioc';
|
|
4
4
|
import { reaction, IReactionDisposer } from 'mobx';
|
|
5
5
|
|
|
6
6
|
import { ConnectionService } from './connection-service';
|
|
7
7
|
import { LogConfig, LOG_CONFIG_TOKEN, Log } from './log';
|
|
8
8
|
import { LogStorage, lastId, StorageItem } from './log-storage';
|
|
9
9
|
|
|
10
|
+
const SECOND = 1000;
|
|
11
|
+
const DEFAULT_SEND_INTERVAL = 60 * SECOND;
|
|
12
|
+
const INITIAL_RETRY_INTERVAL = SECOND;
|
|
13
|
+
const MAX_RETRY_INTERVAL = INITIAL_RETRY_INTERVAL * 512;
|
|
14
|
+
const BATCH_SIZE = 128;
|
|
15
|
+
|
|
10
16
|
@injectable()
|
|
11
|
-
export class LogSender {
|
|
17
|
+
export class LogSender extends Store {
|
|
18
|
+
private inSendFailedScenario = false;
|
|
12
19
|
private isSending = false;
|
|
13
|
-
|
|
14
|
-
private batchSize = 128;
|
|
15
|
-
|
|
16
|
-
private initialDuration = 60 * 1000;
|
|
17
|
-
private initialFailDuration = 1000;
|
|
18
|
-
private failIntervalDuration = this.initialFailDuration;
|
|
19
|
-
private maxIntervalDuration = this.initialFailDuration * 512;
|
|
20
|
-
|
|
21
|
-
private failSendScenarioEnabled = false;
|
|
22
|
-
|
|
23
|
-
private sendInterval?: number;
|
|
24
20
|
private onItemsChangedDisposer?: IReactionDisposer;
|
|
21
|
+
private sendInterval = DEFAULT_SEND_INTERVAL;
|
|
22
|
+
private intervalId?: number;
|
|
25
23
|
|
|
26
24
|
constructor(
|
|
27
25
|
@inject(ConnectionService) readonly connectionService: ConnectionService,
|
|
@@ -29,68 +27,74 @@ export class LogSender {
|
|
|
29
27
|
@inject(LOG_CONFIG_TOKEN) private readonly logConfig: LogConfig,
|
|
30
28
|
@inject(Log) readonly log: Log
|
|
31
29
|
) {
|
|
30
|
+
super();
|
|
32
31
|
this.setRegularSendScenario();
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
dispose() {
|
|
35
|
+
window.clearInterval(this.intervalId);
|
|
36
|
+
}
|
|
37
|
+
|
|
36
38
|
private onItemsChanged(items: StorageItem[]) {
|
|
37
|
-
if (items.length >=
|
|
39
|
+
if (items.length >= BATCH_SIZE) {
|
|
38
40
|
this.send();
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/* send periodically */
|
|
43
44
|
private setSendInterval(interval: number) {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
this.sendInterval = interval;
|
|
46
|
+
window.clearInterval(this.intervalId);
|
|
47
|
+
this.intervalId = window.setInterval(() => {
|
|
46
48
|
if (this.logStorage.items.length > 0) {
|
|
47
49
|
this.send();
|
|
48
50
|
}
|
|
49
51
|
}, interval);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
this.log.warning({
|
|
55
|
-
category: 'Log Service',
|
|
56
|
-
code: 'LogSenderAlreadySending',
|
|
57
|
-
message: 'send_already_in_progress',
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
/* istanbul ignore next */
|
|
54
|
+
private send() {
|
|
55
|
+
// If offline, punt
|
|
62
56
|
if (!this.connectionService.online) {
|
|
63
|
-
|
|
57
|
+
return;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
/*
|
|
61
|
+
* Using Web Locks API to prevent multiple tabs from simultaneously
|
|
62
|
+
* sending previously saved items. If the API isn't available, ¯\_(ツ)_/¯
|
|
63
|
+
*/
|
|
64
|
+
if (!('locks' in globalThis.navigator)) {
|
|
65
|
+
this.sendEvents();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
// eslint-disable-next-line no-restricted-globals
|
|
70
|
+
navigator.locks.request(this.logConfig.storageKey, { ifAvailable: true }, async lock => {
|
|
71
|
+
if (lock) {
|
|
72
|
+
await this.sendEvents();
|
|
74
73
|
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async sendEvents() {
|
|
78
|
+
if (this.isSending) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
this.isSending = true;
|
|
83
|
+
try {
|
|
84
|
+
const logEvents = this.logStorage.loadBatch(BATCH_SIZE);
|
|
85
|
+
if (logEvents.length) {
|
|
77
86
|
await axios.post(this.logConfig.endpoint, logEvents);
|
|
78
87
|
this.logStorage.deleteToId(lastId(logEvents));
|
|
79
88
|
|
|
80
|
-
if (this.
|
|
89
|
+
if (this.inSendFailedScenario) {
|
|
81
90
|
this.setRegularSendScenario();
|
|
82
91
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/* istanbul ignore next */
|
|
91
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
92
|
-
throw e;
|
|
93
|
-
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
this.setFailedSendScenario();
|
|
95
|
+
/* istanbul ignore next */
|
|
96
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
97
|
+
throw e;
|
|
94
98
|
}
|
|
95
99
|
} finally {
|
|
96
100
|
this.isSending = false;
|
|
@@ -98,29 +102,28 @@ export class LogSender {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
private setRegularSendScenario() {
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
|
|
105
|
+
this.inSendFailedScenario = false;
|
|
106
|
+
this.setSendInterval(DEFAULT_SEND_INTERVAL);
|
|
104
107
|
this.onItemsChangedDisposer = reaction(
|
|
105
108
|
() => this.logStorage.items,
|
|
106
109
|
items => this.onItemsChanged(items)
|
|
107
110
|
);
|
|
108
|
-
this.setSendInterval(this.initialDuration);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
private
|
|
112
|
-
this.
|
|
113
|
+
private setFailedSendScenario() {
|
|
114
|
+
if (this.inSendFailedScenario) {
|
|
115
|
+
this.increaseSendInterval();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
113
118
|
|
|
119
|
+
this.inSendFailedScenario = true;
|
|
120
|
+
this.setSendInterval(INITIAL_RETRY_INTERVAL);
|
|
114
121
|
this.onItemsChangedDisposer?.();
|
|
115
|
-
this.setSendInterval(this.initialFailDuration);
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
private increaseSendInterval() {
|
|
119
|
-
if (this.
|
|
120
|
-
|
|
125
|
+
if (this.sendInterval < MAX_RETRY_INTERVAL) {
|
|
126
|
+
this.setSendInterval(this.sendInterval * 2);
|
|
121
127
|
}
|
|
122
|
-
this.failIntervalDuration *= 2;
|
|
123
|
-
|
|
124
|
-
this.setSendInterval(this.failIntervalDuration);
|
|
125
128
|
}
|
|
126
129
|
}
|
package/src/log-storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { inject, injectable } from '@servicetitan/react-ioc';
|
|
2
|
-
import { observable, action, makeObservable } from 'mobx';
|
|
1
|
+
import { inject, injectable, Store } from '@servicetitan/react-ioc';
|
|
2
|
+
import { observable, action, makeObservable, toJS } from 'mobx';
|
|
3
3
|
|
|
4
4
|
import { EntryComplete, Nullable } from './log';
|
|
5
5
|
import { LOG_CONFIG_TOKEN, LogConfig } from './log-config';
|
|
@@ -14,8 +14,17 @@ export interface StorageItem {
|
|
|
14
14
|
exception: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
enum EventType {
|
|
18
|
+
Delete = 'delete',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DeleteEvent {
|
|
22
|
+
type: EventType;
|
|
23
|
+
items: StorageItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
@injectable()
|
|
18
|
-
export class LogStorage {
|
|
27
|
+
export class LogStorage extends Store {
|
|
19
28
|
@observable
|
|
20
29
|
items: StorageItem[] = [];
|
|
21
30
|
|
|
@@ -23,8 +32,10 @@ export class LogStorage {
|
|
|
23
32
|
private readonly version = 1;
|
|
24
33
|
|
|
25
34
|
private nextItemId: number;
|
|
35
|
+
private channel?: BroadcastChannel;
|
|
26
36
|
|
|
27
37
|
constructor(@inject(LOG_CONFIG_TOKEN) private readonly logConfig: LogConfig) {
|
|
38
|
+
super();
|
|
28
39
|
makeObservable(this);
|
|
29
40
|
|
|
30
41
|
const version = window.parseInt(localStorage.getItem(this.storageVersionKey) as string);
|
|
@@ -33,22 +44,24 @@ export class LogStorage {
|
|
|
33
44
|
localStorage.setItem(this.storageVersionKey, String(this.version));
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
this.loadSavedItems();
|
|
48
|
+
this.nextItemId = lastId(this.items) + 1;
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Use the BroadcastChannel API to prevent different tabs from processing
|
|
52
|
+
* the saved items. The first one to process the items sends a "delete" event
|
|
53
|
+
* that causes all other instances to discard them from memory.
|
|
54
|
+
*/
|
|
55
|
+
if ('BroadcastChannel' in globalThis) {
|
|
56
|
+
this.channel = new BroadcastChannel(logConfig.storageKey);
|
|
57
|
+
this.channel.addEventListener('message', ({ data }: MessageEvent<DeleteEvent>) =>
|
|
58
|
+
this.handleDeleteEvent(data)
|
|
59
|
+
);
|
|
49
60
|
}
|
|
61
|
+
}
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
dispose() {
|
|
64
|
+
this.channel?.close();
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
get config(): LogConfig {
|
|
@@ -72,14 +85,70 @@ export class LogStorage {
|
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
deleteToId(id: number) {
|
|
75
|
-
const
|
|
88
|
+
const deletedItems: StorageItem[] = [];
|
|
89
|
+
const items = this.items.filter(item => {
|
|
90
|
+
if (item.id > id) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
deletedItems.push(item);
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
|
|
76
97
|
this.setItems(items);
|
|
98
|
+
this.postDeleteEvent(deletedItems);
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
loadBatch(batchSize: number) {
|
|
80
102
|
return this.items.slice(0, batchSize);
|
|
81
103
|
}
|
|
82
104
|
|
|
105
|
+
private loadSavedItems() {
|
|
106
|
+
try {
|
|
107
|
+
const items = JSON.parse(localStorage.getItem(this.storageKey) as string) ?? [];
|
|
108
|
+
|
|
109
|
+
/* migrate error -> exception */
|
|
110
|
+
items.forEach((item: StorageItem & { error?: string }) => {
|
|
111
|
+
if (typeof item.error === 'string') {
|
|
112
|
+
item.exception = item.error;
|
|
113
|
+
delete item.error;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
this.setItems(items);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this.setItems([]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private postDeleteEvent(items: StorageItem[]) {
|
|
123
|
+
if (this.channel) {
|
|
124
|
+
const event: DeleteEvent = {
|
|
125
|
+
type: EventType.Delete,
|
|
126
|
+
items: items.map(item => toJS(item)),
|
|
127
|
+
};
|
|
128
|
+
this.channel.postMessage(event);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private handleDeleteEvent(event: DeleteEvent) {
|
|
133
|
+
/* istanbul ignore next: because there's only one event type */
|
|
134
|
+
if (event.type !== EventType.Delete) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Map item id to object for fast lookup
|
|
139
|
+
const map = new Map<number, StorageItem>(event.items.map(item => [item.id, item]));
|
|
140
|
+
|
|
141
|
+
// Discard deleted items
|
|
142
|
+
const items = this.items.filter(item => !isEqual(item, map.get(item.id)));
|
|
143
|
+
this.setItems(items);
|
|
144
|
+
|
|
145
|
+
// If no more items, close channel; all saved items have been processed
|
|
146
|
+
if (items.length === 0 && this.channel) {
|
|
147
|
+
this.channel.close();
|
|
148
|
+
delete this.channel;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
83
152
|
@action
|
|
84
153
|
private setItems(items: StorageItem[]) {
|
|
85
154
|
this.items = [...items];
|
|
@@ -100,6 +169,10 @@ export class LogStorage {
|
|
|
100
169
|
}
|
|
101
170
|
}
|
|
102
171
|
|
|
172
|
+
function isEqual(a: StorageItem, b?: StorageItem) {
|
|
173
|
+
return b && Object.keys(a).every((key: keyof StorageItem) => a[key] === b[key]);
|
|
174
|
+
}
|
|
175
|
+
|
|
103
176
|
export function lastId(items: StorageItem[]): number {
|
|
104
177
|
if (!items.length) {
|
|
105
178
|
return 0;
|