kersiosocket 0.0.0-stage → 0.6.0-dev
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/BUILD-INFO.md +15 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +191 -2
- package/bench/wabinary.js +31 -0
- package/bridges/whatsmeow/README.md +42 -0
- package/docs/ENGINE-MATRIX.md +23 -0
- package/docs/PROTOCOL-ROADMAP.md +74 -0
- package/docs/RESEARCH-BASELINE.md +36 -0
- package/docs/WABINARY.md +37 -0
- package/examples/basic.js +10 -0
- package/examples/client-payload.js +21 -0
- package/examples/device-props.js +18 -0
- package/examples/sessions.js +11 -0
- package/examples/wabinary.js +15 -0
- package/licenses/THIRD-PARTY-NOTICES.md +12 -0
- package/package.json +31 -4
- package/scripts/import-token-table.js +95 -0
- package/src/backends/baileys.js +121 -0
- package/src/backends/base.js +102 -0
- package/src/backends/whatsmeow-process.js +202 -0
- package/src/backends/zapo.js +123 -0
- package/src/client/kersio-client.js +305 -0
- package/src/client/session-manager.js +67 -0
- package/src/index.js +36 -0
- package/src/pairing/adapter.js +115 -0
- package/src/pairing/lifecycle.js +120 -0
- package/src/pairing/qr.js +38 -0
- package/src/pairing/verified-commit.js +46 -0
- package/src/proto/client-payload.js +185 -0
- package/src/proto/device-props.js +143 -0
- package/src/proto/handshake.js +79 -0
- package/src/proto/wire.js +137 -0
- package/src/protocol/node-protocol.js +155 -0
- package/src/session/codec.js +25 -0
- package/src/session/encrypted-store.js +107 -0
- package/src/session/locking.js +233 -0
- package/src/session/manager.js +61 -0
- package/src/session/stores.js +154 -0
- package/src/socket/connection.js +278 -0
- package/src/socket/frame.js +77 -0
- package/src/socket/make.js +70 -0
- package/src/socket/noise.js +146 -0
- package/src/socket/transport.js +39 -0
- package/src/utils/crypto.js +52 -0
- package/src/utils/latency.js +32 -0
- package/src/utils/lru.js +76 -0
- package/src/wabinary/codec.js +374 -0
- package/src/wabinary/tags.js +25 -0
- package/src/wabinary/token-source.js +146 -0
- package/src/wabinary/tokens.json +5 -0
- package/test/backend-adapters.test.js +59 -0
- package/test/connection.test.js +173 -0
- package/test/device-props.test.js +67 -0
- package/test/encrypted-store.test.js +54 -0
- package/test/frame.test.js +37 -0
- package/test/helpers.js +43 -0
- package/test/locking-archive.test.js +49 -0
- package/test/node-protocol.test.js +90 -0
- package/test/noise.test.js +88 -0
- package/test/pairing-adapter.test.js +71 -0
- package/test/pairing.test.js +32 -0
- package/test/proto.test.js +64 -0
- package/test/session-manager.test.js +34 -0
- package/test/session.test.js +134 -0
- package/test/token-source.test.js +35 -0
- package/test/unified-client.test.js +63 -0
- package/test/verified-commit.test.js +43 -0
- package/test/wabinary.test.js +147 -0
- package/test/whatsmeow-process.test.js +43 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Noise Protocol (pola XX, X25519, AES-GCM, SHA-256) untuk transport WhatsApp Web.
|
|
4
|
+
* Ditulis dari spesifikasi Noise (noiseprotocol.org), bukan salinan library lain.
|
|
5
|
+
*
|
|
6
|
+
* NoiseState : state mesin Noise (hash, chaining key, kunci, counter)
|
|
7
|
+
* ClientHandshake : alur sisi klien (initiator)
|
|
8
|
+
*/
|
|
9
|
+
const {
|
|
10
|
+
EMPTY, generateKeyPair, sharedKey, sha256, hkdf, aesGcmEncrypt, aesGcmDecrypt
|
|
11
|
+
} = require('../utils/crypto')
|
|
12
|
+
const { encodeClientHello, encodeClientFinish, decodeServerHello } = require('../proto/handshake')
|
|
13
|
+
|
|
14
|
+
// Nama protokol, dipad nol sampai 32 byte (panjang hash SHA-256)
|
|
15
|
+
const NOISE_MODE = Buffer.from('Noise_XX_25519_AESGCM_SHA256\0\0\0\0')
|
|
16
|
+
// 'W','A', versi magic, versi dictionary. PERLU diverifikasi ke versi WA Web terbaru.
|
|
17
|
+
const WA_HEADER = Buffer.from([0x57, 0x41, 6, 3])
|
|
18
|
+
// IV AES-GCM memakai counter 32 bit; lewat ini nonce akan berulang (fatal), jadi berhenti.
|
|
19
|
+
const MAX_COUNTER = 0xffffffff
|
|
20
|
+
|
|
21
|
+
class NoiseState {
|
|
22
|
+
constructor(header) {
|
|
23
|
+
this.hash = NOISE_MODE.length === 32 ? NOISE_MODE : sha256(NOISE_MODE)
|
|
24
|
+
this.salt = this.hash // chaining key
|
|
25
|
+
this.encKey = this.hash
|
|
26
|
+
this.decKey = this.hash
|
|
27
|
+
this.readCounter = 0
|
|
28
|
+
this.writeCounter = 0
|
|
29
|
+
this.finished = false
|
|
30
|
+
this.authenticate(header)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** hash = SHA256(hash || data). Tidak dilakukan setelah handshake selesai. */
|
|
34
|
+
authenticate(data) {
|
|
35
|
+
if (!this.finished) this.hash = sha256(Buffer.concat([this.hash, data]))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_iv(counter) {
|
|
39
|
+
const iv = Buffer.alloc(12)
|
|
40
|
+
iv.writeUInt32BE(counter, 8)
|
|
41
|
+
return iv
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
encrypt(plaintext) {
|
|
45
|
+
if (this.writeCounter >= MAX_COUNTER) throw new Error('noise: counter tulis habis, koneksi harus dibuat ulang')
|
|
46
|
+
const aad = this.finished ? EMPTY : this.hash
|
|
47
|
+
const out = aesGcmEncrypt(this.encKey, this._iv(this.writeCounter++), plaintext, aad)
|
|
48
|
+
this.authenticate(out)
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
decrypt(ciphertext) {
|
|
53
|
+
if (this.readCounter >= MAX_COUNTER) throw new Error('noise: counter baca habis, koneksi harus dibuat ulang')
|
|
54
|
+
const aad = this.finished ? EMPTY : this.hash
|
|
55
|
+
const out = aesGcmDecrypt(this.decKey, this._iv(this.readCounter++), ciphertext, aad)
|
|
56
|
+
this.authenticate(ciphertext)
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** MixKey: (ck, k) = HKDF(ck, input). Counter di-reset. */
|
|
61
|
+
mixIntoKey(data) {
|
|
62
|
+
const out = hkdf(data, 64, this.salt)
|
|
63
|
+
this.salt = out.subarray(0, 32)
|
|
64
|
+
this.encKey = this.decKey = out.subarray(32)
|
|
65
|
+
this.readCounter = this.writeCounter = 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Split: turunkan kunci transport akhir. */
|
|
69
|
+
finish(isInitiator) {
|
|
70
|
+
const out = hkdf(EMPTY, 64, this.salt)
|
|
71
|
+
const a = out.subarray(0, 32), b = out.subarray(32)
|
|
72
|
+
this.encKey = isInitiator ? a : b
|
|
73
|
+
this.decKey = isInitiator ? b : a
|
|
74
|
+
this.hash = EMPTY
|
|
75
|
+
this.readCounter = this.writeCounter = 0
|
|
76
|
+
this.finished = true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class ClientHandshake {
|
|
81
|
+
/**
|
|
82
|
+
* @param {{ staticKey: {public:Buffer, private:Buffer}, ephemeralKey?: object, header?: Buffer }} opts
|
|
83
|
+
*/
|
|
84
|
+
constructor({ staticKey, ephemeralKey = generateKeyPair(), header = WA_HEADER, verifyServerCertificate, requireServerCertificateVerification = false }) {
|
|
85
|
+
this.staticKey = staticKey
|
|
86
|
+
this.verifyServerCertificate = verifyServerCertificate
|
|
87
|
+
this.requireServerCertificateVerification = requireServerCertificateVerification
|
|
88
|
+
this.serverCertificateVerified = false
|
|
89
|
+
this.ephemeralKey = ephemeralKey
|
|
90
|
+
this.noise = new NoiseState(header)
|
|
91
|
+
this.noise.authenticate(ephemeralKey.public)
|
|
92
|
+
this.serverStatic = null
|
|
93
|
+
this.serverCertificate = null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pesan pertama (ClientHello), sudah di-encode protobuf. */
|
|
97
|
+
hello() {
|
|
98
|
+
return encodeClientHello(this.ephemeralKey.public)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Proses ServerHello, kembalikan ClientFinish (protobuf).
|
|
103
|
+
* Verifikasi sertifikat dipasok oleh verifier yang menggunakan root/intermediate key
|
|
104
|
+
* resmi yang dipin dan test vector independen. Tidak ada verifier bawaan yang diklaim.
|
|
105
|
+
* @param {Buffer} data isi frame ServerHello
|
|
106
|
+
* @param {Buffer} clientPayload ClientPayload (login/registrasi), sudah di-encode protobuf
|
|
107
|
+
*/
|
|
108
|
+
processServerHello(data, clientPayload) {
|
|
109
|
+
const n = this.noise
|
|
110
|
+
const sh = decodeServerHello(data)
|
|
111
|
+
|
|
112
|
+
n.authenticate(sh.ephemeral)
|
|
113
|
+
n.mixIntoKey(sharedKey(this.ephemeralKey.private, sh.ephemeral))
|
|
114
|
+
|
|
115
|
+
this.serverStatic = n.decrypt(sh.static)
|
|
116
|
+
n.mixIntoKey(sharedKey(this.ephemeralKey.private, this.serverStatic))
|
|
117
|
+
|
|
118
|
+
this.serverCertificate = n.decrypt(sh.payload)
|
|
119
|
+
if (typeof this.verifyServerCertificate === 'function') {
|
|
120
|
+
const verified = this.verifyServerCertificate(this.serverCertificate, {
|
|
121
|
+
serverStatic: Buffer.from(this.serverStatic),
|
|
122
|
+
header: Buffer.from(WA_HEADER)
|
|
123
|
+
})
|
|
124
|
+
if (verified !== true) {
|
|
125
|
+
const error = new Error('Noise: sertifikat server ditolak oleh verifier')
|
|
126
|
+
error.code = 'ERR_NOISE_CERTIFICATE_REJECTED'
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
this.serverCertificateVerified = true
|
|
130
|
+
} else if (this.requireServerCertificateVerification) {
|
|
131
|
+
const error = new Error('Noise: koneksi ditolak karena verifier sertifikat server belum dikonfigurasi')
|
|
132
|
+
error.code = 'ERR_NOISE_CERTIFICATE_VERIFIER_MISSING'
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const encStatic = n.encrypt(this.staticKey.public)
|
|
137
|
+
n.mixIntoKey(sharedKey(this.staticKey.private, sh.ephemeral))
|
|
138
|
+
|
|
139
|
+
const encPayload = n.encrypt(clientPayload)
|
|
140
|
+
n.finish(true)
|
|
141
|
+
|
|
142
|
+
return encodeClientFinish({ static: encStatic, payload: encPayload })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { NoiseState, ClientHandshake, WA_HEADER, NOISE_MODE, MAX_COUNTER }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Adapter WebSocket. Memakai paket `ws` bila terpasang (bisa set header Origin,
|
|
4
|
+
* disarankan untuk WhatsApp), jika tidak jatuh ke WebSocket bawaan Node.
|
|
5
|
+
* Event yang dipancarkan: open, message(Buffer), close(code, reason), transport.error(err)
|
|
6
|
+
*/
|
|
7
|
+
const { EventEmitter } = require('node:events')
|
|
8
|
+
|
|
9
|
+
function createTransport(url, { origin, headers } = {}) {
|
|
10
|
+
const t = new EventEmitter()
|
|
11
|
+
let WS = null
|
|
12
|
+
try { WS = require('ws') } catch { /* opsional */ }
|
|
13
|
+
|
|
14
|
+
if (WS) {
|
|
15
|
+
const ws = new WS(url, { origin, headers, handshakeTimeout: 20000 })
|
|
16
|
+
ws.on('open', () => t.emit('open'))
|
|
17
|
+
ws.on('message', (d) => t.emit('message', Buffer.isBuffer(d) ? d : Buffer.from(d)))
|
|
18
|
+
ws.on('close', (code, reason) => t.emit('close', code, String(reason || '')))
|
|
19
|
+
ws.on('error', (err) => t.emit('transport.error', err))
|
|
20
|
+
t.send = (buf) => ws.send(buf)
|
|
21
|
+
t.close = () => ws.close()
|
|
22
|
+
return t
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof WebSocket === 'undefined') {
|
|
26
|
+
throw new Error('WebSocket tidak tersedia. Pakai Node >= 22 atau jalankan: npm i ws')
|
|
27
|
+
}
|
|
28
|
+
const ws = new WebSocket(url)
|
|
29
|
+
ws.binaryType = 'arraybuffer'
|
|
30
|
+
ws.onopen = () => t.emit('open')
|
|
31
|
+
ws.onmessage = (e) => t.emit('message', Buffer.from(e.data))
|
|
32
|
+
ws.onclose = (e) => t.emit('close', e.code, e.reason)
|
|
33
|
+
ws.onerror = (e) => t.emit('transport.error', e.error || new Error('WebSocket error'))
|
|
34
|
+
t.send = (buf) => ws.send(buf)
|
|
35
|
+
t.close = () => ws.close()
|
|
36
|
+
return t
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { createTransport }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Helper kripto kersiosocket. Hanya memakai node:crypto (tanpa dependensi).
|
|
4
|
+
* Semua fungsi bekerja langsung di atas Buffer agar hemat memori.
|
|
5
|
+
*/
|
|
6
|
+
const crypto = require('node:crypto')
|
|
7
|
+
|
|
8
|
+
const EMPTY = Buffer.alloc(0)
|
|
9
|
+
// Prefix DER untuk membungkus kunci mentah X25519 (32 byte)
|
|
10
|
+
const X25519_SPKI = Buffer.from('302a300506032b656e032100', 'hex')
|
|
11
|
+
const X25519_PKCS8 = Buffer.from('302e020100300506032b656e04220420', 'hex')
|
|
12
|
+
|
|
13
|
+
/** Buat pasangan kunci Curve25519 mentah (32 byte masing-masing). */
|
|
14
|
+
function generateKeyPair() {
|
|
15
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519')
|
|
16
|
+
return {
|
|
17
|
+
public: publicKey.export({ type: 'spki', format: 'der' }).subarray(X25519_SPKI.length),
|
|
18
|
+
private: privateKey.export({ type: 'pkcs8', format: 'der' }).subarray(X25519_PKCS8.length)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Diffie-Hellman X25519: rahasia bersama dari kunci privat sendiri + publik lawan. */
|
|
23
|
+
function sharedKey(privateKey, publicKey) {
|
|
24
|
+
const priv = crypto.createPrivateKey({ key: Buffer.concat([X25519_PKCS8, privateKey]), format: 'der', type: 'pkcs8' })
|
|
25
|
+
const pub = crypto.createPublicKey({ key: Buffer.concat([X25519_SPKI, publicKey]), format: 'der', type: 'spki' })
|
|
26
|
+
return crypto.diffieHellman({ privateKey: priv, publicKey: pub })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha256(data) {
|
|
30
|
+
return crypto.createHash('sha256').update(data).digest()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hkdf(ikm, length, salt, info = EMPTY) {
|
|
34
|
+
return Buffer.from(crypto.hkdfSync('sha256', ikm, salt, info, length))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** AES-256-GCM. Hasil = ciphertext || tag(16). */
|
|
38
|
+
function aesGcmEncrypt(key, iv, plaintext, aad = EMPTY) {
|
|
39
|
+
const c = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
40
|
+
c.setAAD(aad)
|
|
41
|
+
return Buffer.concat([c.update(plaintext), c.final(), c.getAuthTag()])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function aesGcmDecrypt(key, iv, data, aad = EMPTY) {
|
|
45
|
+
if (data.length < 16) throw new Error('ciphertext terlalu pendek')
|
|
46
|
+
const d = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
47
|
+
d.setAAD(aad)
|
|
48
|
+
d.setAuthTag(data.subarray(data.length - 16))
|
|
49
|
+
return Buffer.concat([d.update(data.subarray(0, data.length - 16)), d.final()])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { EMPTY, generateKeyPair, sharedKey, sha256, hkdf, aesGcmEncrypt, aesGcmDecrypt }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/** Pelacak latensi ringan: ring buffer Float64Array tetap (tanpa alokasi per sampel). */
|
|
3
|
+
class LatencyTracker {
|
|
4
|
+
constructor(size = 256) {
|
|
5
|
+
this.buf = new Float64Array(size)
|
|
6
|
+
this.n = 0
|
|
7
|
+
this.i = 0
|
|
8
|
+
this.max = 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Mulai ukur; kembalikan fungsi yang mencatat selisih (ms) saat dipanggil. */
|
|
12
|
+
start() {
|
|
13
|
+
const t0 = process.hrtime.bigint()
|
|
14
|
+
return () => this.add(Number(process.hrtime.bigint() - t0) / 1e6)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
add(ms) {
|
|
18
|
+
this.buf[this.i] = ms
|
|
19
|
+
this.i = (this.i + 1) % this.buf.length
|
|
20
|
+
if (this.n < this.buf.length) this.n++
|
|
21
|
+
if (ms > this.max) this.max = ms
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
summary() {
|
|
25
|
+
if (!this.n) return { count: 0, p50: 0, p95: 0, max: 0 }
|
|
26
|
+
const a = Array.from(this.buf.subarray(0, this.n)).sort((x, y) => x - y)
|
|
27
|
+
const q = (p) => +a[Math.min(a.length - 1, Math.floor(p * a.length))].toFixed(3)
|
|
28
|
+
return { count: this.n, p50: q(0.5), p95: q(0.95), max: +this.max.toFixed(3) }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { LatencyTracker }
|
package/src/utils/lru.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* Cache LRU dengan batas jumlah entri, batas ukuran (byte) opsional, dan TTL opsional.
|
|
4
|
+
* Memakai urutan sisip Map, jadi get/set O(1) tanpa struktur tambahan.
|
|
5
|
+
*
|
|
6
|
+
* new LRUCache({ max: 1000, maxBytes: 8 * 1024 * 1024, sizeOf: (v) => v.length, ttl: 60000 })
|
|
7
|
+
*/
|
|
8
|
+
class LRUCache {
|
|
9
|
+
constructor({ max = 500, maxBytes = Infinity, sizeOf = null, ttl = 0, onEvict = null } = {}) {
|
|
10
|
+
if (!(max > 0)) throw new Error('LRUCache: max harus > 0')
|
|
11
|
+
this.max = max
|
|
12
|
+
this.maxBytes = maxBytes
|
|
13
|
+
this.sizeOf = sizeOf
|
|
14
|
+
this.ttl = ttl
|
|
15
|
+
this.onEvict = onEvict
|
|
16
|
+
this.map = new Map() // key -> { v, size, exp }
|
|
17
|
+
this.bytes = 0
|
|
18
|
+
this.hits = 0
|
|
19
|
+
this.misses = 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get size() { return this.map.size }
|
|
23
|
+
|
|
24
|
+
get(key) {
|
|
25
|
+
const e = this.map.get(key)
|
|
26
|
+
if (!e) { this.misses++; return undefined }
|
|
27
|
+
if (e.exp && e.exp <= Date.now()) { this._drop(key, e, 'expired'); this.misses++; return undefined }
|
|
28
|
+
this.map.delete(key)
|
|
29
|
+
this.map.set(key, e) // pindah ke ujung = paling baru dipakai
|
|
30
|
+
this.hits++
|
|
31
|
+
return e.v
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(key) {
|
|
35
|
+
const e = this.map.get(key)
|
|
36
|
+
if (!e) return false
|
|
37
|
+
if (e.exp && e.exp <= Date.now()) { this._drop(key, e, 'expired'); return false }
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(key, v) {
|
|
42
|
+
const old = this.map.get(key)
|
|
43
|
+
if (old) { this.bytes -= old.size; this.map.delete(key) }
|
|
44
|
+
const size = this.sizeOf ? this.sizeOf(v) : 0
|
|
45
|
+
this.map.set(key, { v, size, exp: this.ttl ? Date.now() + this.ttl : 0 })
|
|
46
|
+
this.bytes += size
|
|
47
|
+
while (this.map.size > this.max || this.bytes > this.maxBytes) {
|
|
48
|
+
const [k, e] = this.map.entries().next().value
|
|
49
|
+
if (k === key && this.map.size === 1) break // satu entri raksasa tetap disimpan
|
|
50
|
+
this._drop(k, e, 'evicted')
|
|
51
|
+
}
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
delete(key) {
|
|
56
|
+
const e = this.map.get(key)
|
|
57
|
+
if (!e) return false
|
|
58
|
+
this.bytes -= e.size
|
|
59
|
+
return this.map.delete(key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
clear() { this.map.clear(); this.bytes = 0 }
|
|
63
|
+
|
|
64
|
+
_drop(key, e, reason) {
|
|
65
|
+
this.map.delete(key)
|
|
66
|
+
this.bytes -= e.size
|
|
67
|
+
if (this.onEvict) this.onEvict(key, e.v, reason)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stats() {
|
|
71
|
+
const n = this.hits + this.misses
|
|
72
|
+
return { size: this.map.size, bytes: this.bytes, hits: this.hits, misses: this.misses, hitRate: n ? +(this.hits / n).toFixed(3) : 0 }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { LRUCache }
|