main
ts 98 lines 2.78 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { Transform, TransformCallback } from 'stream'
4 import { TokenBucket } from 'limiter'
5
6 // throttled stream
7 export class ThrottledStream extends Transform {
8
9 private sent: number = 0
10 private lastSpeed: number = 0
11 private lastSpeedTime = Date.now()
12 private totalSent: number = 0 // total sent over connection, since connection can be re-used for multiple requests
13
14 constructor(private group: ThrottleGroup, copyStats?: ThrottledStream) {
15 super()
16 if (!copyStats) return
17 this.sent = copyStats.sent
18 this.totalSent = copyStats.totalSent
19 this.lastSpeedTime = copyStats.lastSpeedTime
20 this.lastSpeed = copyStats.lastSpeed
21 }
22
23 async _transform(chunk: any, encoding: BufferEncoding, done: TransformCallback) {
24 let pos = 0
25 while (1) {
26 let n = this.group.suggestChunkSize()
27 const slice = chunk.slice(pos, pos + n)
28 n = slice.length
29 if (!n) // we're done here
30 return done()
31 try {
32 await this.group.consume(n)
33 this.push(slice)
34 this.sent += n
35 this.totalSent += n
36 pos += n
37 this.emit('sent', n)
38 } catch (e) {
39 done(e as Error)
40 return
41 }
42 }
43 }
44
45 // @return kBs
46 getSpeed(): number {
47 const now = Date.now()
48 const past = now - this.lastSpeedTime
49 if (past >= 1000) { // recalculate?
50 this.lastSpeedTime = now
51 this.lastSpeed = this.sent / past
52 this.sent = 0
53 }
54 return this.lastSpeed
55 }
56
57 getBytesSent() {
58 return this.totalSent
59 }
60 }
61
62 export class ThrottleGroup {
63
64 private bucket: TokenBucket
65
66 constructor(kBs: number, private parent?: ThrottleGroup) {
67 this.bucket = this.updateLimit(kBs) // assignment is redundant and yet the best way I've found to shut up typescript
68 }
69
70 // @return kBs
71 getLimit() {
72 return this.bucket.bucketSize / 1000
73 }
74
75 updateLimit(kBs: number) {
76 if (kBs < 0)
77 throw Error('invalid bytesPerSecond')
78 kBs *= 1000
79 return this.bucket = new TokenBucket({
80 bucketSize: kBs,
81 tokensPerInterval: kBs,
82 interval: 'second',
83 })
84 }
85
86 suggestChunkSize() {
87 let b: TokenBucket | undefined = this.bucket
88 b.parentBucket = this.parent?.bucket
89 let min = b.bucketSize
90 while (b = b.parentBucket)
91 min = Math.min(min, b.bucketSize)
92 return min / 10
93 }
94
95 consume(n: number) {
96 return this.bucket.removeTokens(n)
97 }
98 }