@nmtjs/client 0.15.0-beta.9 → 0.15.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 (50) hide show
  1. package/dist/clients/runtime.d.ts +2 -2
  2. package/dist/clients/runtime.js +1 -1
  3. package/dist/clients/runtime.js.map +1 -1
  4. package/dist/clients/static.d.ts +2 -2
  5. package/dist/clients/static.js +6 -3
  6. package/dist/clients/static.js.map +1 -1
  7. package/dist/core.d.ts +38 -8
  8. package/dist/core.js +414 -66
  9. package/dist/core.js.map +1 -1
  10. package/dist/events.d.ts +1 -1
  11. package/dist/events.js.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +6 -5
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/browser.d.ts +2 -0
  16. package/dist/plugins/browser.js +41 -0
  17. package/dist/plugins/browser.js.map +1 -0
  18. package/dist/plugins/heartbeat.d.ts +6 -0
  19. package/dist/plugins/heartbeat.js +86 -0
  20. package/dist/plugins/heartbeat.js.map +1 -0
  21. package/dist/plugins/index.d.ts +5 -0
  22. package/dist/plugins/index.js +6 -0
  23. package/dist/plugins/index.js.map +1 -0
  24. package/dist/plugins/logging.d.ts +9 -0
  25. package/dist/plugins/logging.js +30 -0
  26. package/dist/plugins/logging.js.map +1 -0
  27. package/dist/plugins/reconnect.d.ts +6 -0
  28. package/dist/plugins/reconnect.js +98 -0
  29. package/dist/plugins/reconnect.js.map +1 -0
  30. package/dist/plugins/types.d.ts +63 -0
  31. package/dist/plugins/types.js +2 -0
  32. package/dist/plugins/types.js.map +1 -0
  33. package/dist/streams.d.ts +3 -3
  34. package/dist/streams.js.map +1 -1
  35. package/dist/transformers.js.map +1 -1
  36. package/dist/types.d.ts +1 -4
  37. package/dist/types.js.map +1 -1
  38. package/package.json +27 -17
  39. package/src/clients/runtime.ts +4 -4
  40. package/src/clients/static.ts +9 -13
  41. package/src/core.ts +476 -77
  42. package/src/index.ts +1 -0
  43. package/src/plugins/browser.ts +61 -0
  44. package/src/plugins/heartbeat.ts +111 -0
  45. package/src/plugins/index.ts +5 -0
  46. package/src/plugins/logging.ts +42 -0
  47. package/src/plugins/reconnect.ts +130 -0
  48. package/src/plugins/types.ts +72 -0
  49. package/src/streams.ts +3 -3
  50. package/src/types.ts +1 -17
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './core.ts'
2
2
  export * from './events.ts'
3
+ export * from './plugins/index.ts'
3
4
  export * from './transformers.ts'
4
5
  export * from './transport.ts'
5
6
  export * from './types.ts'
@@ -0,0 +1,61 @@
1
+ import type { ClientPlugin } from './types.ts'
2
+
3
+ export const browserConnectivityPlugin = (): ClientPlugin => {
4
+ return (client) => {
5
+ const cleanup: Array<() => void> = []
6
+
7
+ const maybeConnect = () => {
8
+ if (client.state === 'disconnected' && !client.isDisposed()) {
9
+ client.connect().catch(() => void 0)
10
+ }
11
+ }
12
+
13
+ return {
14
+ name: 'browser-connectivity',
15
+ onInit: () => {
16
+ if (globalThis.window) {
17
+ const onPageShow = () => maybeConnect()
18
+ globalThis.window.addEventListener('pageshow', onPageShow)
19
+ cleanup.push(() =>
20
+ globalThis.window?.removeEventListener('pageshow', onPageShow),
21
+ )
22
+
23
+ const onOnline = () => maybeConnect()
24
+ globalThis.window.addEventListener('online', onOnline)
25
+ cleanup.push(() =>
26
+ globalThis.window?.removeEventListener('online', onOnline),
27
+ )
28
+
29
+ const onFocus = () => maybeConnect()
30
+ globalThis.window.addEventListener('focus', onFocus)
31
+ cleanup.push(() =>
32
+ globalThis.window?.removeEventListener('focus', onFocus),
33
+ )
34
+ }
35
+
36
+ if (globalThis.document) {
37
+ const onVisibilityChange = () => {
38
+ if (globalThis.document?.visibilityState === 'visible') {
39
+ maybeConnect()
40
+ }
41
+ }
42
+
43
+ globalThis.document.addEventListener(
44
+ 'visibilitychange',
45
+ onVisibilityChange,
46
+ )
47
+ cleanup.push(() =>
48
+ globalThis.document?.removeEventListener(
49
+ 'visibilitychange',
50
+ onVisibilityChange,
51
+ ),
52
+ )
53
+ }
54
+ },
55
+ dispose: () => {
56
+ for (const stop of cleanup) stop()
57
+ cleanup.length = 0
58
+ },
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,111 @@
1
+ import { ConnectionType } from '@nmtjs/protocol'
2
+
3
+ import type { ClientPlugin } from './types.ts'
4
+
5
+ const DEFAULT_HEARTBEAT_INTERVAL = 15000
6
+ const DEFAULT_HEARTBEAT_TIMEOUT = 5000
7
+
8
+ const sleep = (ms: number, signal?: AbortSignal) => {
9
+ return new Promise<void>((resolve) => {
10
+ if (signal?.aborted) return resolve()
11
+ const timer = setTimeout(resolve, ms)
12
+ if (signal) {
13
+ signal.addEventListener(
14
+ 'abort',
15
+ () => {
16
+ clearTimeout(timer)
17
+ resolve()
18
+ },
19
+ { once: true },
20
+ )
21
+ }
22
+ })
23
+ }
24
+
25
+ const isPaused = () => {
26
+ if (globalThis.window && 'navigator' in globalThis.window) {
27
+ if (globalThis.window.navigator?.onLine === false) return true
28
+ }
29
+ if (globalThis.document) {
30
+ if (globalThis.document.visibilityState === 'hidden') return true
31
+ }
32
+ return false
33
+ }
34
+
35
+ export interface HeartbeatPluginOptions {
36
+ interval?: number
37
+ timeout?: number
38
+ }
39
+
40
+ export const heartbeatPlugin = (
41
+ options: HeartbeatPluginOptions = {},
42
+ ): ClientPlugin => {
43
+ return (client) => {
44
+ const interval = options.interval ?? DEFAULT_HEARTBEAT_INTERVAL
45
+ const timeout = options.timeout ?? DEFAULT_HEARTBEAT_TIMEOUT
46
+
47
+ let heartbeatAbortController: AbortController | null = null
48
+ let heartbeatTask: Promise<void> | null = null
49
+
50
+ const stopHeartbeat = () => {
51
+ heartbeatAbortController?.abort()
52
+ heartbeatAbortController = null
53
+ heartbeatTask = null
54
+ }
55
+
56
+ const startHeartbeat = () => {
57
+ if (heartbeatTask) return
58
+ if (client.transportType !== ConnectionType.Bidirectional) return
59
+
60
+ heartbeatAbortController = new AbortController()
61
+ const signal = heartbeatAbortController.signal
62
+
63
+ heartbeatTask = (async () => {
64
+ while (
65
+ !signal.aborted &&
66
+ !client.isDisposed() &&
67
+ client.state === 'connected'
68
+ ) {
69
+ if (isPaused()) {
70
+ await sleep(1000, signal)
71
+ continue
72
+ }
73
+
74
+ await sleep(interval, signal)
75
+
76
+ if (
77
+ signal.aborted ||
78
+ client.isDisposed() ||
79
+ client.state !== 'connected'
80
+ ) {
81
+ continue
82
+ }
83
+
84
+ try {
85
+ await client.ping(timeout, signal)
86
+ } catch {
87
+ if (
88
+ !signal.aborted &&
89
+ !client.isDisposed() &&
90
+ client.state === 'connected'
91
+ ) {
92
+ await client
93
+ .requestReconnect('heartbeat_timeout')
94
+ .catch(() => void 0)
95
+ }
96
+ }
97
+ }
98
+ })().finally(() => {
99
+ heartbeatTask = null
100
+ heartbeatAbortController = null
101
+ })
102
+ }
103
+
104
+ return {
105
+ name: 'heartbeat',
106
+ onConnect: startHeartbeat,
107
+ onDisconnect: () => stopHeartbeat(),
108
+ dispose: stopHeartbeat,
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,5 @@
1
+ export * from './browser.ts'
2
+ export * from './heartbeat.ts'
3
+ export * from './logging.ts'
4
+ export * from './reconnect.ts'
5
+ export * from './types.ts'
@@ -0,0 +1,42 @@
1
+ import type { ClientPlugin, ClientPluginEvent } from './types.ts'
2
+
3
+ export type ClientLogEvent = ClientPluginEvent
4
+
5
+ export interface LoggingPluginOptions {
6
+ includeBodies?: boolean
7
+ onEvent(event: ClientLogEvent): void | Promise<void>
8
+ mapEvent?(event: ClientLogEvent): ClientLogEvent | null
9
+ onSinkError?(error: unknown, event: ClientLogEvent): void
10
+ }
11
+
12
+ const stripEventBody = (event: ClientLogEvent): ClientLogEvent => {
13
+ if (!('body' in event)) return event
14
+
15
+ const { body: _body, ...rest } = event
16
+ return rest as ClientLogEvent
17
+ }
18
+
19
+ export const loggingPlugin = (options: LoggingPluginOptions): ClientPlugin => {
20
+ const includeBodies = options.includeBodies ?? false
21
+
22
+ return () => ({
23
+ name: 'logging',
24
+ onClientEvent: (event) => {
25
+ const eventToMap = includeBodies ? event : stripEventBody(event)
26
+ const mappedEvent = options.mapEvent
27
+ ? options.mapEvent(eventToMap)
28
+ : eventToMap
29
+
30
+ if (!mappedEvent) return
31
+
32
+ try {
33
+ const sinkResult = options.onEvent(mappedEvent)
34
+ Promise.resolve(sinkResult).catch((error) => {
35
+ options.onSinkError?.(error, mappedEvent)
36
+ })
37
+ } catch (error) {
38
+ options.onSinkError?.(error, mappedEvent)
39
+ }
40
+ },
41
+ })
42
+ }
@@ -0,0 +1,130 @@
1
+ import { ConnectionType } from '@nmtjs/protocol'
2
+
3
+ import type { ClientDisconnectReason, ClientPlugin } from './types.ts'
4
+
5
+ const DEFAULT_RECONNECT_TIMEOUT = 1000
6
+ const DEFAULT_MAX_RECONNECT_TIMEOUT = 60000
7
+
8
+ const sleep = (ms: number, signal?: AbortSignal) => {
9
+ return new Promise<void>((resolve) => {
10
+ if (signal?.aborted) return resolve()
11
+ const timer = setTimeout(resolve, ms)
12
+ if (signal) {
13
+ signal.addEventListener(
14
+ 'abort',
15
+ () => {
16
+ clearTimeout(timer)
17
+ resolve()
18
+ },
19
+ { once: true },
20
+ )
21
+ }
22
+ })
23
+ }
24
+
25
+ const computeReconnectDelay = (ms: number) => {
26
+ if (globalThis.window) {
27
+ const jitter = Math.floor(ms * 0.2 * Math.random())
28
+ return ms + jitter
29
+ }
30
+ return ms
31
+ }
32
+
33
+ const isReconnectPaused = () => {
34
+ if (globalThis.window && 'navigator' in globalThis.window) {
35
+ if (globalThis.window.navigator?.onLine === false) return true
36
+ }
37
+ if (globalThis.document) {
38
+ if (globalThis.document.visibilityState === 'hidden') return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ export interface ReconnectPluginOptions {
44
+ initialTimeout?: number
45
+ maxTimeout?: number
46
+ }
47
+
48
+ export const reconnectPlugin = (
49
+ options: ReconnectPluginOptions = {},
50
+ ): ClientPlugin => {
51
+ return (client) => {
52
+ let reconnecting: Promise<void> | null = null
53
+ let reconnectAbortController: AbortController | null = null
54
+ let reconnectTimeout = options.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
55
+
56
+ const cancelReconnect = () => {
57
+ reconnectAbortController?.abort()
58
+ reconnectAbortController = null
59
+ reconnecting = null
60
+ }
61
+
62
+ const ensureReconnectLoop = () => {
63
+ if (reconnecting) return
64
+
65
+ reconnectAbortController = new AbortController()
66
+ const signal = reconnectAbortController.signal
67
+
68
+ reconnecting = (async () => {
69
+ while (
70
+ !signal.aborted &&
71
+ !client.isDisposed() &&
72
+ client.state === 'disconnected' &&
73
+ client.lastDisconnectReason !== 'client'
74
+ ) {
75
+ if (isReconnectPaused()) {
76
+ await sleep(1000, signal)
77
+ continue
78
+ }
79
+
80
+ const delay = computeReconnectDelay(reconnectTimeout)
81
+ await sleep(delay, signal)
82
+
83
+ if (
84
+ signal.aborted ||
85
+ client.isDisposed() ||
86
+ client.state !== 'disconnected' ||
87
+ client.lastDisconnectReason === 'client'
88
+ ) {
89
+ break
90
+ }
91
+
92
+ const previousTimeout = reconnectTimeout
93
+ await client.connect().catch(() => void 0)
94
+
95
+ if (client.state === 'disconnected') {
96
+ reconnectTimeout = Math.min(
97
+ previousTimeout * 2,
98
+ options.maxTimeout ?? DEFAULT_MAX_RECONNECT_TIMEOUT,
99
+ )
100
+ }
101
+ }
102
+ })().finally(() => {
103
+ reconnecting = null
104
+ reconnectAbortController = null
105
+ })
106
+ }
107
+
108
+ const onDisconnect = (reason: ClientDisconnectReason) => {
109
+ if (
110
+ client.transportType !== ConnectionType.Bidirectional ||
111
+ reason === 'client' ||
112
+ client.isDisposed()
113
+ ) {
114
+ cancelReconnect()
115
+ return
116
+ }
117
+ ensureReconnectLoop()
118
+ }
119
+
120
+ return {
121
+ name: 'reconnect',
122
+ onConnect: () => {
123
+ reconnectTimeout = options.initialTimeout ?? DEFAULT_RECONNECT_TIMEOUT
124
+ cancelReconnect()
125
+ },
126
+ onDisconnect,
127
+ dispose: cancelReconnect,
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,72 @@
1
+ import type { BaseClient } from '../core.ts'
2
+
3
+ export type ClientDisconnectReason = 'client' | 'server' | (string & {})
4
+
5
+ export type ClientPluginEvent =
6
+ | {
7
+ kind: 'connected'
8
+ timestamp: number
9
+ transportType: 'bidirectional' | 'unidirectional'
10
+ }
11
+ | { kind: 'disconnected'; timestamp: number; reason: ClientDisconnectReason }
12
+ | {
13
+ kind: 'server_message'
14
+ timestamp: number
15
+ messageType: number | string
16
+ rawByteLength: number
17
+ body?: unknown
18
+ }
19
+ | {
20
+ kind: 'rpc_request'
21
+ timestamp: number
22
+ callId: number
23
+ procedure: string
24
+ body?: unknown
25
+ }
26
+ | {
27
+ kind: 'rpc_response'
28
+ timestamp: number
29
+ callId: number
30
+ procedure: string
31
+ body?: unknown
32
+ stream?: boolean
33
+ }
34
+ | {
35
+ kind: 'rpc_error'
36
+ timestamp: number
37
+ callId: number
38
+ procedure: string
39
+ error: unknown
40
+ }
41
+ | {
42
+ kind: 'stream_event'
43
+ timestamp: number
44
+ direction: 'incoming' | 'outgoing'
45
+ streamType: 'rpc' | 'client_blob' | 'server_blob'
46
+ action: 'response' | 'pull' | 'push' | 'end' | 'abort'
47
+ callId?: number
48
+ streamId?: number
49
+ byteLength?: number
50
+ reason?: string
51
+ }
52
+
53
+ /**
54
+ * Client plugin lifecycle contract.
55
+ *
56
+ * Ordering guarantees:
57
+ * - `onInit`, `onConnect`, `onServerMessage`, `onClientEvent`: registration order
58
+ * - `onDisconnect`, `dispose`: reverse registration order
59
+ */
60
+ export interface ClientPluginInstance {
61
+ name?: string
62
+ onInit?(): void
63
+ onConnect?(): void | Promise<void>
64
+ onDisconnect?(reason: ClientDisconnectReason): void | Promise<void>
65
+ onServerMessage?(message: unknown, raw: ArrayBufferView): void
66
+ onClientEvent?(event: ClientPluginEvent): void | Promise<void>
67
+ dispose?(): void
68
+ }
69
+
70
+ export type ClientPlugin = (
71
+ client: BaseClient<any, any, any, any, any>,
72
+ ) => ClientPluginInstance
package/src/streams.ts CHANGED
@@ -29,7 +29,7 @@ export class ClientStreams {
29
29
  this.#collection.delete(streamId)
30
30
  }
31
31
 
32
- async abort(streamId: number, reason?: string) {
32
+ async abort(streamId: number, reason?: any) {
33
33
  const stream = this.#collection.get(streamId)
34
34
  if (!stream) return // Stream already cleaned up
35
35
  await stream.abort(reason)
@@ -46,7 +46,7 @@ export class ClientStreams {
46
46
  this.remove(streamId)
47
47
  }
48
48
 
49
- async clear(reason?: string) {
49
+ async clear(reason?: any) {
50
50
  if (reason) {
51
51
  const abortPromises = [...this.#collection.values()].map((stream) =>
52
52
  stream.abort(reason),
@@ -118,7 +118,7 @@ export class ServerStreams<
118
118
  this.remove(streamId)
119
119
  }
120
120
 
121
- async clear(reason?: string) {
121
+ async clear(reason?: any) {
122
122
  if (reason) {
123
123
  const abortPromises = [...this.#writers.values()].map((writer) =>
124
124
  writer.abort(reason).finally(() => writer.releaseLock()),
package/src/types.ts CHANGED
@@ -4,7 +4,6 @@ import type { ProtocolBlobInterface } from '@nmtjs/protocol'
4
4
  import type {
5
5
  ProtocolError,
6
6
  ProtocolServerBlobStream,
7
- ProtocolServerStreamInterface,
8
7
  } from '@nmtjs/protocol/client'
9
8
  import type { BaseTypeAny, PlainType, t } from '@nmtjs/type'
10
9
 
@@ -60,9 +59,7 @@ export type AnyResolvedContractProcedure = {
60
59
 
61
60
  export type AnyResolvedContractRouter = {
62
61
  [ResolvedType]: 'router'
63
- [key: string]:
64
- | AnyResolvedContractProcedure
65
- | { [ResolvedType]: 'router'; [key: string]: AnyResolvedContractProcedure }
62
+ [key: string]: AnyResolvedContractProcedure | AnyResolvedContractRouter
66
63
  }
67
64
 
68
65
  export type ResolveAPIRouterRoutes<
@@ -114,19 +111,6 @@ type OmitType<T extends object, E> = {
114
111
  [K in keyof T as T[K] extends E ? never : K]: T[K]
115
112
  }
116
113
 
117
- // export type FilterResolvedContractRouter<
118
- // Resolved extends AnyResolvedContractRouter,
119
- // Stream extends boolean,
120
- // > = {
121
- // [K in keyof Resolved]: Resolved[K] extends AnyResolvedContractProcedure
122
- // ? Resolved[K]['stream'] extends Stream
123
- // ? Resolved[K]
124
- // : never
125
- // : Resolved[K] extends AnyResolvedContractRouter
126
- // ? FilterResolvedContractRouter<Resolved[K], Stream>
127
- // : never
128
- // }
129
-
130
114
  export type ClientCallers<
131
115
  Resolved extends AnyResolvedContractRouter,
132
116
  SafeCall extends boolean,