@onekeyfe/hd-core 1.0.25 → 1.0.26
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/api/CheckBootloaderRelease.d.ts +1 -0
- package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/firmware/releaseHelper.d.ts +1 -0
- package/dist/api/firmware/releaseHelper.d.ts.map +1 -1
- package/dist/core/RequestQueue.d.ts +23 -0
- package/dist/core/RequestQueue.d.ts.map +1 -0
- package/dist/core/index.d.ts +8 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +5 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DeviceCommands.d.ts +84 -7
- package/dist/device/DeviceCommands.d.ts.map +1 -1
- package/dist/index.d.ts +55 -7
- package/dist/index.js +544 -153
- package/dist/utils/getMutex.d.ts +2 -0
- package/dist/utils/getMutex.d.ts.map +1 -0
- package/dist/utils/getSynchronize.d.ts +4 -0
- package/dist/utils/getSynchronize.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/api/BaseMethod.ts +1 -1
- package/src/api/FirmwareUpdateV2.ts +10 -0
- package/src/api/firmware/releaseHelper.ts +1 -0
- package/src/core/RequestQueue.ts +107 -0
- package/src/core/index.ts +229 -53
- package/src/device/Device.ts +47 -4
- package/src/device/DeviceCommands.ts +212 -22
- package/src/utils/getMutex.ts +41 -0
- package/src/utils/getSynchronize.ts +25 -0
package/src/device/Device.ts
CHANGED
|
@@ -102,6 +102,16 @@ export class Device extends EventEmitter {
|
|
|
102
102
|
// @ts-expect-error: strictPropertyInitialization
|
|
103
103
|
commands: DeviceCommands;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 可取消的操作
|
|
107
|
+
*/
|
|
108
|
+
private cancelableAction?: (err?: Error) => Promise<unknown>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 设备是否被占用
|
|
112
|
+
*/
|
|
113
|
+
private deviceAcquired = false;
|
|
114
|
+
|
|
105
115
|
/**
|
|
106
116
|
* 设备信息
|
|
107
117
|
*/
|
|
@@ -221,6 +231,7 @@ export class Device extends EventEmitter {
|
|
|
221
231
|
);
|
|
222
232
|
Log.debug('Expected session id:', this.mainId);
|
|
223
233
|
}
|
|
234
|
+
this.deviceAcquired = true;
|
|
224
235
|
this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
|
|
225
236
|
if (this.commands) {
|
|
226
237
|
await this.commands.dispose(false);
|
|
@@ -262,6 +273,7 @@ export class Device extends EventEmitter {
|
|
|
262
273
|
this.needReloadDevice = true;
|
|
263
274
|
}
|
|
264
275
|
}
|
|
276
|
+
this.deviceAcquired = false;
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
getCommands() {
|
|
@@ -481,6 +493,12 @@ export class Device extends EventEmitter {
|
|
|
481
493
|
)
|
|
482
494
|
);
|
|
483
495
|
}
|
|
496
|
+
} else if (env === 'react-native') {
|
|
497
|
+
// TODO: implement react-native acquire
|
|
498
|
+
// cancel input pin or passphrase on device request, then the following requests will report an error
|
|
499
|
+
if (this.commands) {
|
|
500
|
+
this.commands.disposed = false;
|
|
501
|
+
}
|
|
484
502
|
}
|
|
485
503
|
}
|
|
486
504
|
|
|
@@ -542,14 +560,31 @@ export class Device extends EventEmitter {
|
|
|
542
560
|
}
|
|
543
561
|
|
|
544
562
|
async interruptionFromUser() {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
563
|
+
const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
|
|
564
|
+
await this.cancelableAction?.(error);
|
|
565
|
+
await this.commands?.cancel();
|
|
566
|
+
|
|
548
567
|
if (this.runPromise) {
|
|
549
|
-
this.runPromise.reject(
|
|
568
|
+
this.runPromise.reject(error);
|
|
569
|
+
this.runPromise = null;
|
|
550
570
|
}
|
|
551
571
|
}
|
|
552
572
|
|
|
573
|
+
setCancelableAction(callback: NonNullable<typeof this.cancelableAction>) {
|
|
574
|
+
this.cancelableAction = (e?: Error) =>
|
|
575
|
+
callback(e)
|
|
576
|
+
.catch(e2 => {
|
|
577
|
+
Log.debug('cancelableAction error', e2);
|
|
578
|
+
})
|
|
579
|
+
.finally(() => {
|
|
580
|
+
this.clearCancelableAction();
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
clearCancelableAction() {
|
|
585
|
+
this.cancelableAction = undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
553
588
|
getMode() {
|
|
554
589
|
if (this.features?.bootloader_mode) {
|
|
555
590
|
// bootloader mode
|
|
@@ -584,6 +619,14 @@ export class Device extends EventEmitter {
|
|
|
584
619
|
return typeof this.originalDescriptor.session === 'string';
|
|
585
620
|
}
|
|
586
621
|
|
|
622
|
+
hasDeviceAcquire() {
|
|
623
|
+
const env = DataManager.getSettings('env');
|
|
624
|
+
if (DataManager.isBleConnect(env)) {
|
|
625
|
+
return this.deviceAcquired;
|
|
626
|
+
}
|
|
627
|
+
return this.isUsed() && this.deviceAcquired;
|
|
628
|
+
}
|
|
629
|
+
|
|
587
630
|
isUsedHere() {
|
|
588
631
|
const env = DataManager.getSettings('env');
|
|
589
632
|
if (DataManager.isBleConnect(env)) {
|
|
@@ -2,9 +2,16 @@ import type { Transport, Messages, FailureType } from '@onekeyfe/hd-transport';
|
|
|
2
2
|
import { ERRORS, HardwareError, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
3
|
import TransportManager from '../data-manager/TransportManager';
|
|
4
4
|
import DataManager from '../data-manager/DataManager';
|
|
5
|
-
import { patchFeatures, getLogger, LoggerNames } from '../utils';
|
|
5
|
+
import { patchFeatures, getLogger, LoggerNames, getDeviceType } from '../utils';
|
|
6
6
|
import type { Device } from './Device';
|
|
7
7
|
import { DEVICE } from '../events';
|
|
8
|
+
import { DeviceModelToTypes } from '../types';
|
|
9
|
+
|
|
10
|
+
export type PassphrasePromptResponse = {
|
|
11
|
+
passphrase?: string;
|
|
12
|
+
passphraseOnDevice?: boolean;
|
|
13
|
+
cache?: boolean;
|
|
14
|
+
};
|
|
8
15
|
|
|
9
16
|
type MessageType = Messages.MessageType;
|
|
10
17
|
type MessageKey = keyof MessageType;
|
|
@@ -17,12 +24,6 @@ type TypedCallResponseMap = {
|
|
|
17
24
|
};
|
|
18
25
|
export type DefaultMessageResponse = TypedCallResponseMap[keyof MessageType];
|
|
19
26
|
|
|
20
|
-
export type PassphrasePromptResponse = {
|
|
21
|
-
passphrase?: string;
|
|
22
|
-
passphraseOnDevice?: boolean;
|
|
23
|
-
cache?: boolean;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
27
|
const assertType = (res: DefaultMessageResponse, resType: string | string[]) => {
|
|
27
28
|
const splitResTypes = Array.isArray(resType) ? resType : resType.split('|');
|
|
28
29
|
if (!splitResTypes.includes(res.type)) {
|
|
@@ -33,8 +34,90 @@ const assertType = (res: DefaultMessageResponse, resType: string | string[]) =>
|
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
export const cancelDeviceInPrompt = (device: Device, expectResponse = true) => {
|
|
38
|
+
const session = device.hasDeviceAcquire() ? device.mainId : undefined;
|
|
39
|
+
|
|
40
|
+
if (!session) {
|
|
41
|
+
// device disconnected or acquired by someone else
|
|
42
|
+
return Promise.resolve({
|
|
43
|
+
success: false,
|
|
44
|
+
error: HardwareErrorCode.RuntimeError,
|
|
45
|
+
payload: {
|
|
46
|
+
message: 'Device disconnected or acquired by someone else',
|
|
47
|
+
},
|
|
48
|
+
} as const);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const transport = device.commands?.transport;
|
|
52
|
+
|
|
53
|
+
if (expectResponse) {
|
|
54
|
+
return transport
|
|
55
|
+
?.call(session, 'Cancel', {})
|
|
56
|
+
.then(() => ({
|
|
57
|
+
success: true,
|
|
58
|
+
error: null,
|
|
59
|
+
payload: {
|
|
60
|
+
message: 'Cancel request sent',
|
|
61
|
+
},
|
|
62
|
+
}))
|
|
63
|
+
.catch((error: HardwareError) => ({
|
|
64
|
+
success: false,
|
|
65
|
+
error: error.errorCode,
|
|
66
|
+
payload: {
|
|
67
|
+
message: error.message,
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return transport?.post(session, 'Cancel', {}).then(() => ({
|
|
73
|
+
success: true,
|
|
74
|
+
error: HardwareErrorCode.RuntimeError,
|
|
75
|
+
payload: {
|
|
76
|
+
message: 'Cancel request sent',
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const cancelDeviceWithInitialize = (device: Device) => {
|
|
82
|
+
const session = device.hasDeviceAcquire() ? device.mainId : undefined;
|
|
83
|
+
|
|
84
|
+
if (!session) {
|
|
85
|
+
// device disconnected or acquired by someone else
|
|
86
|
+
return Promise.resolve({
|
|
87
|
+
success: false,
|
|
88
|
+
error: HardwareErrorCode.RuntimeError,
|
|
89
|
+
payload: {
|
|
90
|
+
message: 'Device disconnected or acquired by someone else',
|
|
91
|
+
},
|
|
92
|
+
} as const);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const transport = device.commands?.transport;
|
|
96
|
+
|
|
97
|
+
return transport
|
|
98
|
+
?.call(session, 'Initialize', {})
|
|
99
|
+
.then(() => ({
|
|
100
|
+
success: true,
|
|
101
|
+
error: null,
|
|
102
|
+
payload: {
|
|
103
|
+
message: 'Cancel request sent',
|
|
104
|
+
},
|
|
105
|
+
}))
|
|
106
|
+
.catch((error: HardwareError) => ({
|
|
107
|
+
success: false,
|
|
108
|
+
error: error.errorCode,
|
|
109
|
+
payload: {
|
|
110
|
+
message: error.message,
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
|
|
36
115
|
const Log = getLogger(LoggerNames.DeviceCommands);
|
|
37
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The life cycle begins with the acquisition of the device and ends with the disposal device commands
|
|
119
|
+
* acquire device -> create DeviceCommands -> release device -> dispose DeviceCommands
|
|
120
|
+
*/
|
|
38
121
|
export class DeviceCommands {
|
|
39
122
|
device: Device;
|
|
40
123
|
|
|
@@ -46,8 +129,6 @@ export class DeviceCommands {
|
|
|
46
129
|
|
|
47
130
|
callPromise?: Promise<DefaultMessageResponse>;
|
|
48
131
|
|
|
49
|
-
_cancelableRequest?: (error?: any) => void;
|
|
50
|
-
|
|
51
132
|
constructor(device: Device, mainId: string) {
|
|
52
133
|
this.device = device;
|
|
53
134
|
this.mainId = mainId;
|
|
@@ -55,15 +136,75 @@ export class DeviceCommands {
|
|
|
55
136
|
this.disposed = false;
|
|
56
137
|
}
|
|
57
138
|
|
|
58
|
-
async dispose(
|
|
139
|
+
async dispose(_cancelRequest: boolean) {
|
|
59
140
|
this.disposed = true;
|
|
60
|
-
if (cancelRequest && this._cancelableRequest) {
|
|
61
|
-
this._cancelableRequest();
|
|
62
|
-
}
|
|
63
|
-
this._cancelableRequest = undefined;
|
|
64
141
|
await this.transport.cancel?.();
|
|
65
142
|
}
|
|
66
143
|
|
|
144
|
+
checkDisposed() {
|
|
145
|
+
if (this.disposed) {
|
|
146
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'DeviceCommands already disposed');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// on device input pin or passphrase, cancel the request with initialize
|
|
151
|
+
async cancelDeviceOnOneKeyDevice() {
|
|
152
|
+
const { name } = this.transport;
|
|
153
|
+
if (name === 'HttpTransport') {
|
|
154
|
+
/**
|
|
155
|
+
* Bridge throws "other call in progress" error.
|
|
156
|
+
* as workaround takeover transportSession (acquire) before sending Cancel, this will resolve previous pending call.
|
|
157
|
+
*/
|
|
158
|
+
try {
|
|
159
|
+
await this.device.acquire();
|
|
160
|
+
await cancelDeviceWithInitialize(this.device);
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore whatever happens
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
return cancelDeviceWithInitialize(this.device);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async cancelDevice() {
|
|
170
|
+
const { name } = this.transport;
|
|
171
|
+
if (name === 'HttpTransport') {
|
|
172
|
+
/**
|
|
173
|
+
* Bridge throws "other call in progress" error.
|
|
174
|
+
* as workaround takeover transportSession (acquire) before sending Cancel, this will resolve previous pending call.
|
|
175
|
+
*/
|
|
176
|
+
try {
|
|
177
|
+
await this.device.acquire();
|
|
178
|
+
await cancelDeviceInPrompt(this.device, false);
|
|
179
|
+
} catch {
|
|
180
|
+
// ignore whatever happens
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
return cancelDeviceInPrompt(this.device, false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async cancel() {
|
|
188
|
+
if (this.disposed) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.dispose(true);
|
|
192
|
+
if (this.callPromise) {
|
|
193
|
+
try {
|
|
194
|
+
await Promise.all([
|
|
195
|
+
new Promise((_resolve, reject) =>
|
|
196
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
197
|
+
setTimeout(() => reject(new Error('cancel timeout')), 10 * 1000)
|
|
198
|
+
),
|
|
199
|
+
await this.callPromise,
|
|
200
|
+
]);
|
|
201
|
+
} catch {
|
|
202
|
+
// device error
|
|
203
|
+
this.callPromise = undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
67
208
|
// Sends an async message to the opened device.
|
|
68
209
|
async call(
|
|
69
210
|
type: MessageKey,
|
|
@@ -199,6 +340,7 @@ export class DeviceCommands {
|
|
|
199
340
|
// ignore
|
|
200
341
|
}
|
|
201
342
|
|
|
343
|
+
this.device.clearCancelableAction();
|
|
202
344
|
if (res.type === 'Failure') {
|
|
203
345
|
const { code, message } = res.message as {
|
|
204
346
|
code?: string | FailureType;
|
|
@@ -261,6 +403,12 @@ export class DeviceCommands {
|
|
|
261
403
|
}
|
|
262
404
|
|
|
263
405
|
if (res.type === 'ButtonRequest') {
|
|
406
|
+
const deviceType = getDeviceType(this.device.features);
|
|
407
|
+
if (DeviceModelToTypes.model_mini.includes(deviceType)) {
|
|
408
|
+
this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice());
|
|
409
|
+
} else {
|
|
410
|
+
this.device.setCancelableAction(() => this.cancelDevice());
|
|
411
|
+
}
|
|
264
412
|
if (res.message.code === 'ButtonRequest_PassphraseEntry') {
|
|
265
413
|
this.device.emit(DEVICE.PASSPHRASE_ON_DEVICE, this.device);
|
|
266
414
|
} else {
|
|
@@ -277,11 +425,15 @@ export class DeviceCommands {
|
|
|
277
425
|
return this._promptPin(res.message.type).then(
|
|
278
426
|
pin => {
|
|
279
427
|
if (pin === '@@ONEKEY_INPUT_PIN_IN_DEVICE') {
|
|
280
|
-
|
|
428
|
+
// only classic\1s\mini\pure
|
|
429
|
+
this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice());
|
|
430
|
+
return this._commonCall('BixinPinInputOnDevice').finally(() => {
|
|
431
|
+
this.device.clearCancelableAction();
|
|
432
|
+
});
|
|
281
433
|
}
|
|
282
434
|
return this._commonCall('PinMatrixAck', { pin });
|
|
283
435
|
},
|
|
284
|
-
|
|
436
|
+
error => Promise.reject(error)
|
|
285
437
|
);
|
|
286
438
|
}
|
|
287
439
|
|
|
@@ -308,12 +460,31 @@ export class DeviceCommands {
|
|
|
308
460
|
|
|
309
461
|
_promptPin(type?: Messages.PinMatrixRequestType) {
|
|
310
462
|
return new Promise<string>((resolve, reject) => {
|
|
463
|
+
const cancelAndReject = (_error?: Error) =>
|
|
464
|
+
cancelDeviceInPrompt(this.device, false)
|
|
465
|
+
.then(onCancel => {
|
|
466
|
+
const error = ERRORS.TypedError(
|
|
467
|
+
HardwareErrorCode.ActionCancelled,
|
|
468
|
+
`${DEVICE.PIN} canceled`
|
|
469
|
+
);
|
|
470
|
+
// onCancel not void
|
|
471
|
+
if (onCancel) {
|
|
472
|
+
const { payload } = onCancel || {};
|
|
473
|
+
reject(error || new Error(payload?.message));
|
|
474
|
+
} else {
|
|
475
|
+
reject(error);
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
.catch(error => {
|
|
479
|
+
reject(error);
|
|
480
|
+
});
|
|
481
|
+
|
|
311
482
|
if (this.device.listenerCount(DEVICE.PIN) > 0) {
|
|
312
|
-
this.
|
|
483
|
+
this.device.setCancelableAction(cancelAndReject);
|
|
313
484
|
this.device.emit(DEVICE.PIN, this.device, type, (err, pin) => {
|
|
314
|
-
this.
|
|
485
|
+
this.device.clearCancelableAction();
|
|
315
486
|
if (err) {
|
|
316
|
-
|
|
487
|
+
cancelAndReject(err);
|
|
317
488
|
} else {
|
|
318
489
|
resolve(pin);
|
|
319
490
|
}
|
|
@@ -332,15 +503,34 @@ export class DeviceCommands {
|
|
|
332
503
|
|
|
333
504
|
_promptPassphrase() {
|
|
334
505
|
return new Promise<PassphrasePromptResponse>((resolve, reject) => {
|
|
506
|
+
const cancelAndReject = (_error?: Error) =>
|
|
507
|
+
cancelDeviceInPrompt(this.device, false)
|
|
508
|
+
.then(onCancel => {
|
|
509
|
+
const error = ERRORS.TypedError(
|
|
510
|
+
HardwareErrorCode.ActionCancelled,
|
|
511
|
+
`${DEVICE.PASSPHRASE} canceled`
|
|
512
|
+
);
|
|
513
|
+
// onCancel not void
|
|
514
|
+
if (onCancel) {
|
|
515
|
+
const { payload } = onCancel || {};
|
|
516
|
+
reject(error || new Error(payload?.message));
|
|
517
|
+
} else {
|
|
518
|
+
reject(error);
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
.catch(error => {
|
|
522
|
+
reject(error);
|
|
523
|
+
});
|
|
524
|
+
|
|
335
525
|
if (this.device.listenerCount(DEVICE.PASSPHRASE) > 0) {
|
|
336
|
-
this.
|
|
526
|
+
this.device.setCancelableAction(cancelAndReject);
|
|
337
527
|
this.device.emit(
|
|
338
528
|
DEVICE.PASSPHRASE,
|
|
339
529
|
this.device,
|
|
340
530
|
(response: PassphrasePromptResponse, error?: Error) => {
|
|
341
|
-
this.
|
|
531
|
+
this.device.clearCancelableAction();
|
|
342
532
|
if (error) {
|
|
343
|
-
|
|
533
|
+
cancelAndReject(error);
|
|
344
534
|
} else {
|
|
345
535
|
resolve(response);
|
|
346
536
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures that since the awaited lock is obtained until unlock return from it
|
|
3
|
+
* is called, no other part of code can enter the same lock.
|
|
4
|
+
*
|
|
5
|
+
* Optionally, it also takes `lockId` param, in which case only actions with
|
|
6
|
+
* the same lock id are blocking each other.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* const lock = getMutex();
|
|
12
|
+
*
|
|
13
|
+
* lock().then(unlock => writeToSocket('foo').finally(unlock));
|
|
14
|
+
* lock().then(unlock => writeToSocket('bar').finally(unlock));
|
|
15
|
+
* lock('differentLockId').then(unlock => writeToAnotherSocket('baz').finally(unlock));
|
|
16
|
+
*
|
|
17
|
+
* const unlock = await lock();
|
|
18
|
+
* await readFromSocket();
|
|
19
|
+
* unlock();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const getMutex = () => {
|
|
23
|
+
// eslint-disable-next-line symbol-description
|
|
24
|
+
const DEFAULT_ID = Symbol();
|
|
25
|
+
const locks: Record<keyof any, Promise<void>> = {};
|
|
26
|
+
|
|
27
|
+
return async (lockId: keyof any = DEFAULT_ID) => {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
29
|
+
while (locks[lockId]) {
|
|
30
|
+
await locks[lockId];
|
|
31
|
+
}
|
|
32
|
+
let resolve = () => {};
|
|
33
|
+
locks[lockId] = new Promise<void>(res => {
|
|
34
|
+
resolve = res;
|
|
35
|
+
}).finally(() => {
|
|
36
|
+
delete locks[lockId];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return resolve;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getMutex } from './getMutex';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures that all async actions passed to the returned function are called
|
|
5
|
+
* immediately one after another, without interfering with each other.
|
|
6
|
+
* Optionally, it also takes `lockId` param, in which case only actions with
|
|
7
|
+
* the same lock id are blocking each other.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* const synchronize = getSynchronize();
|
|
13
|
+
* synchronize(() => asyncAction1());
|
|
14
|
+
* synchronize(() => asyncAction2());
|
|
15
|
+
* synchronize(() => asyncAction3(), 'differentLockId');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const getSynchronize = (mutex?: ReturnType<typeof getMutex>) => {
|
|
19
|
+
const lock = mutex ?? getMutex();
|
|
20
|
+
|
|
21
|
+
return <T>(action: () => T, lockId?: keyof any): T extends Promise<unknown> ? T : Promise<T> =>
|
|
22
|
+
lock(lockId).then(unlock => Promise.resolve().then(action).finally(unlock)) as any;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type Synchronize = ReturnType<typeof getSynchronize>;
|