55import { now , sleep } from './time.js' ;
66/* ------------------------------------------------------------------------ */
77
8+ // Multi-rule throttler with backward compatibility for single-rule configs
9+ // Rule fields: id, refillRate, delay (global), capacity, maxCapacity (global), tokens, cost
10+
811class Throttler {
912 constructor ( config ) {
1013 this . config = {
@@ -16,16 +19,101 @@ class Throttler {
1619 'cost' : 1.0 ,
1720 } ;
1821 Object . assign ( this . config , config ) ;
22+ // If multi-rule config provided, normalize rules
23+ this . rules = undefined ;
24+ if ( Array . isArray ( this . config [ 'rules' ] ) ) {
25+ // deep-clone minimal fields for internal use
26+ this . rules = this . config [ 'rules' ] . map ( ( r , idx ) => ( {
27+ 'id' : ( r [ 'id' ] !== undefined ) ? r [ 'id' ] : ( 'rule' + idx . toString ( ) ) ,
28+ 'refillRate' : ( r [ 'refillRate' ] !== undefined ) ? r [ 'refillRate' ] : this . config [ 'refillRate' ] ,
29+ 'capacity' : ( r [ 'capacity' ] !== undefined ) ? r [ 'capacity' ] : this . config [ 'capacity' ] ,
30+ 'tokens' : ( r [ 'tokens' ] !== undefined ) ? r [ 'tokens' ] : 0 ,
31+ 'cost' : ( r [ 'cost' ] !== undefined ) ? r [ 'cost' ] : this . config [ 'cost' ] ,
32+ } ) ) ;
33+ // ensure stable ids
34+ const seen = { } ;
35+ for ( let i = 0 ; i < this . rules . length ; i ++ ) {
36+ const id = this . rules [ i ] [ 'id' ] ;
37+ if ( seen [ id ] ) {
38+ this . rules [ i ] [ 'id' ] = this . rules [ i ] [ 'id' ] + ':' + i . toString ( ) ;
39+ }
40+ seen [ this . rules [ i ] [ 'id' ] ] = true ;
41+ }
42+ }
1943 this . queue = [ ] ;
2044 this . running = false ;
2145 }
2246
47+ // determine if the head of queue can run given current tokens across rules
48+ canRunHead ( ) {
49+ if ( this . queue . length === 0 ) {
50+ return false ;
51+ }
52+ const head = this . queue [ 0 ] ;
53+ if ( ! this . rules ) { // legacy single-rule mode
54+ return this . config [ 'tokens' ] >= 0 ;
55+ }
56+ // multi-rule mode
57+ const costs = head . cost ;
58+ for ( let i = 0 ; i < this . rules . length ; i ++ ) {
59+ const rule = this . rules [ i ] ;
60+ const ruleId = rule [ 'id' ] ;
61+ let cost = 0 ;
62+ if ( costs === undefined ) {
63+ cost = rule [ 'cost' ] ;
64+ } else if ( typeof costs === 'number' ) {
65+ // if a specific 'default' rule exists, apply to that; otherwise apply to all rules
66+ const hasDefault = this . rules . find ( ( r ) => r [ 'id' ] === 'default' ) !== undefined ;
67+ if ( hasDefault ) {
68+ cost = ( ruleId === 'default' ) ? costs : 0 ;
69+ } else {
70+ cost = costs ;
71+ }
72+ } else if ( typeof costs === 'object' ) {
73+ cost = ( costs [ ruleId ] !== undefined ) ? costs [ ruleId ] : 0 ;
74+ }
75+ if ( cost > 0 && rule [ 'tokens' ] < 0 ) {
76+ return false ;
77+ }
78+ }
79+ return true ;
80+ }
81+
2382 async loop ( ) {
2483 let lastTimestamp = now ( ) ;
2584 while ( this . running ) {
85+ if ( this . queue . length === 0 ) {
86+ this . running = false ;
87+ break ;
88+ }
2689 const { resolver, cost } = this . queue [ 0 ] ;
27- if ( this . config [ 'tokens' ] >= 0 ) {
28- this . config [ 'tokens' ] -= cost ;
90+ if ( this . canRunHead ( ) ) {
91+ // consume tokens and resolve
92+ if ( ! this . rules ) {
93+ const consume = ( cost === undefined ) ? this . config [ 'cost' ] : cost ;
94+ this . config [ 'tokens' ] -= consume ;
95+ } else {
96+ for ( let i = 0 ; i < this . rules . length ; i ++ ) {
97+ const rule = this . rules [ i ] ;
98+ const ruleId = rule [ 'id' ] ;
99+ let consume = 0 ;
100+ if ( cost === undefined ) {
101+ consume = rule [ 'cost' ] ;
102+ } else if ( typeof cost === 'number' ) {
103+ const hasDefault = this . rules . find ( ( r ) => r [ 'id' ] === 'default' ) !== undefined ;
104+ if ( hasDefault ) {
105+ consume = ( ruleId === 'default' ) ? cost : 0 ;
106+ } else {
107+ consume = cost ;
108+ }
109+ } else if ( typeof cost === 'object' ) {
110+ consume = ( cost [ ruleId ] !== undefined ) ? cost [ ruleId ] : 0 ;
111+ }
112+ if ( consume !== 0 ) {
113+ rule [ 'tokens' ] -= consume ;
114+ }
115+ }
116+ }
29117 resolver ( ) ;
30118 this . queue . shift ( ) ;
31119 // contextswitch
@@ -38,8 +126,16 @@ class Throttler {
38126 const current = now ( ) ;
39127 const elapsed = current - lastTimestamp ;
40128 lastTimestamp = current ;
41- const tokens = this . config [ 'tokens' ] + ( this . config [ 'refillRate' ] * elapsed ) ;
42- this . config [ 'tokens' ] = Math . min ( tokens , this . config [ 'capacity' ] ) ;
129+ if ( ! this . rules ) {
130+ const tokens = this . config [ 'tokens' ] + ( this . config [ 'refillRate' ] * elapsed ) ;
131+ this . config [ 'tokens' ] = Math . min ( tokens , this . config [ 'capacity' ] ) ;
132+ } else {
133+ for ( let i = 0 ; i < this . rules . length ; i ++ ) {
134+ const rule = this . rules [ i ] ;
135+ const tokens = rule [ 'tokens' ] + ( rule [ 'refillRate' ] * elapsed ) ;
136+ rule [ 'tokens' ] = Math . min ( tokens , rule [ 'capacity' ] ) ;
137+ }
138+ }
43139 }
44140 }
45141 }
@@ -52,8 +148,9 @@ class Throttler {
52148 if ( this . queue . length > this . config [ 'maxCapacity' ] ) {
53149 throw new Error ( 'throttle queue is over maxCapacity (' + this . config [ 'maxCapacity' ] . toString ( ) + '), see https://github.com/ccxt/ccxt/issues/11645#issuecomment-1195695526' ) ;
54150 }
55- cost = ( cost === undefined ) ? this . config [ 'cost' ] : cost ;
56- this . queue . push ( { resolver, cost } ) ;
151+ // in multi-rule mode, cost can be a number or a map of ruleId->cost
152+ const effectiveCost = ( cost === undefined ) ? undefined : cost ;
153+ this . queue . push ( { resolver, cost : effectiveCost } ) ;
57154 if ( ! this . running ) {
58155 this . running = true ;
59156 this . loop ( ) ;
0 commit comments