@servicetitan/log-service 27.1.0 → 27.2.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.
@@ -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 setFailSendScenario;
22
+ private setFailedSendScenario;
24
23
  private increaseSendInterval;
25
24
  }
26
25
  //# sourceMappingURL=log-sender.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"log-sender.d.ts","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"AAKA,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;AAEhE,qBACa,SAAS;IAgBa,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB;IACpD,QAAQ,CAAC,UAAU,EAAE,UAAU;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IACvC,QAAQ,CAAC,GAAG,EAAE,GAAG;IAlBlC,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,SAAS,CAAO;IAExB,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,oBAAoB,CAA4B;IACxD,OAAO,CAAC,mBAAmB,CAAkC;IAE7D,OAAO,CAAC,uBAAuB,CAAS;IAExC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,sBAAsB,CAAC,CAAoB;gBAGX,iBAAiB,EAAE,iBAAiB,EAC3C,UAAU,EAAE,UAAU,EACR,SAAS,EAAE,SAAS,EACzC,GAAG,EAAE,GAAG;IAMlC,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,eAAe;YAST,IAAI;IAgDlB,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,oBAAoB;CAQ/B"}
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"}
@@ -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
- let LogSender = class LogSender {
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, "isSending", {
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, "batchSize", {
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: this.initialFailDuration * 512
61
+ value: false
80
62
  });
81
- Object.defineProperty(this, "failSendScenarioEnabled", {
63
+ Object.defineProperty(this, "onItemsChangedDisposer", {
82
64
  enumerable: true,
83
65
  configurable: true,
84
66
  writable: true,
85
- value: false
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: void 0
73
+ value: DEFAULT_SEND_INTERVAL
92
74
  });
93
- Object.defineProperty(this, "onItemsChangedDisposer", {
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
- /* send on items change */
83
+ dispose() {
84
+ window.clearInterval(this.intervalId);
85
+ }
102
86
  onItemsChanged(items) {
103
- if (items.length >= this.batchSize) {
87
+ if (items.length >= BATCH_SIZE) {
104
88
  this.send();
105
89
  }
106
90
  }
107
- /* send periodically */
108
91
  setSendInterval(interval) {
109
- window.clearInterval(this.sendInterval);
110
- this.sendInterval = window.setInterval(() => {
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
- async send() {
117
- if (this.isSending) {
118
- this.log.warning({
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
- /* istanbul ignore next */
126
- if (!this.connectionService.online) {
127
- throw new Error('cannot_send_while_offline');
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(this.batchSize);
132
- /* istanbul ignore next */
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.failSendScenarioEnabled) {
130
+ if (this.inSendFailedScenario) {
140
131
  this.setRegularSendScenario();
141
132
  }
142
133
  }
143
- catch (e) {
144
- if (!this.failSendScenarioEnabled) {
145
- this.setFailSendScenario();
146
- }
147
- else {
148
- this.increaseSendInterval();
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.failSendScenarioEnabled = false;
162
- this.failIntervalDuration = this.initialFailDuration;
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
- setFailSendScenario() {
151
+ setFailedSendScenario() {
167
152
  var _a;
168
- this.failSendScenarioEnabled = true;
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.failIntervalDuration >= this.maxIntervalDuration) {
174
- return;
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([
@@ -1 +1 @@
1
- {"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"log-sender.js","sourceRoot":"","sources":["../src/log-sender.ts"],"names":[],"mappings":"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"}
@@ -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":"AAGA,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;AAED,qBACa,UAAU;IASmB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAPhE,KAAK,EAAE,WAAW,EAAE,CAAM;IAE1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAK;IAE7B,OAAO,CAAC,UAAU,CAAS;gBAE4B,SAAS,EAAE,SAAS;IA2B3E,IAAI,MAAM,IAAI,SAAS,CAEtB;IAED,KAAK,CAAC,KAAK,EAAE,aAAa;IAgB1B,UAAU,CAAC,EAAE,EAAE,MAAM;IAKrB,SAAS,CAAC,SAAS,EAAE,MAAM;IAK3B,OAAO,CAAC,QAAQ;IAKhB,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,iBAAiB,GAE5B;IAED,OAAO,CAAC,cAAc;CAIzB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAMnD"}
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"}
@@ -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
- let LogStorage = class LogStorage {
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
- var _a;
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
- try {
56
- const items = (_a = JSON.parse(localStorage.getItem(this.storageKey))) !== null && _a !== void 0 ? _a : [];
57
- /* migrate error -> exception */
58
- items.forEach((item) => {
59
- if (typeof item.error === 'string') {
60
- item.exception = item.error;
61
- delete item.error;
62
- }
63
- });
64
- this.setItems(items);
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
- catch (e) {
67
- this.setItems([]);
68
- }
69
- this.nextItemId = lastId(this.items);
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 items = this.items.filter(item => item.id > id);
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;
@@ -1 +1 @@
1
- {"version":3,"file":"log-storage.js","sourceRoot":"","sources":["../src/log-storage.ts"],"names":[],"mappings":"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"}
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.1.0",
3
+ "version": "27.2.0",
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.1.0",
19
+ "@servicetitan/react-ioc": "27.2.0",
20
20
  "@testing-library/dom": "^10.4.0",
21
- "@testing-library/react": "^16.0.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.1",
24
+ "mobx": "~6.13.2",
25
25
  "react": "~18.3.1"
26
26
  },
27
27
  "peerDependencies": {
28
- "@servicetitan/react-ioc": "27.1.0",
28
+ "@servicetitan/react-ioc": "27.2.0",
29
29
  "axios": "~0.28.1",
30
- "mobx": ">=6.13.1",
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": "6eca03725ca72a85b405412c0b8cb934eb341366"
42
+ "gitHead": "311c957e8712703f03339865cd83b98e0140a523"
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 new Promise(process.nextTick); // resolve pending promises
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('logs warning', async () => {
118
- const spy = jest.spyOn(log, 'warning');
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(spy).toHaveBeenCalledWith(
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
- const entries = Array.isArray(entryOrEntries) ? entryOrEntries : [entryOrEntries];
13
- const logStorage = new LogStorage(logConfig);
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
- /* send on items change */
34
+ dispose() {
35
+ window.clearInterval(this.intervalId);
36
+ }
37
+
36
38
  private onItemsChanged(items: StorageItem[]) {
37
- if (items.length >= this.batchSize) {
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
- window.clearInterval(this.sendInterval);
45
- this.sendInterval = window.setInterval(() => {
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 async send() {
53
- if (this.isSending) {
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
- throw new Error('cannot_send_while_offline');
57
+ return;
64
58
  }
65
59
 
66
- this.isSending = true;
67
-
68
- try {
69
- const logEvents = this.logStorage.loadBatch(this.batchSize);
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
- /* istanbul ignore next */
72
- if (!logEvents.length) {
73
- throw new Error('zero_log_events');
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
- try {
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.failSendScenarioEnabled) {
89
+ if (this.inSendFailedScenario) {
81
90
  this.setRegularSendScenario();
82
91
  }
83
- } catch (e) {
84
- if (!this.failSendScenarioEnabled) {
85
- this.setFailSendScenario();
86
- } else {
87
- this.increaseSendInterval();
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.failSendScenarioEnabled = false;
102
- this.failIntervalDuration = this.initialFailDuration;
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 setFailSendScenario() {
112
- this.failSendScenarioEnabled = true;
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.failIntervalDuration >= this.maxIntervalDuration) {
120
- return;
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
  }
@@ -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
- try {
37
- const items = JSON.parse(localStorage.getItem(this.storageKey) as string) ?? [];
38
-
39
- /* migrate error -> exception */
40
- items.forEach((item: StorageItem & { error?: string }) => {
41
- if (typeof item.error === 'string') {
42
- item.exception = item.error;
43
- delete item.error;
44
- }
45
- });
46
- this.setItems(items);
47
- } catch (e) {
48
- this.setItems([]);
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
- this.nextItemId = lastId(this.items);
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 items = this.items.filter(item => item.id > id);
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;