@zajno/common 1.4.2 → 1.4.6

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.
Files changed (91) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +5 -5
  3. package/coverage/clover.xml +394 -107
  4. package/coverage/coverage-final.json +18 -13
  5. package/coverage/lcov-report/index.html +38 -38
  6. package/coverage/lcov-report/src/__tests__/helpers/index.html +1 -1
  7. package/coverage/lcov-report/src/__tests__/helpers/main.ts.html +1 -1
  8. package/coverage/lcov-report/src/async/arrays.ts.html +1 -1
  9. package/coverage/lcov-report/src/async/index.html +24 -9
  10. package/coverage/lcov-report/src/async/timeout.ts.html +230 -0
  11. package/coverage/lcov-report/src/cache.ts.html +623 -0
  12. package/coverage/lcov-report/src/dates/calc.ts.html +1 -1
  13. package/coverage/lcov-report/src/dates/convert.ts.html +1 -1
  14. package/coverage/lcov-report/src/dates/datex.ts.html +1 -1
  15. package/coverage/lcov-report/src/dates/format.ts.html +1 -1
  16. package/coverage/lcov-report/src/dates/index.html +1 -1
  17. package/coverage/lcov-report/src/dates/index.ts.html +1 -1
  18. package/coverage/lcov-report/src/dates/parse.ts.html +1 -1
  19. package/coverage/lcov-report/src/dates/period.ts.html +1 -1
  20. package/coverage/lcov-report/src/dates/shift.ts.html +1 -1
  21. package/coverage/lcov-report/src/dates/types.ts.html +1 -1
  22. package/coverage/lcov-report/src/dates/yearDate.ts.html +1 -1
  23. package/coverage/lcov-report/src/disposer.ts.html +332 -0
  24. package/coverage/lcov-report/src/enumHelper.ts.html +1 -1
  25. package/coverage/lcov-report/src/event.ts.html +1 -1
  26. package/coverage/lcov-report/src/fields/index.html +1 -1
  27. package/coverage/lcov-report/src/fields/update.ts.html +1 -1
  28. package/coverage/lcov-report/src/functions.ts.html +1 -1
  29. package/coverage/lcov-report/src/index.html +78 -18
  30. package/coverage/lcov-report/src/lazy.light.ts.html +1 -1
  31. package/coverage/lcov-report/src/logger/batch.ts.html +2 -2
  32. package/coverage/lcov-report/src/logger/console.ts.html +5 -5
  33. package/coverage/lcov-report/src/logger/index.html +1 -1
  34. package/coverage/lcov-report/src/logger/index.ts.html +22 -22
  35. package/coverage/lcov-report/src/logger/named.ts.html +13 -13
  36. package/coverage/lcov-report/src/logger/proxy.ts.html +7 -7
  37. package/coverage/lcov-report/src/math/arrays.ts.html +23 -23
  38. package/coverage/lcov-report/src/math/calc.ts.html +9 -9
  39. package/coverage/lcov-report/src/math/distribution.ts.html +2 -2
  40. package/coverage/lcov-report/src/math/index.html +1 -1
  41. package/coverage/lcov-report/src/math/index.ts.html +6 -6
  42. package/coverage/lcov-report/src/observers.ts.html +368 -0
  43. package/coverage/lcov-report/src/observersMap.ts.html +395 -0
  44. package/coverage/lcov-report/src/observingCache.ts.html +530 -0
  45. package/coverage/lcov-report/src/transitionObserver.ts.html +29 -8
  46. package/coverage/lcov-report/src/types.ts.html +5 -5
  47. package/coverage/lcov-report/src/validation/ValidationErrors.ts.html +1 -1
  48. package/coverage/lcov-report/src/validation/creditCard.ts.html +1 -1
  49. package/coverage/lcov-report/src/validation/helpers.ts.html +1 -1
  50. package/coverage/lcov-report/src/validation/index.html +1 -1
  51. package/coverage/lcov-report/src/validation/index.ts.html +1 -1
  52. package/coverage/lcov-report/src/validation/types.ts.html +1 -1
  53. package/coverage/lcov-report/src/validation/validators.ts.html +2 -2
  54. package/coverage/lcov-report/src/validation/wrappers.ts.html +1 -1
  55. package/coverage/lcov-report/src/viewModels/FlagModel.ts.html +1 -1
  56. package/coverage/lcov-report/src/viewModels/MultiSelectModel.ts.html +1 -1
  57. package/coverage/lcov-report/src/viewModels/NumberModel.ts.html +24 -24
  58. package/coverage/lcov-report/src/viewModels/SelectModel.ts.html +1 -1
  59. package/coverage/lcov-report/src/viewModels/Validatable.ts.html +1 -1
  60. package/coverage/lcov-report/src/viewModels/index.html +15 -15
  61. package/coverage/lcov-report/src/viewModels/wrappers.ts.html +1 -1
  62. package/coverage/lcov.info +741 -133
  63. package/lib/cache.d.ts +6 -2
  64. package/lib/cache.d.ts.map +1 -1
  65. package/lib/cache.js +20 -4
  66. package/lib/cache.js.map +1 -1
  67. package/lib/observersMap.d.ts +22 -0
  68. package/lib/observersMap.d.ts.map +1 -0
  69. package/lib/observersMap.js +93 -0
  70. package/lib/observersMap.js.map +1 -0
  71. package/lib/observingCache.d.ts +31 -0
  72. package/lib/observingCache.d.ts.map +1 -0
  73. package/lib/observingCache.js +118 -0
  74. package/lib/observingCache.js.map +1 -0
  75. package/lib/transitionObserver.d.ts +1 -0
  76. package/lib/transitionObserver.d.ts.map +1 -1
  77. package/lib/transitionObserver.js +8 -1
  78. package/lib/transitionObserver.js.map +1 -1
  79. package/lib/validation/validators.js +1 -1
  80. package/lib/viewModels/InitializableModel.d.ts +6 -0
  81. package/lib/viewModels/InitializableModel.d.ts.map +1 -0
  82. package/lib/viewModels/InitializableModel.js +23 -0
  83. package/lib/viewModels/InitializableModel.js.map +1 -0
  84. package/package.json +1 -1
  85. package/src/__tests__/observingCache.test.ts +127 -0
  86. package/src/cache.ts +25 -4
  87. package/src/observersMap.ts +105 -0
  88. package/src/observingCache.ts +150 -0
  89. package/src/transitionObserver.ts +7 -0
  90. package/src/validation/validators.ts +1 -1
  91. package/src/viewModels/InitializableModel.ts +17 -0
@@ -0,0 +1,127 @@
1
+ import { observable, reaction, runInAction, toJS } from 'mobx';
2
+ import { ObservingCache } from '../observingCache';
3
+ import { setTimeoutAsync } from '../async/timeout';
4
+
5
+ type TestItem = {
6
+ hello: string,
7
+ };
8
+
9
+ function createData() {
10
+ const Database: Record<string, TestItem> = observable({
11
+ '123': { hello: 'Hi, 123' },
12
+ });
13
+
14
+ const Repository = {
15
+ fetch: (key: string, cb: (value: TestItem) => void) => {
16
+ return reaction(() => Database[key], async item => {
17
+ await setTimeoutAsync(100);
18
+ cb(item);
19
+ }, { fireImmediately: true, delay: 100 });
20
+ },
21
+ };
22
+
23
+ const subscribeFn = jest.fn().mockImplementation(null);
24
+ const unsubFn = jest.fn().mockImplementation(null);
25
+
26
+ const Cache = new ObservingCache((key, cb) => {
27
+ subscribeFn(key);
28
+ const unsub = Repository.fetch(key, cb);
29
+ return () => {
30
+ unsubFn();
31
+ unsub();
32
+ };
33
+ });
34
+
35
+ return {
36
+ Database,
37
+ Repository,
38
+ Cache,
39
+ subscribeFn,
40
+ unsubFn,
41
+ };
42
+ }
43
+
44
+
45
+ describe('ObservingCache works with', () => {
46
+ it('no observing', async () => {
47
+ const { Database, Cache, subscribeFn, unsubFn } = createData();
48
+ try {
49
+ expect(Cache).not.toBeNull();
50
+ expect(subscribeFn).not.toHaveBeenCalled();
51
+ expect(unsubFn).not.toHaveBeenCalled();
52
+
53
+ const KEY = '123';
54
+
55
+ const deferred = Cache.get(KEY);
56
+
57
+ expect(deferred.busy).toBeUndefined();
58
+ expect(deferred.current).toBeUndefined();
59
+ expect(deferred.busy).toBeTruthy();
60
+
61
+ const expectedItem = toJS(Database[KEY]);
62
+
63
+ await expect(deferred.promise).resolves.toStrictEqual(expectedItem);
64
+
65
+ expect(deferred.current).toStrictEqual(expectedItem);
66
+ expect(deferred.busy).toBeFalsy();
67
+
68
+ expect(unsubFn).toHaveBeenCalledTimes(1);
69
+
70
+ expect(subscribeFn).toHaveBeenCalledTimes(1);
71
+ expect(subscribeFn).toHaveBeenCalledWith(KEY);
72
+
73
+ subscribeFn.mockClear();
74
+ unsubFn.mockClear();
75
+
76
+ Database[KEY] = { hello: 'bye' };
77
+
78
+ expect(subscribeFn).not.toHaveBeenCalled();
79
+ } finally {
80
+ Cache.dispose();
81
+ }
82
+ });
83
+
84
+ it('infinite observing', async () => {
85
+ const { Database, Cache, subscribeFn, unsubFn } = createData();
86
+ try {
87
+ Cache.useObservingStrategy(true);
88
+
89
+ const KEY = '123';
90
+
91
+ const deferred = Cache.get(KEY);
92
+
93
+ expect(deferred.busy).toBeUndefined();
94
+ expect(deferred.current).toBeUndefined();
95
+ expect(deferred.busy).toBeTruthy();
96
+
97
+ const expectedItem = toJS(Database[KEY]);
98
+
99
+ await expect(deferred.promise).resolves.toStrictEqual(expectedItem);
100
+
101
+ expect(deferred.current).toStrictEqual(expectedItem);
102
+ expect(deferred.busy).toBeFalsy();
103
+
104
+ expect(unsubFn).not.toHaveBeenCalled();
105
+
106
+ expect(subscribeFn).toHaveBeenCalledTimes(1);
107
+ expect(subscribeFn).toHaveBeenCalledWith(KEY);
108
+
109
+ subscribeFn.mockClear();
110
+ unsubFn.mockClear();
111
+
112
+ const replaceItem: TestItem = { hello: 'bye' };
113
+ runInAction(() => {
114
+ Database[KEY] = replaceItem;
115
+ });
116
+
117
+ expect(subscribeFn).not.toHaveBeenCalled();
118
+
119
+ await setTimeoutAsync(300);
120
+
121
+ expect(deferred.current).toStrictEqual(replaceItem);
122
+ } finally {
123
+ Cache.dispose();
124
+ expect(unsubFn).toHaveBeenCalledTimes(1);
125
+ }
126
+ });
127
+ });
package/src/cache.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { observable, makeObservable, runInAction, action } from 'mobx';
2
+ import { NumberModel } from './viewModels/NumberModel';
2
3
  import { createLogger, ILogger } from './logger';
3
4
 
4
5
  export type DeferredGetter<T> = {
@@ -28,14 +29,25 @@ export class PromiseCache<T, K = string> {
28
29
  private readonly _fetchCache: Record<string, Promise<T>> = { };
29
30
 
30
31
  private _logger: ILogger = null;
32
+ private _observeItems = false;
33
+
34
+ private readonly _busyCount = new NumberModel(0);
31
35
 
32
36
  constructor(
33
37
  readonly fetcher: (id: K) => Promise<T>,
34
38
  readonly keyAdapter?: K extends string ? null : (k: K) => string,
35
39
  readonly keyParser?: K extends string ? null : (id: string) => K,
36
- readonly observeItems = false,
40
+ observeItems = false,
37
41
  ) {
38
42
  makeObservable(this);
43
+ this._observeItems = observeItems;
44
+ }
45
+
46
+ public get busyCount() { return this._busyCount.value; }
47
+
48
+ useObserveItems(observeItems: boolean) {
49
+ this._observeItems = observeItems;
50
+ return this;
39
51
  }
40
52
 
41
53
  private _pk(k: K): string {
@@ -73,10 +85,12 @@ export class PromiseCache<T, K = string> {
73
85
  return this._itemsStatus[key];
74
86
  }
75
87
 
76
- getCurrent(id: K): T {
88
+ getCurrent(id: K, initiateFetch = true): T {
77
89
  const key = this._pk(id);
78
90
  const item = this._itemsCache[key];
79
- this.get(id);
91
+ if (initiateFetch) {
92
+ this.get(id);
93
+ }
80
94
  this._logger?.log(key, 'getCurrent returns', item);
81
95
  return item;
82
96
  }
@@ -104,17 +118,19 @@ export class PromiseCache<T, K = string> {
104
118
 
105
119
  private _doFetchAsync = async (id: K, key: string) => {
106
120
  try {
121
+ this._busyCount.increment();
107
122
  const res = await this.fetcher(id);
108
123
  if (this._fetchCache[key]) {
109
124
  this._logger?.log(key, 'item\'s <promise> resolved to', res);
110
125
  const result = res
111
- ? (this.observeItems ? observable.object(res) : res)
126
+ ? (this._observeItems ? observable.object(res) : res)
112
127
  : null;
113
128
  runInAction(() => this._itemsCache[key] = result);
114
129
  }
115
130
  return res;
116
131
  } finally {
117
132
  runInAction(() => {
133
+ this._busyCount.decrement();
118
134
  delete this._fetchCache[key];
119
135
  this._itemsStatus[key] = false;
120
136
  });
@@ -131,6 +147,11 @@ export class PromiseCache<T, K = string> {
131
147
  this._set(key, value, undefined, undefined);
132
148
  }
133
149
 
150
+ hasKey(id: K) {
151
+ const key = this._pk(id);
152
+ return this._itemsCache[key] !== undefined || this._itemsStatus[key] !== undefined;
153
+ }
154
+
134
155
  keys() {
135
156
  return Object.keys(this._itemsCache);
136
157
  }
@@ -0,0 +1,105 @@
1
+ import { createLogger, ILogger } from './logger';
2
+ import { combineDisposers, IDisposable } from './disposer';
3
+ import { NumberModel } from './viewModels/NumberModel';
4
+
5
+ type Unsub = () => void;
6
+
7
+ export class ObserversMap implements IDisposable {
8
+ /** Unsusbcrobers map: key => unsub fn */
9
+ private readonly _map = new Map<string, () => void>();
10
+ /** Timeouts map: key => timeout handle */
11
+ private readonly _timeouts = new Map<string, any>();
12
+
13
+ private readonly _logger: ILogger = null;
14
+ private readonly _count = new NumberModel();
15
+
16
+ constructor(readonly subscribe: null | ((key: string) => Promise<Unsub[]>), readonly name?: string) {
17
+ this._logger = createLogger(`[Observers:${this.name || '?'}]`);
18
+ }
19
+
20
+ public get count() { return this._count.value; }
21
+
22
+ public getIsObserving(key: string) {
23
+ return this._map.has(key);
24
+ }
25
+
26
+ public getHasObserveTimeout(key: string) {
27
+ return this.getIsObserving(key) && this._timeouts.has(key);
28
+ }
29
+
30
+ public async enable(key: string, enable: boolean, clearAfter: number = null, existingUnsubs: Unsub[] = null) {
31
+ if (enable === this.getIsObserving(key)) {
32
+ this.refreshTimeout(key, enable, clearAfter, true);
33
+ return;
34
+ }
35
+
36
+ if (enable) {
37
+ this._logger.log('Adding observer for key =', key, clearAfter ? `, clearAfter = ${clearAfter}` : '');
38
+
39
+ // this marker will help to determine whether unsubscribe was requested while we were in process of subscribing
40
+ let disabed = false;
41
+ const marker = () => { disabed = true; };
42
+
43
+ this._map.set(key, marker);
44
+
45
+ if (!this.subscribe && !existingUnsubs) {
46
+ throw new Error('Neither subscribe function nor existingUnsubs has been configured');
47
+ }
48
+
49
+ const unsubs = existingUnsubs || await this.subscribe(key);
50
+ const result = combineDisposers(...unsubs);
51
+
52
+ if (disabed) { // unsubscribe was requested
53
+ result();
54
+ } else {
55
+ this._map.set(key, result);
56
+ this._count.increment();
57
+ this.refreshTimeout(key, true, clearAfter);
58
+ }
59
+ } else {
60
+ this._logger.log('Removing observer for key =', key);
61
+ this.refreshTimeout(key, false);
62
+ const unsub = this._map.get(key);
63
+ this._map.delete(key);
64
+ unsub();
65
+ this._count.decrement();
66
+ }
67
+ }
68
+
69
+ private refreshTimeout(key: string, enable: boolean, timeout?: number, refresh = false) {
70
+ const current = this._timeouts.get(key);
71
+ if (current) {
72
+ clearTimeout(current);
73
+ this._timeouts.delete(key);
74
+ }
75
+
76
+ if (enable && refresh && current == null) {
77
+ // DO NOT setup new timeout because it's not intended to clear subscribtion if it was previously enabled for long term
78
+ return;
79
+ }
80
+
81
+ if (enable && timeout) {
82
+ const t = setTimeout(() => this.enable(key, false), timeout);
83
+ this._timeouts.set(key, t);
84
+ }
85
+ }
86
+
87
+ public clear() {
88
+ // Clear timeouts
89
+ for (const t of this._timeouts.values()) {
90
+ clearTimeout(t);
91
+ }
92
+ this._timeouts.clear();
93
+
94
+ // Invoke unsubscribers
95
+ for (const u of this._map.values()) {
96
+ u();
97
+ }
98
+ this._map.clear();
99
+ this._count.setValue(0);
100
+ }
101
+
102
+ public dispose() {
103
+ this.clear();
104
+ }
105
+ }
@@ -0,0 +1,150 @@
1
+ import { action } from 'mobx';
2
+ import { Disposable } from './disposer';
3
+ import { PromiseCache, DeferredGetter } from './cache';
4
+ import { ObserversMap } from './observersMap';
5
+ import { Fields } from './fields';
6
+
7
+ export type Unsub = () => void;
8
+ export type Fetcher<T> = (key: string, cb: (val: T) => Promise<void> | void) => Unsub | Promise<Unsub>;
9
+
10
+ type ObserveStrategy = boolean | 'short' | number;
11
+
12
+ export interface IObservingCache<T> {
13
+ get(key: string): DeferredGetter<T>;
14
+ }
15
+
16
+ export class ObservingCache<T> extends Disposable implements IObservingCache<T> {
17
+
18
+ private readonly _cache: PromiseCache<T>;
19
+ private readonly _observers: ObserversMap;
20
+
21
+ private _observeStrategy: ObserveStrategy = null;
22
+ private readonly _observeStrategyOverrides: Record<string, ObserveStrategy> = { };
23
+
24
+ private _updater: Fields.Updater<T> = null;
25
+
26
+ constructor(readonly fetcher: Fetcher<T>) {
27
+ super();
28
+
29
+ this._cache = new PromiseCache(this._fetch);
30
+ this._observers = new ObserversMap(this._subscribe);
31
+
32
+ this.disposer.add(this._observers);
33
+ }
34
+
35
+ public get loadingCount() { return this._cache.busyCount; }
36
+ public get observersCount() { return this._observers.count; }
37
+
38
+ useObservingStrategy(observe: ObserveStrategy) {
39
+ this._observeStrategy = observe;
40
+ if (!this._observeStrategy) {
41
+ this._observers.clear();
42
+ } else {
43
+ const currentKeys = this._cache.keys();
44
+ const timeout = getObserveTimeout(this._observeStrategy);
45
+ currentKeys.forEach(key => this._observers.enable(key, true, timeout));
46
+ }
47
+
48
+ return this;
49
+ }
50
+
51
+ useUpdater(updater: Fields.Updater<T>) {
52
+ this._updater = updater;
53
+ this._cache.useObserveItems(updater != null);
54
+ return this;
55
+ }
56
+
57
+ getIsCached(key: string) {
58
+ return this._cache.hasKey(key);
59
+ }
60
+
61
+ getCurent(key: string) {
62
+ return this._cache.getCurrent(key, false);
63
+ }
64
+
65
+ get(key: string, overrideStrategy?: ObserveStrategy): DeferredGetter<T> {
66
+ if (overrideStrategy !== undefined) {
67
+ this._observeStrategyOverrides[key] = overrideStrategy;
68
+ }
69
+
70
+ const strategy = firstDefined(this._observeStrategyOverrides[key], this._observeStrategy);
71
+
72
+ if (strategy && !this._observers.getIsObserving(key)) {
73
+ // ensure observe
74
+ if (this._cache.hasKey(key)) {
75
+ // the request has been initiated already
76
+ const timeout = getObserveTimeout(strategy);
77
+ this._observers.enable(key, true, timeout);
78
+ }
79
+ }
80
+
81
+ return this._cache.getDeferred(key);
82
+ }
83
+
84
+ populate = (key: string, item: T) => {
85
+ this._updateItem(key, item);
86
+ };
87
+
88
+ private _fetch = async (key: string): Promise<T> => {
89
+ let firstLoad = true;
90
+
91
+ return new Promise<T>((resolve) => {
92
+ Promise.resolve<Unsub>(
93
+ this.fetcher(key, item => {
94
+ if (firstLoad) {
95
+ resolve(item);
96
+ firstLoad = false;
97
+ } else {
98
+ this._updateItem(key, item);
99
+ }
100
+ })
101
+ ).then(unsub => {
102
+ const strategy = firstDefined(this._observeStrategyOverrides[key], this._observeStrategy);
103
+ if (!strategy) {
104
+ // immediate unsub in case no observing strategy has been set
105
+ unsub();
106
+ } else {
107
+ const timeout = getObserveTimeout(strategy);
108
+ this._observers.enable(key, true, timeout, [unsub]);
109
+ }
110
+ });
111
+ });
112
+ };
113
+
114
+ private _subscribe = (key: string) => {
115
+ return Promise.all([
116
+ this.fetcher(key, item => this._updateItem(key, item)),
117
+ ]);
118
+ };
119
+
120
+ private _updateItem = action((key: string, item: T) => {
121
+ if (this._updater != null) {
122
+ const current = this._cache.getCurrent(key, false);
123
+ if (current != null) {
124
+ const result = this._updater(current, item);
125
+ // re-set existing item but with updated contents
126
+ this._cache.updateValueDirectly(key, result);
127
+ return;
128
+ }
129
+ }
130
+
131
+ this._cache.updateValueDirectly(key, item);
132
+ });
133
+ }
134
+
135
+ function getObserveTimeout(s: ObserveStrategy) {
136
+ if (!s) {
137
+ return undefined;
138
+ }
139
+
140
+ return typeof s === 'number'
141
+ ? s
142
+ : (s === 'short'
143
+ ? 5 * 60 * 1000
144
+ : null
145
+ );
146
+ }
147
+
148
+ function firstDefined<T>(...values: (T | undefined)[]) {
149
+ return values.find(v => v !== undefined);
150
+ }
@@ -174,3 +174,10 @@ export class TransitionObserver<T> implements IDisposable {
174
174
  }
175
175
  }
176
176
  }
177
+
178
+ export function waitFor<T>(current: () => T, toBe: T) {
179
+ return new TransitionObserver(current)
180
+ .to(toBe)
181
+ .fireOnce()
182
+ .getPromise();
183
+ }
@@ -22,7 +22,7 @@ export const ValidatorsRegExps = {
22
22
  expiryDateCard: /^\d+$/,
23
23
  cvv: /^\d+$/,
24
24
  // eslint-disable-next-line no-useless-escape
25
- website: /^(https?:\/\/)?([a-z0-9]+(-?[a-z0-9]+\.))+[a-z]{2,}(:[0-9]{1,})?(\/.*)?$/i,
25
+ website: /^(https?:\/\/)?([a-z0-9]+([a-z0-9\-]+\.))+[a-z]{2,}(:[0-9]{1,})?(\/.*)?$/i,
26
26
  linkedin: /^(https?:\/\/)?([a-z]+\d*[a-z]*\.)?linkedin\.com\/.*$/i,
27
27
  };
28
28
 
@@ -0,0 +1,17 @@
1
+ import NumberModel from './NumberModel';
2
+
3
+ export class InitializableModel {
4
+ private readonly _counter = new NumberModel(0);
5
+
6
+ public get initializing() { return this._counter.value > 0; }
7
+
8
+ protected async runOperation<T>(operation: () => Promise<T> | T): Promise<T> {
9
+ try {
10
+ this._counter.increment();
11
+ const res = await operation();
12
+ return res;
13
+ } finally {
14
+ this._counter.decrement();
15
+ }
16
+ }
17
+ }