walletpair-sdk 1.0.2 → 1.0.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/README.md +13 -0
- package/dist/ble/framing.d.ts.map +1 -1
- package/dist/ble/framing.js +2 -2
- package/dist/ble/framing.js.map +1 -1
- package/dist/ble/index.d.ts +2 -2
- package/dist/ble/index.d.ts.map +1 -1
- package/dist/ble/index.js +2 -2
- package/dist/ble/index.js.map +1 -1
- package/dist/ble/web-ble-transport.d.ts +1 -1
- package/dist/ble/web-ble-transport.d.ts.map +1 -1
- package/dist/ble/web-ble-transport.js +23 -12
- package/dist/ble/web-ble-transport.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +29 -12
- package/dist/crypto.js.map +1 -1
- package/dist/dapp-session.d.ts.map +1 -1
- package/dist/dapp-session.js +15 -5
- package/dist/dapp-session.js.map +1 -1
- package/dist/emitter.d.ts +1 -3
- package/dist/emitter.d.ts.map +1 -1
- package/dist/emitter.js +4 -2
- package/dist/emitter.js.map +1 -1
- package/dist/evm/eip1193.d.ts +2 -2
- package/dist/evm/eip1193.d.ts.map +1 -1
- package/dist/evm/eip1193.js +32 -18
- package/dist/evm/eip1193.js.map +1 -1
- package/dist/evm/index.d.ts +2 -2
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js.map +1 -1
- package/dist/wallet-session.d.ts.map +1 -1
- package/dist/wallet-session.js +4 -3
- package/dist/wallet-session.js.map +1 -1
- package/dist/ws-transport.d.ts +3 -2
- package/dist/ws-transport.d.ts.map +1 -1
- package/dist/ws-transport.js +13 -4
- package/dist/ws-transport.js.map +1 -1
- package/package.json +20 -1
- package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
- package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
- package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
- package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
- package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
- package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
- package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
- package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
- package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
- package/src/ble/framing.test.ts +122 -114
- package/src/ble/framing.ts +48 -51
- package/src/ble/index.ts +7 -7
- package/src/ble/web-ble-transport.test.ts +93 -84
- package/src/ble/web-ble-transport.ts +70 -57
- package/src/ble/web-bluetooth.d.ts +19 -19
- package/src/canonical-json.test.ts +301 -285
- package/src/crypto-directional.test.ts +155 -129
- package/src/crypto-hardening.test.ts +292 -283
- package/src/crypto.test.ts +364 -346
- package/src/crypto.ts +185 -175
- package/src/dapp-session.test.ts +522 -385
- package/src/dapp-session.ts +17 -11
- package/src/emitter.test.ts +122 -122
- package/src/emitter.ts +20 -18
- package/src/evm/eip1193.test.ts +283 -205
- package/src/evm/eip1193.ts +162 -138
- package/src/evm/index.ts +5 -5
- package/src/evm/wagmi.test.ts +1 -1
- package/src/integration.test.ts +329 -201
- package/src/security.test.ts +331 -238
- package/src/sequence-validation.test.ts +6 -9
- package/src/test-helpers.ts +102 -78
- package/src/types.test.ts +45 -50
- package/src/wallet-session.test.ts +611 -383
- package/src/wallet-session.ts +7 -9
- package/src/ws-transport.test.ts +141 -139
- package/src/ws-transport.ts +52 -41
package/src/dapp-session.ts
CHANGED
|
@@ -231,6 +231,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
const id = `req-${++this.reqCounter}`
|
|
234
|
+
const sendKey = this.sendKey
|
|
234
235
|
|
|
235
236
|
// Always seal: even parameterless requests must be authenticated via AEAD
|
|
236
237
|
// to prevent method injection by a malicious relay.
|
|
@@ -243,7 +244,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
243
244
|
? (params as Record<string, unknown>)
|
|
244
245
|
: { _params: params ?? {} }),
|
|
245
246
|
}
|
|
246
|
-
const sealed = sealPayload(
|
|
247
|
+
const sealed = sealPayload(sendKey, this.channelId, seq, sealedParams, hdr)
|
|
247
248
|
|
|
248
249
|
const msg: ProtocolMessage = {
|
|
249
250
|
v: 1,
|
|
@@ -260,7 +261,13 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
260
261
|
reject(new Error(`Request ${method} timed out`))
|
|
261
262
|
}, this.requestTimeout)
|
|
262
263
|
|
|
263
|
-
this.pendingRequests.set(id, {
|
|
264
|
+
this.pendingRequests.set(id, {
|
|
265
|
+
id,
|
|
266
|
+
method,
|
|
267
|
+
resolve: resolve as (v: unknown) => void,
|
|
268
|
+
reject,
|
|
269
|
+
timer,
|
|
270
|
+
})
|
|
264
271
|
this.sendRaw(msg)
|
|
265
272
|
})
|
|
266
273
|
}
|
|
@@ -400,6 +407,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
400
407
|
d.dappMeta ??
|
|
401
408
|
(d.dappName ? { name: d.dappName, description: '', url: '', icon: '' } : this.meta)
|
|
402
409
|
this.sessionStartTime = d.sessionStartTime ?? null
|
|
410
|
+
this.setPhase('connected')
|
|
403
411
|
return true
|
|
404
412
|
} catch {
|
|
405
413
|
return false
|
|
@@ -560,9 +568,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
560
568
|
const declared = new Set(joinCapabilities.methods)
|
|
561
569
|
const absent = requiredMethods.filter((m) => !declared.has(m))
|
|
562
570
|
if (absent.length > 0) {
|
|
563
|
-
console.warn(
|
|
564
|
-
`[WalletPair] Wallet missing MUST-support methods: ${absent.join(', ')}`,
|
|
565
|
-
)
|
|
571
|
+
console.warn(`[WalletPair] Wallet missing MUST-support methods: ${absent.join(', ')}`)
|
|
566
572
|
}
|
|
567
573
|
}
|
|
568
574
|
|
|
@@ -634,6 +640,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
634
640
|
}
|
|
635
641
|
|
|
636
642
|
case 'res': {
|
|
643
|
+
if (this.phase !== 'connected') break
|
|
637
644
|
const resBody = msg.body as { id?: string; sealed?: string }
|
|
638
645
|
if (this.remotePubKey && msg.from !== b64urlEncode(this.remotePubKey)) break
|
|
639
646
|
if (!resBody.id) break
|
|
@@ -700,6 +707,7 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
700
707
|
}
|
|
701
708
|
|
|
702
709
|
case 'evt': {
|
|
710
|
+
if (this.phase !== 'connected') break
|
|
703
711
|
const evtBody = msg.body as { id?: string; sealed?: string }
|
|
704
712
|
if (this.remotePubKey && msg.from !== b64urlEncode(this.remotePubKey)) break
|
|
705
713
|
// Events MUST be sealed — drop unsealed events to prevent forgery.
|
|
@@ -729,12 +737,10 @@ export class DAppSession extends Emitter<DAppSessionEvents> {
|
|
|
729
737
|
}
|
|
730
738
|
const persisted = this.persistSnapshot()
|
|
731
739
|
if (isPromiseLike(persisted)) {
|
|
732
|
-
void persisted
|
|
733
|
-
.
|
|
734
|
-
.
|
|
735
|
-
|
|
736
|
-
this.emit('error', this.persistenceError(e))
|
|
737
|
-
})
|
|
740
|
+
void persisted.then(afterPersist).catch((e) => {
|
|
741
|
+
this.recvSeq = prevRecvSeqEvt // rollback on persist failure
|
|
742
|
+
this.emit('error', this.persistenceError(e))
|
|
743
|
+
})
|
|
738
744
|
} else {
|
|
739
745
|
afterPersist()
|
|
740
746
|
}
|
package/src/emitter.test.ts
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import { Emitter } from './emitter.js'
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { Emitter } from './emitter.js'
|
|
3
3
|
|
|
4
4
|
interface TestEvents {
|
|
5
|
-
[key: string]: unknown
|
|
6
|
-
message: string
|
|
7
|
-
count: number
|
|
8
|
-
complex: { name: string; value: number }
|
|
5
|
+
[key: string]: unknown
|
|
6
|
+
message: string
|
|
7
|
+
count: number
|
|
8
|
+
complex: { name: string; value: number }
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
describe('Emitter', () => {
|
|
12
12
|
it('emits events to registered handlers', () => {
|
|
13
|
-
const emitter = new Emitter<TestEvents>()
|
|
14
|
-
const handler = vi.fn()
|
|
15
|
-
emitter.on('message', handler)
|
|
16
|
-
emitter.emit('message', 'hello')
|
|
17
|
-
expect(handler).toHaveBeenCalledWith('hello')
|
|
18
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
19
|
-
})
|
|
13
|
+
const emitter = new Emitter<TestEvents>()
|
|
14
|
+
const handler = vi.fn()
|
|
15
|
+
emitter.on('message', handler)
|
|
16
|
+
emitter.emit('message', 'hello')
|
|
17
|
+
expect(handler).toHaveBeenCalledWith('hello')
|
|
18
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
19
|
+
})
|
|
20
20
|
|
|
21
21
|
it('supports multiple handlers for the same event', () => {
|
|
22
|
-
const emitter = new Emitter<TestEvents>()
|
|
23
|
-
const h1 = vi.fn()
|
|
24
|
-
const h2 = vi.fn()
|
|
25
|
-
emitter.on('message', h1)
|
|
26
|
-
emitter.on('message', h2)
|
|
27
|
-
emitter.emit('message', 'test')
|
|
28
|
-
expect(h1).toHaveBeenCalledWith('test')
|
|
29
|
-
expect(h2).toHaveBeenCalledWith('test')
|
|
30
|
-
})
|
|
22
|
+
const emitter = new Emitter<TestEvents>()
|
|
23
|
+
const h1 = vi.fn()
|
|
24
|
+
const h2 = vi.fn()
|
|
25
|
+
emitter.on('message', h1)
|
|
26
|
+
emitter.on('message', h2)
|
|
27
|
+
emitter.emit('message', 'test')
|
|
28
|
+
expect(h1).toHaveBeenCalledWith('test')
|
|
29
|
+
expect(h2).toHaveBeenCalledWith('test')
|
|
30
|
+
})
|
|
31
31
|
|
|
32
32
|
it('supports multiple event types', () => {
|
|
33
|
-
const emitter = new Emitter<TestEvents>()
|
|
34
|
-
const msgHandler = vi.fn()
|
|
35
|
-
const countHandler = vi.fn()
|
|
36
|
-
emitter.on('message', msgHandler)
|
|
37
|
-
emitter.on('count', countHandler)
|
|
33
|
+
const emitter = new Emitter<TestEvents>()
|
|
34
|
+
const msgHandler = vi.fn()
|
|
35
|
+
const countHandler = vi.fn()
|
|
36
|
+
emitter.on('message', msgHandler)
|
|
37
|
+
emitter.on('count', countHandler)
|
|
38
38
|
|
|
39
|
-
emitter.emit('message', 'hello')
|
|
40
|
-
emitter.emit('count', 42)
|
|
39
|
+
emitter.emit('message', 'hello')
|
|
40
|
+
emitter.emit('count', 42)
|
|
41
41
|
|
|
42
|
-
expect(msgHandler).toHaveBeenCalledWith('hello')
|
|
43
|
-
expect(countHandler).toHaveBeenCalledWith(42)
|
|
44
|
-
expect(msgHandler).toHaveBeenCalledTimes(1)
|
|
45
|
-
expect(countHandler).toHaveBeenCalledTimes(1)
|
|
46
|
-
})
|
|
42
|
+
expect(msgHandler).toHaveBeenCalledWith('hello')
|
|
43
|
+
expect(countHandler).toHaveBeenCalledWith(42)
|
|
44
|
+
expect(msgHandler).toHaveBeenCalledTimes(1)
|
|
45
|
+
expect(countHandler).toHaveBeenCalledTimes(1)
|
|
46
|
+
})
|
|
47
47
|
|
|
48
48
|
it('on() returns an unsubscribe function', () => {
|
|
49
|
-
const emitter = new Emitter<TestEvents>()
|
|
50
|
-
const handler = vi.fn()
|
|
51
|
-
const off = emitter.on('message', handler)
|
|
49
|
+
const emitter = new Emitter<TestEvents>()
|
|
50
|
+
const handler = vi.fn()
|
|
51
|
+
const off = emitter.on('message', handler)
|
|
52
52
|
|
|
53
|
-
emitter.emit('message', 'first')
|
|
54
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
53
|
+
emitter.emit('message', 'first')
|
|
54
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
55
55
|
|
|
56
|
-
off()
|
|
57
|
-
emitter.emit('message', 'second')
|
|
58
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
59
|
-
})
|
|
56
|
+
off()
|
|
57
|
+
emitter.emit('message', 'second')
|
|
58
|
+
expect(handler).toHaveBeenCalledTimes(1) // not called again
|
|
59
|
+
})
|
|
60
60
|
|
|
61
61
|
it('off() removes a specific handler', () => {
|
|
62
|
-
const emitter = new Emitter<TestEvents>()
|
|
63
|
-
const h1 = vi.fn()
|
|
64
|
-
const h2 = vi.fn()
|
|
65
|
-
emitter.on('message', h1)
|
|
66
|
-
emitter.on('message', h2)
|
|
62
|
+
const emitter = new Emitter<TestEvents>()
|
|
63
|
+
const h1 = vi.fn()
|
|
64
|
+
const h2 = vi.fn()
|
|
65
|
+
emitter.on('message', h1)
|
|
66
|
+
emitter.on('message', h2)
|
|
67
67
|
|
|
68
|
-
emitter.off('message', h1)
|
|
69
|
-
emitter.emit('message', 'test')
|
|
68
|
+
emitter.off('message', h1)
|
|
69
|
+
emitter.emit('message', 'test')
|
|
70
70
|
|
|
71
|
-
expect(h1).not.toHaveBeenCalled()
|
|
72
|
-
expect(h2).toHaveBeenCalledWith('test')
|
|
73
|
-
})
|
|
71
|
+
expect(h1).not.toHaveBeenCalled()
|
|
72
|
+
expect(h2).toHaveBeenCalledWith('test')
|
|
73
|
+
})
|
|
74
74
|
|
|
75
75
|
it('off() without handler removes all handlers for that event', () => {
|
|
76
|
-
const emitter = new Emitter<TestEvents>()
|
|
77
|
-
const h1 = vi.fn()
|
|
78
|
-
const h2 = vi.fn()
|
|
79
|
-
emitter.on('message', h1)
|
|
80
|
-
emitter.on('message', h2)
|
|
76
|
+
const emitter = new Emitter<TestEvents>()
|
|
77
|
+
const h1 = vi.fn()
|
|
78
|
+
const h2 = vi.fn()
|
|
79
|
+
emitter.on('message', h1)
|
|
80
|
+
emitter.on('message', h2)
|
|
81
81
|
|
|
82
|
-
emitter.off('message')
|
|
83
|
-
emitter.emit('message', 'test')
|
|
82
|
+
emitter.off('message')
|
|
83
|
+
emitter.emit('message', 'test')
|
|
84
84
|
|
|
85
|
-
expect(h1).not.toHaveBeenCalled()
|
|
86
|
-
expect(h2).not.toHaveBeenCalled()
|
|
87
|
-
})
|
|
85
|
+
expect(h1).not.toHaveBeenCalled()
|
|
86
|
+
expect(h2).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
88
|
|
|
89
89
|
it('once() fires handler only once', () => {
|
|
90
|
-
const emitter = new Emitter<TestEvents>()
|
|
91
|
-
const handler = vi.fn()
|
|
92
|
-
emitter.once('message', handler)
|
|
90
|
+
const emitter = new Emitter<TestEvents>()
|
|
91
|
+
const handler = vi.fn()
|
|
92
|
+
emitter.once('message', handler)
|
|
93
93
|
|
|
94
|
-
emitter.emit('message', 'first')
|
|
95
|
-
emitter.emit('message', 'second')
|
|
94
|
+
emitter.emit('message', 'first')
|
|
95
|
+
emitter.emit('message', 'second')
|
|
96
96
|
|
|
97
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
98
|
-
expect(handler).toHaveBeenCalledWith('first')
|
|
99
|
-
})
|
|
97
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(handler).toHaveBeenCalledWith('first')
|
|
99
|
+
})
|
|
100
100
|
|
|
101
101
|
it('once() returns an unsubscribe function that works before emit', () => {
|
|
102
|
-
const emitter = new Emitter<TestEvents>()
|
|
103
|
-
const handler = vi.fn()
|
|
104
|
-
const off = emitter.once('message', handler)
|
|
102
|
+
const emitter = new Emitter<TestEvents>()
|
|
103
|
+
const handler = vi.fn()
|
|
104
|
+
const off = emitter.once('message', handler)
|
|
105
105
|
|
|
106
|
-
off()
|
|
107
|
-
emitter.emit('message', 'test')
|
|
108
|
-
expect(handler).not.toHaveBeenCalled()
|
|
109
|
-
})
|
|
106
|
+
off() // cancel before any emission
|
|
107
|
+
emitter.emit('message', 'test')
|
|
108
|
+
expect(handler).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
110
|
|
|
111
111
|
it('removeAll() clears all events', () => {
|
|
112
|
-
const emitter = new Emitter<TestEvents>()
|
|
113
|
-
const h1 = vi.fn()
|
|
114
|
-
const h2 = vi.fn()
|
|
115
|
-
emitter.on('message', h1)
|
|
116
|
-
emitter.on('count', h2)
|
|
112
|
+
const emitter = new Emitter<TestEvents>()
|
|
113
|
+
const h1 = vi.fn()
|
|
114
|
+
const h2 = vi.fn()
|
|
115
|
+
emitter.on('message', h1)
|
|
116
|
+
emitter.on('count', h2)
|
|
117
117
|
|
|
118
|
-
emitter.removeAll()
|
|
119
|
-
emitter.emit('message', 'test')
|
|
120
|
-
emitter.emit('count', 1)
|
|
118
|
+
emitter.removeAll()
|
|
119
|
+
emitter.emit('message', 'test')
|
|
120
|
+
emitter.emit('count', 1)
|
|
121
121
|
|
|
122
|
-
expect(h1).not.toHaveBeenCalled()
|
|
123
|
-
expect(h2).not.toHaveBeenCalled()
|
|
124
|
-
})
|
|
122
|
+
expect(h1).not.toHaveBeenCalled()
|
|
123
|
+
expect(h2).not.toHaveBeenCalled()
|
|
124
|
+
})
|
|
125
125
|
|
|
126
126
|
it('emitting with no handlers does not throw', () => {
|
|
127
|
-
const emitter = new Emitter<TestEvents>()
|
|
128
|
-
expect(() => emitter.emit('message', 'test')).not.toThrow()
|
|
129
|
-
})
|
|
127
|
+
const emitter = new Emitter<TestEvents>()
|
|
128
|
+
expect(() => emitter.emit('message', 'test')).not.toThrow()
|
|
129
|
+
})
|
|
130
130
|
|
|
131
131
|
it('handles complex event data', () => {
|
|
132
|
-
const emitter = new Emitter<TestEvents>()
|
|
133
|
-
const handler = vi.fn()
|
|
134
|
-
emitter.on('complex', handler)
|
|
132
|
+
const emitter = new Emitter<TestEvents>()
|
|
133
|
+
const handler = vi.fn()
|
|
134
|
+
emitter.on('complex', handler)
|
|
135
135
|
|
|
136
|
-
const data = { name: 'test', value: 99 }
|
|
137
|
-
emitter.emit('complex', data)
|
|
138
|
-
expect(handler).toHaveBeenCalledWith(data)
|
|
139
|
-
})
|
|
136
|
+
const data = { name: 'test', value: 99 }
|
|
137
|
+
emitter.emit('complex', data)
|
|
138
|
+
expect(handler).toHaveBeenCalledWith(data)
|
|
139
|
+
})
|
|
140
140
|
|
|
141
141
|
it('handler added during emit is not called in the same emit cycle', () => {
|
|
142
|
-
const emitter = new Emitter<TestEvents>()
|
|
143
|
-
const late = vi.fn()
|
|
142
|
+
const emitter = new Emitter<TestEvents>()
|
|
143
|
+
const late = vi.fn()
|
|
144
144
|
emitter.on('message', () => {
|
|
145
|
-
emitter.on('message', late)
|
|
146
|
-
})
|
|
147
|
-
emitter.emit('message', 'trigger')
|
|
145
|
+
emitter.on('message', late)
|
|
146
|
+
})
|
|
147
|
+
emitter.emit('message', 'trigger')
|
|
148
148
|
// The late handler was added during iteration, behavior depends on Set iteration
|
|
149
149
|
// but it should not cause errors
|
|
150
|
-
})
|
|
150
|
+
})
|
|
151
151
|
|
|
152
152
|
it('multiple on() calls return independent unsubscribe functions', () => {
|
|
153
|
-
const emitter = new Emitter<TestEvents>()
|
|
154
|
-
const handler = vi.fn()
|
|
155
|
-
const off1 = emitter.on('message', handler)
|
|
156
|
-
const off2 = emitter.on('count', handler)
|
|
157
|
-
|
|
158
|
-
off1()
|
|
159
|
-
emitter.emit('message', 'gone')
|
|
160
|
-
emitter.emit('count', 42)
|
|
161
|
-
|
|
162
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
163
|
-
expect(handler).toHaveBeenCalledWith(42)
|
|
164
|
-
|
|
165
|
-
off2()
|
|
166
|
-
emitter.emit('count', 99)
|
|
167
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
168
|
-
})
|
|
169
|
-
})
|
|
153
|
+
const emitter = new Emitter<TestEvents>()
|
|
154
|
+
const handler = vi.fn()
|
|
155
|
+
const off1 = emitter.on('message', handler)
|
|
156
|
+
const off2 = emitter.on('count', handler)
|
|
157
|
+
|
|
158
|
+
off1()
|
|
159
|
+
emitter.emit('message', 'gone')
|
|
160
|
+
emitter.emit('count', 42)
|
|
161
|
+
|
|
162
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
163
|
+
expect(handler).toHaveBeenCalledWith(42)
|
|
164
|
+
|
|
165
|
+
off2()
|
|
166
|
+
emitter.emit('count', 99)
|
|
167
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
168
|
+
})
|
|
169
|
+
})
|
package/src/emitter.ts
CHANGED
|
@@ -2,44 +2,46 @@
|
|
|
2
2
|
* Minimal typed event emitter — no external dependencies.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
type Handler<T> = (data: T) => void
|
|
5
|
+
type Handler<T> = (data: T) => void
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
private handlers = new Map<keyof Events, Set<Handler<any>>>()
|
|
7
|
+
export class Emitter<Events extends Record<string, unknown>> {
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic handler storage requires any
|
|
9
|
+
private handlers = new Map<keyof Events, Set<Handler<any>>>()
|
|
10
10
|
|
|
11
11
|
on<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
|
|
12
|
-
let set = this.handlers.get(event)
|
|
12
|
+
let set = this.handlers.get(event)
|
|
13
13
|
if (!set) {
|
|
14
|
-
set = new Set()
|
|
15
|
-
this.handlers.set(event, set)
|
|
14
|
+
set = new Set()
|
|
15
|
+
this.handlers.set(event, set)
|
|
16
|
+
}
|
|
17
|
+
set.add(handler)
|
|
18
|
+
return () => {
|
|
19
|
+
set?.delete(handler)
|
|
16
20
|
}
|
|
17
|
-
set.add(handler);
|
|
18
|
-
return () => { set!.delete(handler); };
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
once<K extends keyof Events>(event: K, handler: Handler<Events[K]>): () => void {
|
|
22
24
|
const off = this.on(event, (data) => {
|
|
23
|
-
off()
|
|
24
|
-
handler(data)
|
|
25
|
-
})
|
|
26
|
-
return off
|
|
25
|
+
off()
|
|
26
|
+
handler(data)
|
|
27
|
+
})
|
|
28
|
+
return off
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
emit<K extends keyof Events>(event: K, data: Events[K]): void {
|
|
30
|
-
const set = this.handlers.get(event)
|
|
31
|
-
if (set) for (const h of set) h(data)
|
|
32
|
+
const set = this.handlers.get(event)
|
|
33
|
+
if (set) for (const h of set) h(data)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
off<K extends keyof Events>(event: K, handler?: Handler<Events[K]>): void {
|
|
35
37
|
if (handler) {
|
|
36
|
-
this.handlers.get(event)?.delete(handler)
|
|
38
|
+
this.handlers.get(event)?.delete(handler)
|
|
37
39
|
} else {
|
|
38
|
-
this.handlers.delete(event)
|
|
40
|
+
this.handlers.delete(event)
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
removeAll(): void {
|
|
43
|
-
this.handlers.clear()
|
|
45
|
+
this.handlers.clear()
|
|
44
46
|
}
|
|
45
47
|
}
|