@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.
Files changed (58) hide show
  1. package/README.md +6 -6
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/client-socket/SKILL.md +1 -1
  4. package/build/consts.d.ts +1 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +1 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/consts.local.d.ts +3 -0
  9. package/build/consts.local.d.ts.map +1 -0
  10. package/build/consts.local.js +2 -0
  11. package/build/consts.local.js.map +1 -0
  12. package/build/helper/types.d.ts +22 -0
  13. package/build/helper/types.d.ts.map +1 -0
  14. package/build/helper/types.js +2 -0
  15. package/build/helper/types.js.map +1 -0
  16. package/build/helper.d.ts +7 -28
  17. package/build/helper.d.ts.map +1 -1
  18. package/build/helper.js +74 -141
  19. package/build/helper.js.map +1 -1
  20. package/build/hooks.d.ts +18 -0
  21. package/build/hooks.d.ts.map +1 -0
  22. package/build/hooks.js +66 -0
  23. package/build/hooks.js.map +1 -0
  24. package/build/index.d.ts +2 -0
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +1 -0
  27. package/build/index.js.map +1 -1
  28. package/build/status.d.ts +0 -1
  29. package/build/status.d.ts.map +1 -1
  30. package/build/status.js +2 -2
  31. package/build/status.js.map +1 -1
  32. package/build/utils/connection.d.ts +1 -30
  33. package/build/utils/connection.d.ts.map +1 -1
  34. package/build/utils/connection.js +1 -5
  35. package/build/utils/connection.js.map +1 -1
  36. package/build/utils/consts.local.d.ts +6 -0
  37. package/build/utils/consts.local.d.ts.map +1 -0
  38. package/build/utils/consts.local.js +6 -0
  39. package/build/utils/consts.local.js.map +1 -0
  40. package/build/utils/index.d.ts +1 -0
  41. package/build/utils/index.d.ts.map +1 -1
  42. package/build/utils/types.d.ts +31 -0
  43. package/build/utils/types.d.ts.map +1 -0
  44. package/build/utils/types.js +2 -0
  45. package/build/utils/types.js.map +1 -0
  46. package/package.json +13 -13
  47. package/src/consts.local.ts +3 -0
  48. package/src/consts.ts +2 -0
  49. package/src/helper/types.ts +20 -0
  50. package/src/helper.ts +90 -160
  51. package/src/hooks.ts +75 -0
  52. package/src/index.ts +2 -0
  53. package/src/status.ts +2 -4
  54. package/src/utils/connection.ts +3 -43
  55. package/src/utils/consts.local.ts +5 -0
  56. package/src/utils/index.ts +1 -0
  57. package/src/utils/types.ts +33 -0
  58. 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
@@ -1,5 +1,7 @@
1
1
 
2
2
  export type * from './types.js'
3
+ export type * from './helper/types.js'
3
4
  export * from './consts.js'
4
5
  export * from './helper.js'
6
+ export * from './hooks.js'
5
7
  export * from './status.js'
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
- export const SOCKET_STATUS = 'socket-status'
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
@@ -1,51 +1,11 @@
1
- import type { Connection, EventMessage as EMessage } from '@owlmeans/socket'
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])
@@ -1,2 +1,3 @@
1
1
 
2
2
  export * from './connection.js'
3
+ export type * from './types.js'
@@ -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
+ }
@@ -1,10 +1,10 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
2
  import type { Server } from 'bun'
3
- import { connect } from '../src/helper.js'
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: Awaited<ReturnType<typeof connect>>): string[] => {
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