@owlmeans/client-socket 0.1.18-rc.47 → 0.1.18-rc.49
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 +6 -6
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-socket/SKILL.md +1 -1
- package/build/consts.d.ts +1 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +1 -0
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +3 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +2 -0
- package/build/consts.local.js.map +1 -0
- package/build/helper/types.d.ts +22 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +7 -28
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +74 -141
- package/build/helper.js.map +1 -1
- package/build/hooks.d.ts +18 -0
- package/build/hooks.d.ts.map +1 -0
- package/build/hooks.js +66 -0
- package/build/hooks.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/status.d.ts +0 -1
- package/build/status.d.ts.map +1 -1
- package/build/status.js +2 -2
- package/build/status.js.map +1 -1
- package/build/utils/connection.d.ts +1 -30
- package/build/utils/connection.d.ts.map +1 -1
- package/build/utils/connection.js +1 -5
- package/build/utils/connection.js.map +1 -1
- package/build/utils/consts.local.d.ts +6 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +6 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/types.d.ts +31 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/package.json +13 -13
- package/src/consts.local.ts +3 -0
- package/src/consts.ts +2 -0
- package/src/helper/types.ts +20 -0
- package/src/helper.ts +90 -160
- package/src/hooks.ts +75 -0
- package/src/index.ts +2 -0
- package/src/status.ts +2 -4
- package/src/utils/connection.ts +3 -43
- package/src/utils/consts.local.ts +5 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/types.ts +33 -0
- package/tests/reconnect.spec.ts +11 -11
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ClientEntrypoint, clientRequestHelper } from '@owlmeans/client-entrypoint'
|
|
2
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
|
+
import type { Connection } from '@owlmeans/socket'
|
|
4
|
+
import type { EntrypointReference } from '@owlmeans/context'
|
|
5
|
+
import type { SocketStatusServiceAppend, WsOptions } from './types.js'
|
|
6
|
+
import { useContext, useValue } from '@owlmeans/client'
|
|
7
|
+
import { AUTH_QUERY } from '@owlmeans/auth'
|
|
8
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
9
|
+
import { SOCKET_STATUS } from './consts.js'
|
|
10
|
+
import { socketClientHelper } from './helper.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Open a socket through a declared entrypoint protocol.
|
|
14
|
+
*
|
|
15
|
+
* String aliases remain an adapter input for dynamically addressed integrations, while
|
|
16
|
+
* application callers pass their immutable protocol declaration and receive its bound client
|
|
17
|
+
* entrypoint here at the transport boundary.
|
|
18
|
+
*
|
|
19
|
+
* Re-opens on its own after a network drop (see `ws()`). `null` until the first attempt opens,
|
|
20
|
+
* and again once the retry budget elapses without one ever opening — dialed afresh on the next
|
|
21
|
+
* `SocketStatusService.retry()`. A screen that must tell those two apart reads
|
|
22
|
+
* `useSocketStatus()` (from `./status.js`) alongside this.
|
|
23
|
+
*/
|
|
24
|
+
export const useWs = (
|
|
25
|
+
module: EntrypointReference | string, request?: Partial<AbstractRequest<any>>, options?: WsOptions
|
|
26
|
+
): Connection | null => {
|
|
27
|
+
const ctx = useContext()
|
|
28
|
+
const mod = useMemo(
|
|
29
|
+
() => ctx.entrypoint<ClientEntrypoint>(module),
|
|
30
|
+
[module]
|
|
31
|
+
)
|
|
32
|
+
const [failed, setFailed] = useState(false)
|
|
33
|
+
const [dial, setDial] = useState(0)
|
|
34
|
+
const connection = useValue<Connection | null>(async (cancel) => {
|
|
35
|
+
const _request = clientRequestHelper.provideRequest(mod.alias, mod.path())
|
|
36
|
+
Object.assign(_request, request)
|
|
37
|
+
try {
|
|
38
|
+
const conn = await socketClientHelper.ws(mod, _request, options)
|
|
39
|
+
if (cancel?.current) {
|
|
40
|
+
// The component unmounted while the handshake (or its retries) were still in flight —
|
|
41
|
+
// there is no `useEffect` cleanup coming for this value, so close it here instead of
|
|
42
|
+
// leaving a live socket and its retry timers attached to nothing.
|
|
43
|
+
void conn.close()
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
return conn
|
|
47
|
+
} catch {
|
|
48
|
+
if (!cancel?.current) setFailed(true)
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}, [
|
|
52
|
+
mod.alias,
|
|
53
|
+
request?.query?.[AUTH_QUERY],
|
|
54
|
+
request?.params ? JSON.stringify(request.params) : undefined,
|
|
55
|
+
dial
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!failed || !ctx.hasService(SOCKET_STATUS)) return
|
|
60
|
+
return (ctx as unknown as SocketStatusServiceAppend).socketStatus().onRetry(() => {
|
|
61
|
+
setFailed(false)
|
|
62
|
+
setDial(n => n + 1)
|
|
63
|
+
})
|
|
64
|
+
}, [failed])
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (connection != null) {
|
|
68
|
+
return () => {
|
|
69
|
+
void connection.close()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [connection])
|
|
73
|
+
|
|
74
|
+
return connection
|
|
75
|
+
}
|
package/src/index.ts
CHANGED
package/src/status.ts
CHANGED
|
@@ -4,10 +4,8 @@ import { useCallback, useEffect, useState } from 'react'
|
|
|
4
4
|
import type {
|
|
5
5
|
Config, Context, SocketConnectionState, SocketStatusService, SocketStatusServiceAppend
|
|
6
6
|
} from './types.js'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const RANK: Record<SocketConnectionState, number> = { online: 0, reconnecting: 1, lost: 2 }
|
|
7
|
+
import { RANK } from './consts.local.js'
|
|
8
|
+
import { SOCKET_STATUS } from './consts.js'
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Aggregates the coarse health of every `ws()`/`useWs()` connection an app has open into one
|
package/src/utils/connection.ts
CHANGED
|
@@ -1,51 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
createBasicConnection, MessageType, SocketConnectionError, SocketSystemEvent,
|
|
4
|
-
SOCKET_HEARTBEAT_TIMEOUT_CODE
|
|
5
|
-
} from '@owlmeans/socket'
|
|
1
|
+
import { type EventMessage as EMessage, createBasicConnection, MessageType, SocketConnectionError, SocketSystemEvent, SOCKET_HEARTBEAT_TIMEOUT_CODE } from '@owlmeans/socket'
|
|
6
2
|
import { AuthenticationStage } from '@owlmeans/auth'
|
|
7
|
-
import type { ReconnectPolicy, SocketConnectionState } from '../types.js'
|
|
8
3
|
import { logger } from '@owlmeans/log'
|
|
4
|
+
import { TERMINAL_CLOSE_CODES } from './consts.local.js'
|
|
5
|
+
import type { ManagedConnection, ManagedConnectionOptions } from './types.js'
|
|
9
6
|
|
|
10
7
|
const log = logger('client-socket')
|
|
11
8
|
|
|
12
|
-
/** Close codes that mean "this connection is finished on purpose" — never worth retrying: a
|
|
13
|
-
* normal closure the SERVER initiated (1000, e.g. the protocol's own one-shot completion) and a
|
|
14
|
-
* policy violation (1008, e.g. the guard rejected the frame that opened it). A client-initiated
|
|
15
|
-
* close is handled separately, through `closedByClient`. */
|
|
16
|
-
const TERMINAL_CLOSE_CODES: ReadonlySet<number> = new Set([1000, 1008])
|
|
17
|
-
|
|
18
|
-
export interface ConnectionOpener {
|
|
19
|
-
(): Promise<WebSocket>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ManagedConnectionOptions {
|
|
23
|
-
policy: ReconnectPolicy
|
|
24
|
-
/** Whether a drop schedules a retry at all. `false` keeps the pre-reconnect-support
|
|
25
|
-
* behaviour — one attempt, report and stop — while the heartbeat/liveness check below still
|
|
26
|
-
* runs exactly as it always did. */
|
|
27
|
-
retry: boolean
|
|
28
|
-
/** Opens a fresh socket for the first attempt and every retry — already reflecting whatever a
|
|
29
|
-
* caller's `beforeConnect` refreshed (a token, most often). Resolves once the socket is OPEN,
|
|
30
|
-
* rejects on a handshake failure. */
|
|
31
|
-
open: ConnectionOpener
|
|
32
|
-
/** Told this connection's coarse health, if a caller wants to aggregate it — the status
|
|
33
|
-
* service `@owlmeans/web-panel`'s reload dialog reads. */
|
|
34
|
-
onStatus?: (state: SocketConnectionState) => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ManagedConnection {
|
|
38
|
-
connection: Connection
|
|
39
|
-
/** Settles once the first socket opens; rejects with `SocketConnectionError('lost')` once the
|
|
40
|
-
* retry budget elapses first (or the single attempt fails, with `retry: false`). */
|
|
41
|
-
ready: Promise<void>
|
|
42
|
-
/** Restart retrying a connection that gave up after its budget: one attempt at once, then the
|
|
43
|
-
* usual backoff within `policy.reviveBudget`. On a connection still waiting out a backoff
|
|
44
|
-
* delay: attempt at once, and leave it at least `reviveBudget`. A no-op otherwise — a terminal
|
|
45
|
-
* close (client close, 1000/1008, `retry: false`) is never revived. */
|
|
46
|
-
revive: () => void
|
|
47
|
-
}
|
|
48
|
-
|
|
49
9
|
/**
|
|
50
10
|
* Build a `Connection` whose transport survives a dropped WebSocket.
|
|
51
11
|
*
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Close codes that mean "this connection is finished on purpose" — never worth retrying: a
|
|
2
|
+
* normal closure the SERVER initiated (1000, e.g. the protocol's own one-shot completion) and a
|
|
3
|
+
* policy violation (1008, e.g. the guard rejected the frame that opened it). A client-initiated
|
|
4
|
+
* close is handled separately, through `closedByClient`. */
|
|
5
|
+
export const TERMINAL_CLOSE_CODES: ReadonlySet<number> = new Set([1000, 1008])
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReconnectPolicy, SocketConnectionState } from '../types.js'
|
|
2
|
+
import type { Connection } from '@owlmeans/socket'
|
|
3
|
+
|
|
4
|
+
export interface ConnectionOpener {
|
|
5
|
+
(): Promise<WebSocket>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ManagedConnectionOptions {
|
|
9
|
+
policy: ReconnectPolicy
|
|
10
|
+
/** Whether a drop schedules a retry at all. `false` keeps the pre-reconnect-support
|
|
11
|
+
* behaviour — one attempt, report and stop — while the heartbeat/liveness check below still
|
|
12
|
+
* runs exactly as it always did. */
|
|
13
|
+
retry: boolean
|
|
14
|
+
/** Opens a fresh socket for the first attempt and every retry — already reflecting whatever a
|
|
15
|
+
* caller's `beforeConnect` refreshed (a token, most often). Resolves once the socket is OPEN,
|
|
16
|
+
* rejects on a handshake failure. */
|
|
17
|
+
open: ConnectionOpener
|
|
18
|
+
/** Told this connection's coarse health, if a caller wants to aggregate it — the status
|
|
19
|
+
* service `@owlmeans/web-panel`'s reload dialog reads. */
|
|
20
|
+
onStatus?: (state: SocketConnectionState) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ManagedConnection {
|
|
24
|
+
connection: Connection
|
|
25
|
+
/** Settles once the first socket opens; rejects with `SocketConnectionError('lost')` once the
|
|
26
|
+
* retry budget elapses first (or the single attempt fails, with `retry: false`). */
|
|
27
|
+
ready: Promise<void>
|
|
28
|
+
/** Restart retrying a connection that gave up after its budget: one attempt at once, then the
|
|
29
|
+
* usual backoff within `policy.reviveBudget`. On a connection still waiting out a backoff
|
|
30
|
+
* delay: attempt at once, and leave it at least `reviveBudget`. A no-op otherwise — a terminal
|
|
31
|
+
* close (client close, 1000/1008, `retry: false`) is never revived. */
|
|
32
|
+
revive: () => void
|
|
33
|
+
}
|
package/tests/reconnect.spec.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import type { Server } from 'bun'
|
|
3
|
-
import {
|
|
3
|
+
import { socketClientHelper } from '../src/helper.js'
|
|
4
4
|
import { makeConnection } from '../src/utils/connection.js'
|
|
5
5
|
import { appendSocketStatus } from '../src/status.js'
|
|
6
6
|
import { SocketSystemEvent } from '@owlmeans/socket'
|
|
7
|
-
import type { EventMessage } from '@owlmeans/socket'
|
|
7
|
+
import type { Connection, EventMessage } from '@owlmeans/socket'
|
|
8
8
|
import { makeTestContext } from './context.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -64,7 +64,7 @@ const waitFor = async (check: () => boolean, timeout: number, step = 20): Promis
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const systemEvents = (connection:
|
|
67
|
+
const systemEvents = (connection: Connection): string[] => {
|
|
68
68
|
const seen: string[] = []
|
|
69
69
|
connection.listen(async message => {
|
|
70
70
|
if (typeof message === 'object' && (message as EventMessage<unknown>).type === 'system') {
|
|
@@ -80,7 +80,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
80
80
|
const port = initial.port
|
|
81
81
|
|
|
82
82
|
const ctx = makeTestContext({ reconnect: { minDelay: 50, maxDelay: 150, stableAfter: 200, budget: 30_000 } })
|
|
83
|
-
const connection = await connect(() => `ws://localhost:${port}`, ctx)
|
|
83
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${port}`, ctx)
|
|
84
84
|
const events = systemEvents(connection)
|
|
85
85
|
|
|
86
86
|
// Register an observer BEFORE the drop — it must still be the one that fires after the
|
|
@@ -143,7 +143,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
143
143
|
const port = initial.port
|
|
144
144
|
|
|
145
145
|
const ctx = makeTestContext({ reconnect: { minDelay: 50, maxDelay: 150 } })
|
|
146
|
-
const connection = await connect(() => `ws://localhost:${port}`, ctx)
|
|
146
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${port}`, ctx)
|
|
147
147
|
const events = systemEvents(connection)
|
|
148
148
|
|
|
149
149
|
await connection.close()
|
|
@@ -160,7 +160,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
160
160
|
const ctx = makeTestContext({
|
|
161
161
|
reconnect: { heartbeat: 60, pongTimeout: 30, minDelay: 20, maxDelay: 40, budget: 10_000 }
|
|
162
162
|
})
|
|
163
|
-
const connection = await connect(() => `ws://localhost:${silent.port}`, ctx)
|
|
163
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${silent.port}`, ctx)
|
|
164
164
|
const events = systemEvents(connection)
|
|
165
165
|
|
|
166
166
|
await waitFor(() => events.includes(SocketSystemEvent.Disconnected), 3_000)
|
|
@@ -176,7 +176,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
176
176
|
const ctx = makeTestContext({
|
|
177
177
|
reconnect: { heartbeat: 100, pongTimeout: 50, minDelay: 20, maxDelay: 40, budget: 10_000 }
|
|
178
178
|
})
|
|
179
|
-
const connection = await connect(() => `ws://localhost:${port}`, ctx)
|
|
179
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${port}`, ctx)
|
|
180
180
|
const events = systemEvents(connection)
|
|
181
181
|
let observed = 0
|
|
182
182
|
connection.observe('probe', async () => { observed += 1 })
|
|
@@ -213,7 +213,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
213
213
|
ctx.configure()
|
|
214
214
|
await ctx.init()
|
|
215
215
|
|
|
216
|
-
await expect(connect(() => 'ws://127.0.0.1:1', ctx)).rejects.toBeTruthy()
|
|
216
|
+
await expect(socketClientHelper.connect(() => 'ws://127.0.0.1:1', ctx)).rejects.toBeTruthy()
|
|
217
217
|
|
|
218
218
|
expect(ctx.socketStatus().state()).toBe('lost')
|
|
219
219
|
}, 5_000)
|
|
@@ -229,7 +229,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
229
229
|
ctx.configure()
|
|
230
230
|
await ctx.init()
|
|
231
231
|
|
|
232
|
-
const connection = await connect(() => `ws://localhost:${port}`, ctx)
|
|
232
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${port}`, ctx)
|
|
233
233
|
const events = systemEvents(connection)
|
|
234
234
|
let observed = 0
|
|
235
235
|
connection.observe('probe', async () => { observed += 1 })
|
|
@@ -271,7 +271,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
271
271
|
ctx.configure()
|
|
272
272
|
await ctx.init()
|
|
273
273
|
|
|
274
|
-
const connection = await connect(() => `ws://localhost:${port}`, ctx)
|
|
274
|
+
const connection = await socketClientHelper.connect(() => `ws://localhost:${port}`, ctx)
|
|
275
275
|
const events = systemEvents(connection)
|
|
276
276
|
|
|
277
277
|
stopServer()
|
|
@@ -358,7 +358,7 @@ describe('@owlmeans/client-socket — reconnect', () => {
|
|
|
358
358
|
ctx.configure()
|
|
359
359
|
await ctx.init()
|
|
360
360
|
|
|
361
|
-
await expect(connect(() => 'ws://127.0.0.1:1', ctx)).rejects.toBeTruthy()
|
|
361
|
+
await expect(socketClientHelper.connect(() => 'ws://127.0.0.1:1', ctx)).rejects.toBeTruthy()
|
|
362
362
|
expect(ctx.socketStatus().state()).toBe('lost')
|
|
363
363
|
|
|
364
364
|
let retried = 0
|