@leofcoin/chain 1.7.152 → 1.7.154
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/exports/browser/chain.js +100 -5
- package/exports/chain.js +100 -5
- package/exports/state.d.ts +2 -0
- package/package.json +1 -1
package/exports/browser/chain.js
CHANGED
|
@@ -4189,7 +4189,7 @@ class Machine {
|
|
|
4189
4189
|
break;
|
|
4190
4190
|
}
|
|
4191
4191
|
case 'debug': {
|
|
4192
|
-
debug
|
|
4192
|
+
// debug(data.message)
|
|
4193
4193
|
if (data.message.includes('loaded transactions for block:')) {
|
|
4194
4194
|
pubsub.publish('block-loaded', data.message.replace('loaded transactions for block: ', '').split(' @')[0]);
|
|
4195
4195
|
}
|
|
@@ -4598,6 +4598,36 @@ class Machine {
|
|
|
4598
4598
|
}
|
|
4599
4599
|
}
|
|
4600
4600
|
|
|
4601
|
+
class Jobber {
|
|
4602
|
+
constructor(timeout) {
|
|
4603
|
+
this.busy = false;
|
|
4604
|
+
this.timeout = timeout;
|
|
4605
|
+
}
|
|
4606
|
+
add(fn) {
|
|
4607
|
+
this.busy = true;
|
|
4608
|
+
return new Promise(async (resolve, reject) => {
|
|
4609
|
+
const timeout = setTimeout(() => {
|
|
4610
|
+
reject('timeout');
|
|
4611
|
+
}, this.timeout);
|
|
4612
|
+
this.destroy = () => {
|
|
4613
|
+
clearTimeout(timeout);
|
|
4614
|
+
this.busy = false;
|
|
4615
|
+
resolve('stopped');
|
|
4616
|
+
};
|
|
4617
|
+
try {
|
|
4618
|
+
const result = await fn();
|
|
4619
|
+
clearTimeout(timeout);
|
|
4620
|
+
this.busy = false;
|
|
4621
|
+
resolve(result);
|
|
4622
|
+
}
|
|
4623
|
+
catch (error) {
|
|
4624
|
+
clearTimeout(timeout);
|
|
4625
|
+
reject(error);
|
|
4626
|
+
}
|
|
4627
|
+
});
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4601
4631
|
const debug$1 = createDebugger('leofcoin/state');
|
|
4602
4632
|
class State extends Contract {
|
|
4603
4633
|
#blockHashMap;
|
|
@@ -4610,6 +4640,7 @@ class State extends Contract {
|
|
|
4610
4640
|
// Block resolution state
|
|
4611
4641
|
#resolvingBlocks;
|
|
4612
4642
|
#maxConcurrentResolves;
|
|
4643
|
+
#txConcurrency;
|
|
4613
4644
|
#totalSize;
|
|
4614
4645
|
#lastResolved;
|
|
4615
4646
|
#lastResolvedTime;
|
|
@@ -4696,6 +4727,7 @@ class State extends Contract {
|
|
|
4696
4727
|
// Block resolution state
|
|
4697
4728
|
this.#resolvingBlocks = new Set();
|
|
4698
4729
|
this.#maxConcurrentResolves = 10;
|
|
4730
|
+
this.#txConcurrency = 25;
|
|
4699
4731
|
this.knownBlocks = [];
|
|
4700
4732
|
this.#totalSize = 0;
|
|
4701
4733
|
this._wantList = [];
|
|
@@ -4743,6 +4775,8 @@ class State extends Contract {
|
|
|
4743
4775
|
#lastBlockHandler;
|
|
4744
4776
|
#knownBlocksHandler;
|
|
4745
4777
|
async init() {
|
|
4778
|
+
// Initialize jobber for timed, cancelable tasks
|
|
4779
|
+
this.jobber = new Jobber(this.resolveTimeout);
|
|
4746
4780
|
// Register request handlers
|
|
4747
4781
|
await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler.bind(this));
|
|
4748
4782
|
await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler.bind(this));
|
|
@@ -4862,6 +4896,52 @@ class State extends Contract {
|
|
|
4862
4896
|
this.#resolvingBlocks.delete(hash);
|
|
4863
4897
|
}
|
|
4864
4898
|
}
|
|
4899
|
+
// Small utility to keep event loop responsive
|
|
4900
|
+
async #sleep(ms) {
|
|
4901
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4902
|
+
}
|
|
4903
|
+
// Run a list of tasks with bounded concurrency
|
|
4904
|
+
async #runWithConcurrency(items, concurrency, handler) {
|
|
4905
|
+
let index = 0;
|
|
4906
|
+
const total = items.length;
|
|
4907
|
+
const worker = async () => {
|
|
4908
|
+
while (true) {
|
|
4909
|
+
const current = index++;
|
|
4910
|
+
if (current >= total)
|
|
4911
|
+
break;
|
|
4912
|
+
try {
|
|
4913
|
+
await handler(items[current], current);
|
|
4914
|
+
if (current % 50 === 0)
|
|
4915
|
+
debug$1(`executed ${current}/${total}`);
|
|
4916
|
+
}
|
|
4917
|
+
catch (e) {
|
|
4918
|
+
debug$1(`transaction handler error at ${current}: ${e}`);
|
|
4919
|
+
}
|
|
4920
|
+
// yield a tick every few ops to avoid blocking
|
|
4921
|
+
if (current % 25 === 0)
|
|
4922
|
+
await this.#sleep(1);
|
|
4923
|
+
}
|
|
4924
|
+
};
|
|
4925
|
+
const workers = Array.from({ length: Math.min(concurrency, Math.max(1, total)) }, () => worker());
|
|
4926
|
+
await Promise.all(workers);
|
|
4927
|
+
}
|
|
4928
|
+
// Execute a single transaction with a timeout to avoid indefinite hangs
|
|
4929
|
+
async #executeWithTimeout(transaction, timeoutMs) {
|
|
4930
|
+
const timeout = timeoutMs ?? this.resolveTimeout ?? 5000;
|
|
4931
|
+
let timer;
|
|
4932
|
+
try {
|
|
4933
|
+
return await Promise.race([
|
|
4934
|
+
this.#machine.execute(transaction.decoded.to, transaction.decoded.method, transaction.decoded.params),
|
|
4935
|
+
new Promise((_, reject) => {
|
|
4936
|
+
timer = setTimeout(() => reject(new Error('transaction execution timeout')), timeout);
|
|
4937
|
+
})
|
|
4938
|
+
]);
|
|
4939
|
+
}
|
|
4940
|
+
finally {
|
|
4941
|
+
if (timer)
|
|
4942
|
+
clearTimeout(timer);
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4865
4945
|
async #buildBlockChain(latestHash, maxBlocks = 1000) {
|
|
4866
4946
|
const chain = [];
|
|
4867
4947
|
let currentHash = latestHash;
|
|
@@ -4924,7 +5004,11 @@ class State extends Contract {
|
|
|
4924
5004
|
const blockChain = await this.#buildBlockChain(hash);
|
|
4925
5005
|
debug$1(`Built chain of ${blockChain.length} blocks`);
|
|
4926
5006
|
if (blockChain.length > 0) {
|
|
4927
|
-
|
|
5007
|
+
// If a previous resolve job is still running, cancel it
|
|
5008
|
+
if (this.jobber?.busy && this.jobber.destroy)
|
|
5009
|
+
await this.jobber.destroy();
|
|
5010
|
+
// Run the parallel resolution inside a timed jobber task
|
|
5011
|
+
await this.jobber.add(() => this.#resolveBlocksInParallel(blockChain));
|
|
4928
5012
|
}
|
|
4929
5013
|
}
|
|
4930
5014
|
catch (error) {
|
|
@@ -4942,7 +5026,17 @@ class State extends Contract {
|
|
|
4942
5026
|
const localBlock = await globalThis.chainStore.get('lastBlock');
|
|
4943
5027
|
const hash = new TextDecoder().decode(localBlock);
|
|
4944
5028
|
if (hash && hash !== '0x0') {
|
|
4945
|
-
|
|
5029
|
+
// Cancel any in-flight job before starting a new one
|
|
5030
|
+
if (this.jobber?.busy && this.jobber.destroy)
|
|
5031
|
+
await this.jobber.destroy();
|
|
5032
|
+
// Build chain and resolve in parallel under jobber control
|
|
5033
|
+
const run = async () => {
|
|
5034
|
+
const chain = await this.#buildBlockChain(hash);
|
|
5035
|
+
if (chain.length > 0) {
|
|
5036
|
+
await this.#resolveBlocksInParallel(chain);
|
|
5037
|
+
}
|
|
5038
|
+
};
|
|
5039
|
+
await this.jobber.add(run);
|
|
4946
5040
|
}
|
|
4947
5041
|
}
|
|
4948
5042
|
catch (error) {
|
|
@@ -5043,7 +5137,7 @@ class State extends Contract {
|
|
|
5043
5137
|
// todo throw error
|
|
5044
5138
|
async #_executeTransaction(transaction) {
|
|
5045
5139
|
try {
|
|
5046
|
-
await this.#
|
|
5140
|
+
await this.#executeWithTimeout(transaction);
|
|
5047
5141
|
// await globalThis.accountsStore.put(transaction.decoded.from, String(transaction.decoded.nonce))
|
|
5048
5142
|
// if (transaction.decoded.to === nativeToken) {
|
|
5049
5143
|
// this.#nativeCalls += 1
|
|
@@ -5109,7 +5203,8 @@ class State extends Contract {
|
|
|
5109
5203
|
}
|
|
5110
5204
|
transactions = transactions.filter((transaction) => !transaction.decoded.priority);
|
|
5111
5205
|
debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
|
|
5112
|
-
|
|
5206
|
+
// Concurrency-limited execution to avoid blocking on large blocks
|
|
5207
|
+
await this.#runWithConcurrency(transactions, this.#txConcurrency, async (tx) => this.#_executeTransaction(tx));
|
|
5113
5208
|
this.#blocks[block.index].loaded = true;
|
|
5114
5209
|
debug$1(`executed transactions for block ${block.index}`);
|
|
5115
5210
|
if (Number(block.index) === 0)
|
package/exports/chain.js
CHANGED
|
@@ -330,7 +330,7 @@ class Machine {
|
|
|
330
330
|
break;
|
|
331
331
|
}
|
|
332
332
|
case 'debug': {
|
|
333
|
-
debug
|
|
333
|
+
// debug(data.message)
|
|
334
334
|
if (data.message.includes('loaded transactions for block:')) {
|
|
335
335
|
pubsub.publish('block-loaded', data.message.replace('loaded transactions for block: ', '').split(' @')[0]);
|
|
336
336
|
}
|
|
@@ -739,6 +739,36 @@ class Machine {
|
|
|
739
739
|
}
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
+
class Jobber {
|
|
743
|
+
constructor(timeout) {
|
|
744
|
+
this.busy = false;
|
|
745
|
+
this.timeout = timeout;
|
|
746
|
+
}
|
|
747
|
+
add(fn) {
|
|
748
|
+
this.busy = true;
|
|
749
|
+
return new Promise(async (resolve, reject) => {
|
|
750
|
+
const timeout = setTimeout(() => {
|
|
751
|
+
reject('timeout');
|
|
752
|
+
}, this.timeout);
|
|
753
|
+
this.destroy = () => {
|
|
754
|
+
clearTimeout(timeout);
|
|
755
|
+
this.busy = false;
|
|
756
|
+
resolve('stopped');
|
|
757
|
+
};
|
|
758
|
+
try {
|
|
759
|
+
const result = await fn();
|
|
760
|
+
clearTimeout(timeout);
|
|
761
|
+
this.busy = false;
|
|
762
|
+
resolve(result);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
clearTimeout(timeout);
|
|
766
|
+
reject(error);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
742
772
|
const debug$1 = createDebugger('leofcoin/state');
|
|
743
773
|
class State extends Contract {
|
|
744
774
|
#blockHashMap;
|
|
@@ -751,6 +781,7 @@ class State extends Contract {
|
|
|
751
781
|
// Block resolution state
|
|
752
782
|
#resolvingBlocks;
|
|
753
783
|
#maxConcurrentResolves;
|
|
784
|
+
#txConcurrency;
|
|
754
785
|
#totalSize;
|
|
755
786
|
#lastResolved;
|
|
756
787
|
#lastResolvedTime;
|
|
@@ -837,6 +868,7 @@ class State extends Contract {
|
|
|
837
868
|
// Block resolution state
|
|
838
869
|
this.#resolvingBlocks = new Set();
|
|
839
870
|
this.#maxConcurrentResolves = 10;
|
|
871
|
+
this.#txConcurrency = 25;
|
|
840
872
|
this.knownBlocks = [];
|
|
841
873
|
this.#totalSize = 0;
|
|
842
874
|
this._wantList = [];
|
|
@@ -884,6 +916,8 @@ class State extends Contract {
|
|
|
884
916
|
#lastBlockHandler;
|
|
885
917
|
#knownBlocksHandler;
|
|
886
918
|
async init() {
|
|
919
|
+
// Initialize jobber for timed, cancelable tasks
|
|
920
|
+
this.jobber = new Jobber(this.resolveTimeout);
|
|
887
921
|
// Register request handlers
|
|
888
922
|
await globalThis.peernet.addRequestHandler('lastBlock', this.#lastBlockHandler.bind(this));
|
|
889
923
|
await globalThis.peernet.addRequestHandler('knownBlocks', this.#knownBlocksHandler.bind(this));
|
|
@@ -1003,6 +1037,52 @@ class State extends Contract {
|
|
|
1003
1037
|
this.#resolvingBlocks.delete(hash);
|
|
1004
1038
|
}
|
|
1005
1039
|
}
|
|
1040
|
+
// Small utility to keep event loop responsive
|
|
1041
|
+
async #sleep(ms) {
|
|
1042
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1043
|
+
}
|
|
1044
|
+
// Run a list of tasks with bounded concurrency
|
|
1045
|
+
async #runWithConcurrency(items, concurrency, handler) {
|
|
1046
|
+
let index = 0;
|
|
1047
|
+
const total = items.length;
|
|
1048
|
+
const worker = async () => {
|
|
1049
|
+
while (true) {
|
|
1050
|
+
const current = index++;
|
|
1051
|
+
if (current >= total)
|
|
1052
|
+
break;
|
|
1053
|
+
try {
|
|
1054
|
+
await handler(items[current], current);
|
|
1055
|
+
if (current % 50 === 0)
|
|
1056
|
+
debug$1(`executed ${current}/${total}`);
|
|
1057
|
+
}
|
|
1058
|
+
catch (e) {
|
|
1059
|
+
debug$1(`transaction handler error at ${current}: ${e}`);
|
|
1060
|
+
}
|
|
1061
|
+
// yield a tick every few ops to avoid blocking
|
|
1062
|
+
if (current % 25 === 0)
|
|
1063
|
+
await this.#sleep(1);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
const workers = Array.from({ length: Math.min(concurrency, Math.max(1, total)) }, () => worker());
|
|
1067
|
+
await Promise.all(workers);
|
|
1068
|
+
}
|
|
1069
|
+
// Execute a single transaction with a timeout to avoid indefinite hangs
|
|
1070
|
+
async #executeWithTimeout(transaction, timeoutMs) {
|
|
1071
|
+
const timeout = timeoutMs ?? this.resolveTimeout ?? 5000;
|
|
1072
|
+
let timer;
|
|
1073
|
+
try {
|
|
1074
|
+
return await Promise.race([
|
|
1075
|
+
this.#machine.execute(transaction.decoded.to, transaction.decoded.method, transaction.decoded.params),
|
|
1076
|
+
new Promise((_, reject) => {
|
|
1077
|
+
timer = setTimeout(() => reject(new Error('transaction execution timeout')), timeout);
|
|
1078
|
+
})
|
|
1079
|
+
]);
|
|
1080
|
+
}
|
|
1081
|
+
finally {
|
|
1082
|
+
if (timer)
|
|
1083
|
+
clearTimeout(timer);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1006
1086
|
async #buildBlockChain(latestHash, maxBlocks = 1000) {
|
|
1007
1087
|
const chain = [];
|
|
1008
1088
|
let currentHash = latestHash;
|
|
@@ -1065,7 +1145,11 @@ class State extends Contract {
|
|
|
1065
1145
|
const blockChain = await this.#buildBlockChain(hash);
|
|
1066
1146
|
debug$1(`Built chain of ${blockChain.length} blocks`);
|
|
1067
1147
|
if (blockChain.length > 0) {
|
|
1068
|
-
|
|
1148
|
+
// If a previous resolve job is still running, cancel it
|
|
1149
|
+
if (this.jobber?.busy && this.jobber.destroy)
|
|
1150
|
+
await this.jobber.destroy();
|
|
1151
|
+
// Run the parallel resolution inside a timed jobber task
|
|
1152
|
+
await this.jobber.add(() => this.#resolveBlocksInParallel(blockChain));
|
|
1069
1153
|
}
|
|
1070
1154
|
}
|
|
1071
1155
|
catch (error) {
|
|
@@ -1083,7 +1167,17 @@ class State extends Contract {
|
|
|
1083
1167
|
const localBlock = await globalThis.chainStore.get('lastBlock');
|
|
1084
1168
|
const hash = new TextDecoder().decode(localBlock);
|
|
1085
1169
|
if (hash && hash !== '0x0') {
|
|
1086
|
-
|
|
1170
|
+
// Cancel any in-flight job before starting a new one
|
|
1171
|
+
if (this.jobber?.busy && this.jobber.destroy)
|
|
1172
|
+
await this.jobber.destroy();
|
|
1173
|
+
// Build chain and resolve in parallel under jobber control
|
|
1174
|
+
const run = async () => {
|
|
1175
|
+
const chain = await this.#buildBlockChain(hash);
|
|
1176
|
+
if (chain.length > 0) {
|
|
1177
|
+
await this.#resolveBlocksInParallel(chain);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
await this.jobber.add(run);
|
|
1087
1181
|
}
|
|
1088
1182
|
}
|
|
1089
1183
|
catch (error) {
|
|
@@ -1184,7 +1278,7 @@ class State extends Contract {
|
|
|
1184
1278
|
// todo throw error
|
|
1185
1279
|
async #_executeTransaction(transaction) {
|
|
1186
1280
|
try {
|
|
1187
|
-
await this.#
|
|
1281
|
+
await this.#executeWithTimeout(transaction);
|
|
1188
1282
|
// await globalThis.accountsStore.put(transaction.decoded.from, String(transaction.decoded.nonce))
|
|
1189
1283
|
// if (transaction.decoded.to === nativeToken) {
|
|
1190
1284
|
// this.#nativeCalls += 1
|
|
@@ -1250,7 +1344,8 @@ class State extends Contract {
|
|
|
1250
1344
|
}
|
|
1251
1345
|
transactions = transactions.filter((transaction) => !transaction.decoded.priority);
|
|
1252
1346
|
debug$1(`executing ${transactions.length} transactions for block ${block.index}`);
|
|
1253
|
-
|
|
1347
|
+
// Concurrency-limited execution to avoid blocking on large blocks
|
|
1348
|
+
await this.#runWithConcurrency(transactions, this.#txConcurrency, async (tx) => this.#_executeTransaction(tx));
|
|
1254
1349
|
this.#blocks[block.index].loaded = true;
|
|
1255
1350
|
debug$1(`executed transactions for block ${block.index}`);
|
|
1256
1351
|
if (Number(block.index) === 0)
|
package/exports/state.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { BlockMessage } from '@leofcoin/messages';
|
|
2
2
|
import Contract from './contract.js';
|
|
3
3
|
import Machine from './machine.js';
|
|
4
|
+
import Jobber from './jobs/jobber.js';
|
|
4
5
|
import { BlockHash } from './types.js';
|
|
5
6
|
export default class State extends Contract {
|
|
6
7
|
#private;
|
|
8
|
+
jobber: Jobber;
|
|
7
9
|
knownBlocks: BlockHash[];
|
|
8
10
|
_wantList: any[];
|
|
9
11
|
/**
|