@nmtjs/client 0.15.2 → 0.16.0-beta.1

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.
Files changed (56) hide show
  1. package/dist/client.d.ts +64 -0
  2. package/dist/client.js +97 -0
  3. package/dist/client.js.map +1 -0
  4. package/dist/clients/runtime.d.ts +6 -12
  5. package/dist/clients/runtime.js +58 -57
  6. package/dist/clients/runtime.js.map +1 -1
  7. package/dist/clients/static.d.ts +4 -9
  8. package/dist/clients/static.js +20 -20
  9. package/dist/clients/static.js.map +1 -1
  10. package/dist/core.d.ts +33 -83
  11. package/dist/core.js +305 -690
  12. package/dist/core.js.map +1 -1
  13. package/dist/events.d.ts +0 -1
  14. package/dist/events.js +74 -11
  15. package/dist/events.js.map +1 -1
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/layers/ping.d.ts +6 -0
  20. package/dist/layers/ping.js +65 -0
  21. package/dist/layers/ping.js.map +1 -0
  22. package/dist/layers/rpc.d.ts +19 -0
  23. package/dist/layers/rpc.js +521 -0
  24. package/dist/layers/rpc.js.map +1 -0
  25. package/dist/layers/streams.d.ts +20 -0
  26. package/dist/layers/streams.js +194 -0
  27. package/dist/layers/streams.js.map +1 -0
  28. package/dist/plugins/browser.js +28 -9
  29. package/dist/plugins/browser.js.map +1 -1
  30. package/dist/plugins/heartbeat.js +10 -10
  31. package/dist/plugins/heartbeat.js.map +1 -1
  32. package/dist/plugins/index.d.ts +1 -1
  33. package/dist/plugins/index.js +0 -1
  34. package/dist/plugins/index.js.map +1 -1
  35. package/dist/plugins/reconnect.js +11 -94
  36. package/dist/plugins/reconnect.js.map +1 -1
  37. package/dist/plugins/types.d.ts +27 -11
  38. package/dist/transport.d.ts +49 -31
  39. package/dist/types.d.ts +21 -5
  40. package/package.json +10 -10
  41. package/src/client.ts +216 -0
  42. package/src/clients/runtime.ts +93 -79
  43. package/src/clients/static.ts +46 -38
  44. package/src/core.ts +394 -901
  45. package/src/events.ts +113 -14
  46. package/src/index.ts +4 -0
  47. package/src/layers/ping.ts +99 -0
  48. package/src/layers/rpc.ts +725 -0
  49. package/src/layers/streams.ts +277 -0
  50. package/src/plugins/browser.ts +39 -9
  51. package/src/plugins/heartbeat.ts +10 -10
  52. package/src/plugins/index.ts +8 -1
  53. package/src/plugins/reconnect.ts +12 -119
  54. package/src/plugins/types.ts +30 -13
  55. package/src/transport.ts +75 -46
  56. package/src/types.ts +33 -8
package/src/events.ts CHANGED
@@ -2,6 +2,16 @@ import type { Callback } from '@nmtjs/common'
2
2
 
3
3
  export type EventMap = { [K: string]: any[] }
4
4
 
5
+ type ListenerRegistration = {
6
+ abortHandler?: () => void
7
+ capture: boolean
8
+ disposed: boolean
9
+ event: string
10
+ listener: Callback
11
+ signal?: AbortSignal
12
+ wrapper: EventListener
13
+ }
14
+
5
15
  // TODO: add errors and promise rejections handling
6
16
  /**
7
17
  * Thin node-like event emitter wrapper around EventTarget
@@ -13,25 +23,101 @@ export class EventEmitter<
13
23
  string
14
24
  >,
15
25
  > {
16
- static once<
17
- T extends EventEmitter,
18
- E extends T extends EventEmitter<any, infer Event> ? Event : never,
19
- >(ee: T, event: E) {
20
- return new Promise((resolve) => ee.once(event, resolve))
26
+ #target = new EventTarget()
27
+ #listeners = new Map<string, Map<Callback, Set<ListenerRegistration>>>()
28
+
29
+ #addRegistration(registration: ListenerRegistration) {
30
+ const events =
31
+ this.#listeners.get(registration.event) ??
32
+ new Map<Callback, Set<ListenerRegistration>>()
33
+
34
+ if (!this.#listeners.has(registration.event)) {
35
+ this.#listeners.set(registration.event, events)
36
+ }
37
+
38
+ const registrations =
39
+ events.get(registration.listener) ?? new Set<ListenerRegistration>()
40
+
41
+ if (!events.has(registration.listener)) {
42
+ events.set(registration.listener, registrations)
43
+ }
44
+
45
+ registrations.add(registration)
21
46
  }
22
47
 
23
- #target = new EventTarget()
24
- #listeners = new Map<Callback, Callback>()
48
+ #removeRegistration(registration: ListenerRegistration) {
49
+ if (registration.disposed) return
50
+
51
+ registration.disposed = true
52
+ this.#target.removeEventListener(
53
+ registration.event,
54
+ registration.wrapper,
55
+ registration.capture,
56
+ )
57
+
58
+ if (registration.signal && registration.abortHandler) {
59
+ registration.signal.removeEventListener(
60
+ 'abort',
61
+ registration.abortHandler,
62
+ )
63
+ }
64
+
65
+ const events = this.#listeners.get(registration.event)
66
+ const registrations = events?.get(registration.listener)
67
+
68
+ registrations?.delete(registration)
69
+
70
+ if (registrations?.size === 0) {
71
+ events?.delete(registration.listener)
72
+ }
73
+
74
+ if (events?.size === 0) {
75
+ this.#listeners.delete(registration.event)
76
+ }
77
+ }
25
78
 
26
79
  on<E extends EventName>(
27
80
  event: E | (Object & string),
28
81
  listener: (...args: Events[E]) => void,
29
82
  options?: AddEventListenerOptions,
30
83
  ) {
31
- const wrapper = (event) => listener(...event.detail)
32
- this.#listeners.set(listener, wrapper)
33
- this.#target.addEventListener(event, wrapper, { ...options, once: false })
34
- return () => this.#target.removeEventListener(event, wrapper)
84
+ const cleanup = () => {
85
+ this.#removeRegistration(registration)
86
+ }
87
+
88
+ const registration: ListenerRegistration = {
89
+ capture: !!options?.capture,
90
+ disposed: false,
91
+ event,
92
+ listener,
93
+ wrapper: (rawEvent) => {
94
+ try {
95
+ listener(...(rawEvent as CustomEvent<Events[E]>).detail)
96
+ } finally {
97
+ if (options?.once) {
98
+ cleanup()
99
+ }
100
+ }
101
+ },
102
+ }
103
+
104
+ if (options?.signal) {
105
+ if (options.signal.aborted) {
106
+ return cleanup
107
+ }
108
+
109
+ registration.signal = options.signal
110
+ registration.abortHandler = cleanup
111
+ options.signal.addEventListener('abort', cleanup, { once: true })
112
+ }
113
+
114
+ this.#addRegistration(registration)
115
+ this.#target.addEventListener(registration.event, registration.wrapper, {
116
+ capture: options?.capture,
117
+ passive: options?.passive,
118
+ })
119
+
120
+ return cleanup
35
121
  }
36
122
 
37
123
  once<E extends EventName>(
@@ -43,8 +129,15 @@ export class EventEmitter<
43
129
  }
44
130
 
45
131
  off(event: EventName | (Object & string), listener: Callback) {
46
- const wrapper = this.#listeners.get(listener)
47
- if (wrapper) this.#target.removeEventListener(event, wrapper)
132
+ const registration = this.#listeners
133
+ .get(event)
134
+ ?.get(listener)
135
+ ?.values()
136
+ .next().value
137
+
138
+ if (registration) {
139
+ this.#removeRegistration(registration)
140
+ }
48
141
  }
49
142
 
50
143
  emit<E extends EventName | (Object & string)>(
@@ -65,6 +158,12 @@ export const once = <
65
158
  signal?: AbortSignal,
66
159
  ) => {
67
160
  return new Promise<EventMap[EventName]>((resolve) => {
68
- ee.once(event, resolve, { signal })
161
+ ee.once(
162
+ event,
163
+ ((...args: EventMap[EventName]) => {
164
+ resolve(args)
165
+ }) as (...args: any[]) => void,
166
+ { signal },
167
+ )
69
168
  })
70
169
  }
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
+ export * from './client.ts'
2
+ export * from './clients/runtime.ts'
3
+ export * from './clients/static.ts'
1
4
  export * from './core.ts'
2
5
  export * from './events.ts'
3
6
  export * from './plugins/index.ts'
7
+ export * from './streams.ts'
4
8
  export * from './transformers.ts'
5
9
  export * from './transport.ts'
6
10
  export * from './types.ts'
@@ -0,0 +1,99 @@
1
+ import type { Future } from '@nmtjs/common'
2
+ import { createFuture, MAX_UINT32, withTimeout } from '@nmtjs/common'
3
+ import {
4
+ ClientMessageType,
5
+ ConnectionType,
6
+ ServerMessageType,
7
+ } from '@nmtjs/protocol'
8
+
9
+ import type { ClientCore } from '../core.ts'
10
+
11
+ export interface PingLayerApi {
12
+ ping(timeout: number, signal?: AbortSignal): Promise<void>
13
+ stopAll(reason?: unknown): void
14
+ }
15
+
16
+ export const createPingLayer = (core: ClientCore): PingLayerApi => {
17
+ let pingNonce = 0
18
+ const pendingPings = new Map<number, Future<void>>()
19
+
20
+ const nextPingNonce = () => {
21
+ if (pingNonce >= MAX_UINT32) {
22
+ pingNonce = 0
23
+ }
24
+
25
+ return pingNonce++
26
+ }
27
+
28
+ const stopAll = (reason?: unknown) => {
29
+ if (!pendingPings.size) return
30
+
31
+ const error = new Error('Heartbeat stopped', { cause: reason })
32
+ for (const pending of pendingPings.values()) {
33
+ pending.reject(error)
34
+ }
35
+ pendingPings.clear()
36
+ }
37
+
38
+ core.on('message', (message: any) => {
39
+ switch (message.type) {
40
+ case ServerMessageType.Pong: {
41
+ const pending = pendingPings.get(message.nonce)
42
+ if (!pending) return
43
+
44
+ pendingPings.delete(message.nonce)
45
+ pending.resolve()
46
+ core.emit('pong', message.nonce)
47
+ break
48
+ }
49
+ case ServerMessageType.Ping: {
50
+ if (!core.messageContext) return
51
+
52
+ const buffer = core.protocol.encodeMessage(
53
+ core.messageContext,
54
+ ClientMessageType.Pong,
55
+ { nonce: message.nonce },
56
+ )
57
+
58
+ core.send(buffer).catch(() => {})
59
+ break
60
+ }
61
+ }
62
+ })
63
+
64
+ core.on('disconnected', (reason) => {
65
+ stopAll(reason)
66
+ })
67
+
68
+ return {
69
+ ping(timeout: number, signal?: AbortSignal) {
70
+ if (
71
+ core.transportType !== ConnectionType.Bidirectional ||
72
+ core.state !== 'connected' ||
73
+ !core.messageContext
74
+ ) {
75
+ return Promise.reject(new Error('Client is not connected'))
76
+ }
77
+
78
+ const nonce = nextPingNonce()
79
+ const future = createFuture<void>()
80
+ pendingPings.set(nonce, future)
81
+
82
+ const buffer = core.protocol.encodeMessage(
83
+ core.messageContext,
84
+ ClientMessageType.Ping,
85
+ { nonce },
86
+ )
87
+
88
+ return core
89
+ .send(buffer, signal)
90
+ .then(() =>
91
+ withTimeout(future.promise, timeout, new Error('Heartbeat timeout')),
92
+ )
93
+ .finally(() => {
94
+ pendingPings.delete(nonce)
95
+ })
96
+ },
97
+ stopAll,
98
+ }
99
+ }