@spirobel/monero-wallet-api 0.5.4 → 0.5.5
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/io/sleep.d.ts +1 -1
- package/dist/io/sleep.js +16 -1
- package/dist/scanning-syncing/backgroundWorker.d.ts +2 -0
- package/dist/scanning-syncing/backgroundWorker.js +92 -10
- package/dist/scanning-syncing/blocksbuffer/blocksBufferFetchLoop.js +33 -2
- package/dist/scanning-syncing/connectionStatus.d.ts +6 -0
- package/dist/scanning-syncing/connectionStatus.js +15 -0
- package/dist/scanning-syncing/connectionStatusOpened.d.ts +2 -0
- package/dist/scanning-syncing/connectionStatusOpened.js +20 -3
- package/dist/scanning-syncing/openWallet.d.ts +4 -1
- package/dist/scanning-syncing/openWallet.js +3 -0
- package/dist/scanning-syncing/scanresult/scanCacheOpened.d.ts +10 -4
- package/dist/scanning-syncing/scanresult/scanCacheOpened.js +117 -56
- package/dist/scanning-syncing/scanresult/scanCoordination.d.ts +6 -3
- package/dist/scanning-syncing/scanresult/scanCoordination.js +87 -25
- package/dist/scanning-syncing/scanresult/scanLoop.d.ts +1 -2
- package/dist/scanning-syncing/scanresult/scanLoop.js +0 -13
- package/dist/scanning-syncing/worker-entrypoints/worker.js +1 -1
- package/dist/scanning-syncing/worker-mains/coordinator-main.d.ts +1 -0
- package/dist/scanning-syncing/worker-mains/coordinator-main.js +16 -1
- package/dist/scanning-syncing/worker-mains/cpubound-main.js +3 -2
- package/dist/scanning-syncing/worker-mains/worker.js +45 -2
- package/package.json +1 -1
|
@@ -37,11 +37,13 @@ export class ScanCacheOpened {
|
|
|
37
37
|
/** sample sizes to try when decoyRetry is enabled, in order */
|
|
38
38
|
decoyRetrySizes = [20, 50, 100, 200, 500];
|
|
39
39
|
static async create(params) {
|
|
40
|
-
|
|
40
|
+
// same dir as workers / connection status: next to ScanSettings.json
|
|
41
|
+
const pathPrefix = getPathPrefix(params.scan_settings_path, params.pathPrefix);
|
|
42
|
+
const theCatchToBeOpened = await readCacheFileDefaultLocation(params.primary_address, pathPrefix);
|
|
41
43
|
if (!params.primary_address)
|
|
42
44
|
throw new Error(`primary_address is required, potentially half filled out wallet setting in: ${params.scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT}`);
|
|
43
45
|
// use ScanSettingsOpened instead of direct scanSettings calls
|
|
44
|
-
const scanSettings = await ScanSettingsOpened.create(params.scan_settings_path,
|
|
46
|
+
const scanSettings = await ScanSettingsOpened.create(params.scan_settings_path, pathPrefix);
|
|
45
47
|
const walletSettings = scanSettings.getWallet(params.primary_address);
|
|
46
48
|
if (!walletSettings)
|
|
47
49
|
throw new Error(`wallet not found in settings. did you call openwallet with the right params?
|
|
@@ -55,7 +57,7 @@ export class ScanCacheOpened {
|
|
|
55
57
|
// read secret_view_key and secret_spend_key from env
|
|
56
58
|
const walletSettingsWithKeys = await walletSettingsPlusKeys(walletWithSettings);
|
|
57
59
|
// create viewpair + ScanCacheOpened instance
|
|
58
|
-
const scanCacheOpen = new ScanCacheOpened(scanSettings, await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, scanSettings.node_url), walletSettings.wallet_route, walletSettings.wallet_name, walletSettings.wallet_slot, params.no_worker || false, params.masterCacheChanged || null, scanSettings.start_height, params.scan_settings_path,
|
|
60
|
+
const scanCacheOpen = new ScanCacheOpened(scanSettings, await ViewPair.create(params.primary_address, walletSettingsWithKeys.secret_view_key, walletSettings.subaddress_index, scanSettings.node_url), walletSettings.wallet_route, walletSettings.wallet_name, walletSettings.wallet_slot, params.no_worker || false, params.masterCacheChanged || null, scanSettings.start_height, params.scan_settings_path, pathPrefix, params.workerError);
|
|
59
61
|
if (theCatchToBeOpened)
|
|
60
62
|
scanCacheOpen._cache = theCatchToBeOpened;
|
|
61
63
|
if (!walletSettings.halted) {
|
|
@@ -66,7 +68,7 @@ export class ScanCacheOpened {
|
|
|
66
68
|
scanCacheOpen._highest_subaddress_index =
|
|
67
69
|
walletSettings.subaddress_index || SUB_ADDRESS_INDEX_DEFAULT_VALUE;
|
|
68
70
|
if (!params.no_stats) {
|
|
69
|
-
scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address,
|
|
71
|
+
scanCacheOpen._stats = await alignScanStatsWithCache(scanCacheOpen._cache, scanCacheOpen.view_pair, params.primary_address, pathPrefix, walletSettings.subaddress_index, lastRange(scanCacheOpen._cache.scanned_ranges)?.end);
|
|
70
72
|
}
|
|
71
73
|
else {
|
|
72
74
|
scanCacheOpen._no_stats = params.no_stats; // true
|
|
@@ -83,8 +85,10 @@ export class ScanCacheOpened {
|
|
|
83
85
|
return this._highest_subaddress_index;
|
|
84
86
|
}
|
|
85
87
|
get current_height() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// progress on the range that covers start_height (start can jump;
|
|
89
|
+
// lastRange would pick a different segment and be wrong)
|
|
90
|
+
const current_range = findRange(this._cache.scanned_ranges, this._start_height || 0);
|
|
91
|
+
return current_range == null ? null : current_range.end;
|
|
88
92
|
}
|
|
89
93
|
get current_top_range_height() {
|
|
90
94
|
if (typeof this._stats === "undefined" || this._stats === null)
|
|
@@ -92,7 +96,7 @@ export class ScanCacheOpened {
|
|
|
92
96
|
return this._stats.height;
|
|
93
97
|
}
|
|
94
98
|
async changeStartHeight(start_height) {
|
|
95
|
-
this.stopWorker();
|
|
99
|
+
await this.stopWorker();
|
|
96
100
|
await this._scanSettings.setStartHeight(start_height);
|
|
97
101
|
this._start_height = start_height;
|
|
98
102
|
await this.unpause();
|
|
@@ -167,7 +171,7 @@ export class ScanCacheOpened {
|
|
|
167
171
|
return this._scanSettings.logs_exclude;
|
|
168
172
|
}
|
|
169
173
|
async changeNodeUrlAndStartHeight(node_url, start_height) {
|
|
170
|
-
this.stopWorker();
|
|
174
|
+
await this.stopWorker();
|
|
171
175
|
if (node_url !== undefined) {
|
|
172
176
|
await this._scanSettings.setNodeUrl(node_url);
|
|
173
177
|
this.node_url = node_url;
|
|
@@ -179,7 +183,7 @@ export class ScanCacheOpened {
|
|
|
179
183
|
await this.unpause();
|
|
180
184
|
}
|
|
181
185
|
async changeNodeUrl(node_url) {
|
|
182
|
-
this.stopWorker();
|
|
186
|
+
await this.stopWorker();
|
|
183
187
|
await this._scanSettings.setNodeUrl(node_url);
|
|
184
188
|
this.node_url = node_url;
|
|
185
189
|
await this.unpause();
|
|
@@ -188,7 +192,7 @@ export class ScanCacheOpened {
|
|
|
188
192
|
await this._scanSettings.setMerchantConfirmations(merchant_confirmations);
|
|
189
193
|
}
|
|
190
194
|
async setCpuWorkerCount(cpu_worker_count) {
|
|
191
|
-
this.stopWorker();
|
|
195
|
+
await this.stopWorker();
|
|
192
196
|
await this._scanSettings.setCpuWorkerCount(cpu_worker_count);
|
|
193
197
|
await this.unpause();
|
|
194
198
|
}
|
|
@@ -201,7 +205,7 @@ export class ScanCacheOpened {
|
|
|
201
205
|
await this._scanSettings.setWalletName(this.view_pair.primary_address, name);
|
|
202
206
|
}
|
|
203
207
|
async retry() {
|
|
204
|
-
this.stopWorker();
|
|
208
|
+
await this.stopWorker();
|
|
205
209
|
// scansettings so external changes (e.g. from a sidebar frontend instance) are picked up
|
|
206
210
|
await this._scanSettings.reload();
|
|
207
211
|
const walletStillExists = this._scanSettings.walletExists(this.primary_address);
|
|
@@ -396,7 +400,7 @@ export class ScanCacheOpened {
|
|
|
396
400
|
if (sendResult.status !== "OK")
|
|
397
401
|
throw new Error("send raw transaction rpc returned error");
|
|
398
402
|
// before writing the scan cache, we stop the worker to avoid a race
|
|
399
|
-
this.stopWorker();
|
|
403
|
+
await this.stopWorker();
|
|
400
404
|
// write txlog to cache + update pending_spent_utxos (affects stats + spendability)
|
|
401
405
|
await writeCacheFileDefaultLocationThrows({
|
|
402
406
|
primary_address: this.primary_address,
|
|
@@ -440,7 +444,7 @@ export class ScanCacheOpened {
|
|
|
440
444
|
}
|
|
441
445
|
catch (e) {
|
|
442
446
|
// before writing the scan cache, we stop the worker to avoid a race
|
|
443
|
-
this.stopWorker();
|
|
447
|
+
await this.stopWorker();
|
|
444
448
|
// write txlog error to cache
|
|
445
449
|
await writeCacheFileDefaultLocationThrows({
|
|
446
450
|
primary_address: this.primary_address,
|
|
@@ -550,15 +554,35 @@ export class ScanCacheOpened {
|
|
|
550
554
|
};
|
|
551
555
|
}
|
|
552
556
|
async pause() {
|
|
553
|
-
this.stopWorker();
|
|
557
|
+
await this.stopWorker();
|
|
554
558
|
return await this._scanSettings.haltWallet(this.view_pair.primary_address);
|
|
555
559
|
}
|
|
556
|
-
|
|
560
|
+
// debug: dump coordinator + cpu worker heaps while they are live
|
|
561
|
+
// in browser: use inspector directly, this is only useful for bun
|
|
562
|
+
async dumpWorkerHeaps(dir) {
|
|
563
|
+
if (!this.worker?.dumpHeaps)
|
|
564
|
+
return [];
|
|
565
|
+
return await this.worker.dumpHeaps(dir);
|
|
566
|
+
}
|
|
567
|
+
async stopWorker() {
|
|
557
568
|
if (this.worker) {
|
|
558
|
-
this.worker
|
|
569
|
+
const worker = this.worker;
|
|
570
|
+
// clear ref first so concurrent stop/retry cannot double shutdown
|
|
559
571
|
this.worker.cpuWorkers = [];
|
|
560
572
|
this.worker.fetchWorker = undefined;
|
|
561
573
|
delete this.worker;
|
|
574
|
+
// drain coordinator first, then hard kill workers
|
|
575
|
+
try {
|
|
576
|
+
await worker.shutdown();
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// still terminate below
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
worker.terminate();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
}
|
|
562
586
|
}
|
|
563
587
|
}
|
|
564
588
|
async unpause() {
|
|
@@ -697,10 +721,17 @@ export class ManyScanCachesOpened {
|
|
|
697
721
|
return null;
|
|
698
722
|
return this.wallets[0]?.start_height;
|
|
699
723
|
}
|
|
724
|
+
// overall scan tip = lagging non-halted wallet (this.wallets is already non-halted)
|
|
700
725
|
get current_height() {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
726
|
+
let min = null;
|
|
727
|
+
for (const w of this.wallets) {
|
|
728
|
+
const h = w.current_height;
|
|
729
|
+
if (h == null)
|
|
730
|
+
continue;
|
|
731
|
+
if (min == null || h < min)
|
|
732
|
+
min = h;
|
|
733
|
+
}
|
|
734
|
+
return min;
|
|
704
735
|
}
|
|
705
736
|
get node_url() {
|
|
706
737
|
if (this.wallets.length === 0)
|
|
@@ -709,25 +740,32 @@ export class ManyScanCachesOpened {
|
|
|
709
740
|
}
|
|
710
741
|
async changeNodeUrlAndStartHeight(node_url, start_height) {
|
|
711
742
|
if (this.wallets.length === 0)
|
|
712
|
-
|
|
743
|
+
return;
|
|
713
744
|
const masterWallet = this.wallets[0];
|
|
714
745
|
return await masterWallet.changeNodeUrlAndStartHeight(node_url, start_height);
|
|
715
746
|
}
|
|
716
747
|
async retry() {
|
|
717
748
|
if (this.wallets.length === 0)
|
|
718
|
-
|
|
749
|
+
return;
|
|
719
750
|
const masterWallet = this.wallets[0];
|
|
720
751
|
return await masterWallet.retry();
|
|
721
752
|
}
|
|
722
|
-
stopWorker() {
|
|
753
|
+
async stopWorker() {
|
|
723
754
|
if (this.wallets.length === 0)
|
|
724
|
-
|
|
755
|
+
return;
|
|
725
756
|
const masterWallet = this.wallets[0];
|
|
726
|
-
return masterWallet.stopWorker();
|
|
757
|
+
return await masterWallet.stopWorker();
|
|
758
|
+
}
|
|
759
|
+
// debug: dump live coordinator + cpu worker heaps via master wallet workers
|
|
760
|
+
// in browser: use inspector directly, this is only useful for bun
|
|
761
|
+
async dumpWorkerHeaps(dir) {
|
|
762
|
+
if (this.wallets.length === 0)
|
|
763
|
+
return [];
|
|
764
|
+
return await this.wallets[0].dumpWorkerHeaps(dir);
|
|
727
765
|
}
|
|
728
766
|
async changeNodeUrl(node_url) {
|
|
729
767
|
if (this.wallets.length === 0)
|
|
730
|
-
|
|
768
|
+
return;
|
|
731
769
|
const masterWallet = this.wallets[0];
|
|
732
770
|
return await masterWallet.changeNodeUrl(node_url);
|
|
733
771
|
}
|
|
@@ -742,19 +780,13 @@ export class ManyScanCachesOpened {
|
|
|
742
780
|
return this.wallets[0]?.cpu_worker_count;
|
|
743
781
|
}
|
|
744
782
|
get logs() {
|
|
745
|
-
|
|
746
|
-
return undefined;
|
|
747
|
-
return this.wallets[0]?.logs;
|
|
783
|
+
return this._scanSettings.logs;
|
|
748
784
|
}
|
|
749
785
|
get logs_include() {
|
|
750
|
-
|
|
751
|
-
return undefined;
|
|
752
|
-
return this.wallets[0]?.logs_include;
|
|
786
|
+
return this._scanSettings.logs_include;
|
|
753
787
|
}
|
|
754
788
|
get logs_exclude() {
|
|
755
|
-
|
|
756
|
-
return undefined;
|
|
757
|
-
return this.wallets[0]?.logs_exclude;
|
|
789
|
+
return this._scanSettings.logs_exclude;
|
|
758
790
|
}
|
|
759
791
|
get connectionStatus() {
|
|
760
792
|
return this.connectionStatusOpened.connectionStatus;
|
|
@@ -770,18 +802,16 @@ export class ManyScanCachesOpened {
|
|
|
770
802
|
}
|
|
771
803
|
async setMerchantConfirmations(merchant_confirmations) {
|
|
772
804
|
if (this.wallets.length === 0)
|
|
773
|
-
|
|
805
|
+
return;
|
|
774
806
|
return await this.wallets[0].setMerchantConfirmations(merchant_confirmations);
|
|
775
807
|
}
|
|
776
808
|
async setCpuWorkerCount(cpu_worker_count) {
|
|
777
809
|
if (this.wallets.length === 0)
|
|
778
|
-
|
|
810
|
+
return;
|
|
779
811
|
return await this.wallets[0].setCpuWorkerCount(cpu_worker_count);
|
|
780
812
|
}
|
|
781
813
|
async setLogSettings(logs, logs_include, logs_exclude) {
|
|
782
|
-
|
|
783
|
-
throw new Error("no wallets");
|
|
784
|
-
return await this.wallets[0].setLogSettings(logs, logs_include, logs_exclude);
|
|
814
|
+
return await this._scanSettings.setLogSettings(logs, logs_include, logs_exclude);
|
|
785
815
|
}
|
|
786
816
|
async setWalletName(primary_address, name) {
|
|
787
817
|
await this._scanSettings.setWalletName(primary_address, name);
|
|
@@ -791,7 +821,7 @@ export class ManyScanCachesOpened {
|
|
|
791
821
|
}
|
|
792
822
|
async changeStartHeight(start_height) {
|
|
793
823
|
if (this.wallets.length === 0)
|
|
794
|
-
|
|
824
|
+
return;
|
|
795
825
|
const masterWallet = this.wallets[0];
|
|
796
826
|
return await masterWallet.changeStartHeight(start_height);
|
|
797
827
|
}
|
|
@@ -851,15 +881,34 @@ export class ManyScanCachesOpened {
|
|
|
851
881
|
static async create(options) {
|
|
852
882
|
const { scan_settings_path, pathPrefix, onConnectionStatusChange, connectionStatusIntervalMs, autoRetry, retryDelayMs, } = options;
|
|
853
883
|
const scanSettingsOpened = await ScanSettingsOpened.create(scan_settings_path, pathPrefix);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
884
|
+
// persist log options into ScanSettings.json before workers start
|
|
885
|
+
if (options.logs !== undefined ||
|
|
886
|
+
options.logs_include !== undefined ||
|
|
887
|
+
options.logs_exclude !== undefined) {
|
|
888
|
+
await scanSettingsOpened.setLogSettings(options.logs !== undefined ? options.logs : scanSettingsOpened.logs, options.logs_include !== undefined
|
|
889
|
+
? options.logs_include
|
|
890
|
+
: scanSettingsOpened.logs_include, options.logs_exclude !== undefined
|
|
891
|
+
? options.logs_exclude
|
|
892
|
+
: scanSettingsOpened.logs_exclude);
|
|
893
|
+
}
|
|
857
894
|
// wrap workerError with auto-retry
|
|
858
895
|
let retryTimer;
|
|
859
896
|
let instance = null;
|
|
860
897
|
const retryFn = async () => {
|
|
861
|
-
|
|
862
|
-
|
|
898
|
+
// no wallets means idle, do not rebuild or restart workers
|
|
899
|
+
if (!instance || instance.wallets.length === 0) {
|
|
900
|
+
clearTimeout(retryTimer);
|
|
901
|
+
retryTimer = undefined;
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
await instance.buildWallets();
|
|
905
|
+
// still empty after rebuild, stay idle
|
|
906
|
+
if (instance.wallets.length === 0) {
|
|
907
|
+
clearTimeout(retryTimer);
|
|
908
|
+
retryTimer = undefined;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const node_url = instance.node_url;
|
|
863
912
|
if (!node_url)
|
|
864
913
|
throw new Error("No nodeurl set, can't retry connection");
|
|
865
914
|
const getinfo_result = await get_info(node_url)
|
|
@@ -893,14 +942,13 @@ export class ManyScanCachesOpened {
|
|
|
893
942
|
console.error("unable to connect to node ... retrying ...");
|
|
894
943
|
}
|
|
895
944
|
}
|
|
896
|
-
|
|
945
|
+
// only schedule retry when we still have wallets to scan
|
|
946
|
+
if (!retryTimer && instance && instance.wallets.length > 0) {
|
|
897
947
|
retryTimer = setTimeout(retryFn, retryDelayMs ?? 5000);
|
|
898
948
|
}
|
|
899
949
|
};
|
|
900
950
|
}
|
|
901
|
-
const wallets = await this._buildWallets(scanSettingsOpened, newOptions);
|
|
902
|
-
if (!wallets)
|
|
903
|
-
return undefined;
|
|
951
|
+
const wallets = (await this._buildWallets(scanSettingsOpened, newOptions)) ?? [];
|
|
904
952
|
const csOpened = new ConnectionStatusOpened(scan_settings_path || SCAN_SETTINGS_STORE_NAME_DEFAULT, autoRetry
|
|
905
953
|
? (status) => {
|
|
906
954
|
if (status?.last_packet?.status === "OK" && connectionFailedShown) {
|
|
@@ -916,26 +964,39 @@ export class ManyScanCachesOpened {
|
|
|
916
964
|
return instance;
|
|
917
965
|
}
|
|
918
966
|
async buildWallets() {
|
|
919
|
-
this.stopWorker();
|
|
967
|
+
await this.stopWorker();
|
|
968
|
+
await this.reloadWalletsAfterStop();
|
|
969
|
+
}
|
|
970
|
+
// assumes stopWorker already ran (graceful shutdown + terminate)
|
|
971
|
+
async reloadWalletsAfterStop() {
|
|
920
972
|
await this._scanSettings.reload();
|
|
973
|
+
// empty after reload: stay idle, do not open ScanCacheOpened or start workers
|
|
974
|
+
const remaining = this._scanSettings.wallets.filter((w) => !w?.halted);
|
|
975
|
+
if (remaining.length === 0) {
|
|
976
|
+
this._wallets = [];
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
921
979
|
const newWallets = await ManyScanCachesOpened._buildWallets(this._scanSettings, this._options);
|
|
922
|
-
|
|
923
|
-
throw new Error("no non-halted wallets left after rebuild, use addViewWallet or addSpendWallet first");
|
|
924
|
-
this._wallets = newWallets;
|
|
980
|
+
this._wallets = newWallets ?? [];
|
|
925
981
|
}
|
|
926
982
|
async addViewWallet(primary_address, view_key, fields) {
|
|
983
|
+
await this.stopWorker();
|
|
927
984
|
await this._scanSettings.addViewWallet(primary_address, view_key, fields);
|
|
928
|
-
await this.
|
|
985
|
+
await this.reloadWalletsAfterStop();
|
|
929
986
|
}
|
|
930
987
|
async addSpendWallet(wallet_secret, fields) {
|
|
988
|
+
await this.stopWorker();
|
|
931
989
|
await this._scanSettings.addSpendWallet(wallet_secret, fields);
|
|
932
|
-
await this.
|
|
990
|
+
await this.reloadWalletsAfterStop();
|
|
933
991
|
}
|
|
934
992
|
async removeWallet(primary_address) {
|
|
993
|
+
await this.stopWorker();
|
|
935
994
|
await this._scanSettings.removeWallet(primary_address);
|
|
936
|
-
await this.
|
|
995
|
+
await this.reloadWalletsAfterStop();
|
|
937
996
|
}
|
|
938
997
|
async feed(params) {
|
|
998
|
+
if (this.wallets.length === 0)
|
|
999
|
+
return;
|
|
939
1000
|
await this.wallets[0].feed(params);
|
|
940
1001
|
}
|
|
941
1002
|
_wallets = [];
|
|
@@ -64,7 +64,6 @@ export type CoordinatorEvent = {
|
|
|
64
64
|
} | {
|
|
65
65
|
type: "scan_ready";
|
|
66
66
|
address: string;
|
|
67
|
-
result: ScanLoopYield;
|
|
68
67
|
newCache: ScanCache;
|
|
69
68
|
changed_outputs: {
|
|
70
69
|
output: any;
|
|
@@ -72,11 +71,13 @@ export type CoordinatorEvent = {
|
|
|
72
71
|
}[];
|
|
73
72
|
} | {
|
|
74
73
|
type: "all_idle";
|
|
74
|
+
} | {
|
|
75
|
+
type: "shutdown_done";
|
|
75
76
|
} | {
|
|
76
77
|
type: "error";
|
|
77
78
|
error: Error;
|
|
78
79
|
};
|
|
79
|
-
export declare function setupCoordinator(scanSettingsPath?: string, pathPrefix?: string): Promise<false | {
|
|
80
|
+
export declare function setupCoordinator(scanSettingsPath?: string, pathPrefix?: string, stopSync?: AbortSignal): Promise<false | {
|
|
80
81
|
blocksGenerator: AsyncGenerator<BlocksBufferLoopResult, any, any>;
|
|
81
82
|
workBuffer: WorkItem[];
|
|
82
83
|
blocksBuffer: GetBlocksBinBufferItem[];
|
|
@@ -96,10 +97,12 @@ export type BlocksBufferRacer = {
|
|
|
96
97
|
};
|
|
97
98
|
export type ScanLoopRacer = ScanLoopYield;
|
|
98
99
|
export type Racers = BlocksBufferRacer | ScanLoopRacer;
|
|
100
|
+
export declare function resetInProgressWorkItems(workBuffer: WorkItem[]): void;
|
|
101
|
+
export declare function cancelBusyCpuPorts(ports: PortStatus[], timeoutMs?: number): Promise<void>;
|
|
99
102
|
/**
|
|
100
103
|
* coordinator main (multithreaded): dispatches scan work to CPU workers
|
|
101
104
|
* via MessagePorts, processes results in order per wallet.
|
|
102
105
|
* falls back to single-threaded coordinator if no cpuPorts provided.
|
|
103
106
|
*/
|
|
104
|
-
export declare function coordinatorMainMultithreaded(scanSettingsPath?: string, pathPrefix?: string, cpuPorts?: MessagePort[]): AsyncGenerator<CoordinatorEvent>;
|
|
107
|
+
export declare function coordinatorMainMultithreaded(scanSettingsPath?: string, pathPrefix?: string, cpuPorts?: MessagePort[], stopSync?: AbortSignal): AsyncGenerator<CoordinatorEvent>;
|
|
105
108
|
export declare function scheduleWorkOnCpuPorts(ports: PortStatus[], work_buffer: WorkItem[]): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, } from "../../api";
|
|
2
|
-
import {
|
|
1
|
+
import { setupBlocksBufferGenerator, ViewPair, handleConnectionStatusChanges, processScanResult, writeCacheToFile, sleep, findTipIndex, readWriteConnectionStatusFile, applyWalletScanProgress, } from "../../api";
|
|
2
|
+
import {} from "./scanLoop";
|
|
3
3
|
import { makeWorkItem } from "./scanLoop";
|
|
4
4
|
import { cullTooLargeScanHeight, getNonHaltedWallets, getPathPrefix, openScanSettingsFile, SCAN_SETTINGS_STORE_NAME_DEFAULT, walletSettingsPlusKeys, } from "../../api";
|
|
5
5
|
import { findRange, initScanCacheFile, lastRange, makeCacheRangeForHeight, mergeRanges, } from "./scanCache";
|
|
@@ -220,6 +220,9 @@ export async function processWorkItem(item, workBuffer, blocksBuffer, pathPrefix
|
|
|
220
220
|
throw error;
|
|
221
221
|
}
|
|
222
222
|
await writeCacheToFile(cache, pathPrefix);
|
|
223
|
+
// raw cpu ScanResult is fully merged into cache now. drop it so workBuffer
|
|
224
|
+
// does not pin all_key_images / hash strings until left-edge free.
|
|
225
|
+
item.result = undefined;
|
|
223
226
|
item.status = "process_result_done";
|
|
224
227
|
log("processWorkItem", `process_result_done work_uuid=${item?.work_uuid}`);
|
|
225
228
|
// remove from blocksbuffer if no more work items reference it
|
|
@@ -271,7 +274,7 @@ function computeETA(cache, totalBlocksScanned, scanStartTime) {
|
|
|
271
274
|
const etaMs = remaining / blocksPerMs;
|
|
272
275
|
return msToHHMM(etaMs);
|
|
273
276
|
}
|
|
274
|
-
export async function setupCoordinator(scanSettingsPath, pathPrefix) {
|
|
277
|
+
export async function setupCoordinator(scanSettingsPath, pathPrefix, stopSync) {
|
|
275
278
|
const work_to_be_done = await findWorkToBeDone(scanSettingsPath, pathPrefix);
|
|
276
279
|
if (!work_to_be_done)
|
|
277
280
|
return false;
|
|
@@ -280,6 +283,7 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix) {
|
|
|
280
283
|
startHeight: work_to_be_done.start_height,
|
|
281
284
|
anchor_range: work_to_be_done.anchor_range,
|
|
282
285
|
scanSettingsPath,
|
|
286
|
+
stopSync,
|
|
283
287
|
});
|
|
284
288
|
const workBuffer = [];
|
|
285
289
|
//TODO: refactor to per wallet buffer
|
|
@@ -298,16 +302,46 @@ export async function setupCoordinator(scanSettingsPath, pathPrefix) {
|
|
|
298
302
|
work_to_be_done,
|
|
299
303
|
};
|
|
300
304
|
}
|
|
305
|
+
// flip inflight work back to fresh so a restart can reschedule it
|
|
306
|
+
export function resetInProgressWorkItems(workBuffer) {
|
|
307
|
+
for (const item of workBuffer) {
|
|
308
|
+
if (item.status === "scanwork_in_progress") {
|
|
309
|
+
item.status = "fresh";
|
|
310
|
+
item.result = undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// tell busy cpu ports to cancel and wait for their promises (with timeout)
|
|
315
|
+
export async function cancelBusyCpuPorts(ports, timeoutMs = 3000) {
|
|
316
|
+
const busy = ports.filter((ps) => ps.promise);
|
|
317
|
+
for (const ps of busy) {
|
|
318
|
+
try {
|
|
319
|
+
sendToCpuWorker(ps.port, "cancel");
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
log("cancelBusyCpuPorts", ["send cancel failed", String(err)]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (busy.length === 0)
|
|
326
|
+
return;
|
|
327
|
+
await Promise.race([
|
|
328
|
+
Promise.allSettled(busy.map((ps) => ps.promise)),
|
|
329
|
+
sleep(timeoutMs),
|
|
330
|
+
]);
|
|
331
|
+
for (const ps of ports) {
|
|
332
|
+
ps.promise = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
301
335
|
/**
|
|
302
336
|
* coordinator main (multithreaded): dispatches scan work to CPU workers
|
|
303
337
|
* via MessagePorts, processes results in order per wallet.
|
|
304
338
|
* falls back to single-threaded coordinator if no cpuPorts provided.
|
|
305
339
|
*/
|
|
306
|
-
export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts) {
|
|
340
|
+
export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix, cpuPorts, stopSync) {
|
|
307
341
|
if (!cpuPorts || cpuPorts.length === 0) {
|
|
308
342
|
throw new Error("[coordinatorMain Multithreaded] cpuPorts empty, there must be at least cpu worker");
|
|
309
343
|
}
|
|
310
|
-
const ctx = await setupCoordinator(scanSettingsPath, pathPrefix);
|
|
344
|
+
const ctx = await setupCoordinator(scanSettingsPath, pathPrefix, stopSync);
|
|
311
345
|
if (!ctx)
|
|
312
346
|
throw new Error("[coordinatorMain Multithreaded] findWorkToBeDone returned false");
|
|
313
347
|
const work_to_be_done = ctx.work_to_be_done;
|
|
@@ -327,6 +361,13 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
327
361
|
}
|
|
328
362
|
let race_count = 0;
|
|
329
363
|
while (true) {
|
|
364
|
+
if (stopSync?.aborted) {
|
|
365
|
+
log("coordinatorMainMultithreaded", ["shutdown signal, draining"]);
|
|
366
|
+
await cancelBusyCpuPorts(freePorts);
|
|
367
|
+
resetInProgressWorkItems(workBuffer);
|
|
368
|
+
yield { type: "shutdown_done" };
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
330
371
|
race_count++;
|
|
331
372
|
log("coordinatorMainMultithreaded", ["race_count", race_count]);
|
|
332
373
|
await scheduleWorkOnCpuPorts(freePorts, workBuffer);
|
|
@@ -344,6 +385,16 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
344
385
|
const winner = await Promise.race(races);
|
|
345
386
|
log("coordinatorMainMultithreaded", "cpu ports status");
|
|
346
387
|
if ("src" in winner && winner.src === "blocks") {
|
|
388
|
+
// fetch loop returned (done) on abort, treat as shutdown
|
|
389
|
+
if (winner.value.done) {
|
|
390
|
+
log("coordinatorMainMultithreaded", [
|
|
391
|
+
"blocks generator done, draining",
|
|
392
|
+
]);
|
|
393
|
+
await cancelBusyCpuPorts(freePorts);
|
|
394
|
+
resetInProgressWorkItems(workBuffer);
|
|
395
|
+
yield { type: "shutdown_done" };
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
347
398
|
const result = winner.value.value;
|
|
348
399
|
const { isBlocksBufferChanged } = await handleBlocksYield(result, scanSettingsPath);
|
|
349
400
|
if (isBlocksBufferChanged) {
|
|
@@ -376,30 +427,22 @@ export async function* coordinatorMainMultithreaded(scanSettingsPath, pathPrefix
|
|
|
376
427
|
continue;
|
|
377
428
|
const blockCount = to_be_processed.to - to_be_processed.from + 1;
|
|
378
429
|
totalBlocksScanned += blockCount;
|
|
379
|
-
const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
|
|
380
|
-
// write connection status sync (includes eta)
|
|
381
|
-
if (eta) {
|
|
382
|
-
await readWriteConnectionStatusFile((cs) => {
|
|
383
|
-
cs.sync = {
|
|
384
|
-
...cs.sync,
|
|
385
|
-
scanned_ranges: wallet.cache.scanned_ranges,
|
|
386
|
-
daemon_height: wallet.cache.daemon_height,
|
|
387
|
-
current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
|
|
388
|
-
eta,
|
|
389
|
-
timestamp: new Date().toISOString(),
|
|
390
|
-
};
|
|
391
|
-
}, scanSettingsPath);
|
|
392
|
-
}
|
|
393
430
|
const res = await processWorkItem(to_be_processed, workBuffer, blocksBuffer, getPathPrefix(scanSettingsPath, pathPrefix), wallet.secret_spend_key);
|
|
431
|
+
// always persist wallet progress after process; eta only if we have a new one
|
|
432
|
+
// so missing eta does not wipe the previous value (no flicker)
|
|
433
|
+
const eta = computeETA(wallet.cache, totalBlocksScanned, scanStartTime);
|
|
434
|
+
await readWriteConnectionStatusFile((cs) => {
|
|
435
|
+
applyWalletScanProgress(cs, {
|
|
436
|
+
current_scan_height: lastRange(wallet.cache.scanned_ranges)?.end || 0,
|
|
437
|
+
scanned_ranges: wallet.cache.scanned_ranges,
|
|
438
|
+
daemon_height: wallet.cache.daemon_height,
|
|
439
|
+
eta,
|
|
440
|
+
});
|
|
441
|
+
}, scanSettingsPath);
|
|
394
442
|
// log("coordinatorMainMultithreaded", "processWorkItem result");
|
|
395
443
|
yield {
|
|
396
444
|
type: "scan_ready",
|
|
397
445
|
address: wallet.primary_address,
|
|
398
|
-
result: {
|
|
399
|
-
type: "Ready",
|
|
400
|
-
work_uuid: to_be_processed.work_uuid,
|
|
401
|
-
result: to_be_processed.result,
|
|
402
|
-
},
|
|
403
446
|
newCache: wallet.cache,
|
|
404
447
|
changed_outputs: res.changed_outputs,
|
|
405
448
|
};
|
|
@@ -424,6 +467,17 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
424
467
|
const msg = event.data;
|
|
425
468
|
// handle the result here:
|
|
426
469
|
log("scheduleWorkOnCpuPorts", ["onmessage result", msg]);
|
|
470
|
+
// cancel ack may omit work_uuid when idle, but inflight cancel carries it
|
|
471
|
+
if (msg.type === "Canceled") {
|
|
472
|
+
if (item.status === "scanwork_in_progress") {
|
|
473
|
+
item.status = "fresh";
|
|
474
|
+
item.result = undefined;
|
|
475
|
+
}
|
|
476
|
+
port_status.promise = null;
|
|
477
|
+
// thin resolve: dont put full msg/result on the port promise on cancel
|
|
478
|
+
resolve_port({ type: "Canceled", work_uuid: item.work_uuid });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
427
481
|
if (msg.work_uuid !== item.work_uuid) {
|
|
428
482
|
log("scheduleWorkOnCpuPorts", [
|
|
429
483
|
"wrong work_uuid in msg msg.work_uuid=",
|
|
@@ -441,10 +495,18 @@ export async function scheduleWorkOnCpuPorts(ports, work_buffer) {
|
|
|
441
495
|
log("scheduleWorkOnCpuPorts", "wrong status in msg");
|
|
442
496
|
throw new Error("[scheduleWorkOnCpuPorts] wrong status in msg");
|
|
443
497
|
}
|
|
498
|
+
// fat ScanResult lives only on the work item until process clears it
|
|
444
499
|
item.result = msg.result;
|
|
445
500
|
item.status = "scanwork_done";
|
|
501
|
+
// cpu already has its own buffer copy; process only needs meta.
|
|
502
|
+
// drop work-item bytes now; blocksBuffer still owns the batch until left-edge free.
|
|
503
|
+
item.batch = {
|
|
504
|
+
...item.batch,
|
|
505
|
+
data: new Uint8Array(0),
|
|
506
|
+
};
|
|
446
507
|
port_status.promise = null;
|
|
447
|
-
|
|
508
|
+
// thin resolve: race/await must not retain all_key_images via promise value
|
|
509
|
+
resolve_port({ type: msg.type, work_uuid: msg.work_uuid });
|
|
448
510
|
};
|
|
449
511
|
port_status.port.onmessage = onmessage;
|
|
450
512
|
item.status = "scanwork_in_progress";
|
|
@@ -12,9 +12,8 @@ export declare function makeWorkItem(walletConfig: WalletConfigPlusCache, batch:
|
|
|
12
12
|
export type ScanLoopIteratorResult = IteratorYieldResult<ScanLoopYield>;
|
|
13
13
|
export type ScanLoopInput = WorkItem | "cancel" | undefined;
|
|
14
14
|
export type ScanLoopYield = {
|
|
15
|
-
type: "Ready" | "InProgress";
|
|
15
|
+
type: "Ready" | "InProgress" | "Canceled";
|
|
16
16
|
work_uuid?: string;
|
|
17
17
|
result?: ScanResult;
|
|
18
18
|
};
|
|
19
19
|
export declare function scanLoop(wallet: WalletConfig): AsyncGenerator<ScanLoopYield, void, ScanLoopInput>;
|
|
20
|
-
export declare function markWorkItemAsDone(loop_event: ScanLoopYield, work_buffer: WorkItem[]): Promise<WorkItem | undefined>;
|
|
@@ -101,16 +101,3 @@ export async function* scanLoop(wallet) {
|
|
|
101
101
|
// will be done by processScanResult in the consumer
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
//theoretically markWorkItemAsDone could be done in the generator as WorkItem is passed in by reference
|
|
105
|
-
// but: eventually this has to be done accross CPU worker boundaries
|
|
106
|
-
// so we act on the yielded scan result
|
|
107
|
-
export async function markWorkItemAsDone(loop_event, work_buffer) {
|
|
108
|
-
if (loop_event.type === "Ready" && loop_event.work_uuid) {
|
|
109
|
-
const work_item = work_buffer.find((w) => w.work_uuid === loop_event.work_uuid);
|
|
110
|
-
if (work_item) {
|
|
111
|
-
work_item.result = loop_event.result;
|
|
112
|
-
work_item.status = "scanwork_done";
|
|
113
|
-
return work_item;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|