@flamingo-stack/openframe-frontend-core 0.0.210 → 0.0.212-snapshot.20260528112413
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/dist/{chunk-VBFOCTMD.cjs → chunk-35XIT2CF.cjs} +17 -17
- package/dist/{chunk-VBFOCTMD.cjs.map → chunk-35XIT2CF.cjs.map} +1 -1
- package/dist/{chunk-ATEUJQKU.js → chunk-3JWIJJ44.js} +2 -2
- package/dist/chunk-CZR7ARBA.js +698 -0
- package/dist/chunk-CZR7ARBA.js.map +1 -0
- package/dist/{chunk-UYQOPC57.js → chunk-HICZPTRR.js} +4 -351
- package/dist/chunk-HICZPTRR.js.map +1 -0
- package/dist/{chunk-WJBPLMBX.js → chunk-IK2X5YJU.js} +3 -3
- package/dist/{chunk-MDTIOPVS.cjs → chunk-OTKJASSX.cjs} +26 -26
- package/dist/{chunk-MDTIOPVS.cjs.map → chunk-OTKJASSX.cjs.map} +1 -1
- package/dist/chunk-OZ3GH6OQ.cjs +698 -0
- package/dist/chunk-OZ3GH6OQ.cjs.map +1 -0
- package/dist/{chunk-6RZYJICV.cjs → chunk-P5EE2VJX.cjs} +1 -1
- package/dist/chunk-P5EE2VJX.cjs.map +1 -0
- package/dist/{chunk-EH3RWVF3.cjs → chunk-WT5JV2GS.cjs} +8 -355
- package/dist/chunk-WT5JV2GS.cjs.map +1 -0
- package/dist/{chunk-TWKPYZNQ.cjs → chunk-ZDF6F7ED.cjs} +569 -694
- package/dist/chunk-ZDF6F7ED.cjs.map +1 -0
- package/dist/{chunk-7L4DWM7P.js → chunk-ZG2YY5E7.js} +1 -1
- package/dist/chunk-ZG2YY5E7.js.map +1 -0
- package/dist/{chunk-R5RNRH62.js → chunk-ZTJVRSN5.js} +422 -547
- package/dist/chunk-ZTJVRSN5.js.map +1 -0
- package/dist/components/chat/hooks/use-chat-identity.d.ts +3 -3
- package/dist/components/chat/hooks/use-chat-identity.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts +0 -9
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -5
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +5 -4
- package/dist/components/contact/index.cjs +7 -6
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +6 -5
- package/dist/components/features/index.cjs +6 -5
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -4
- package/dist/components/features/notifications/index.d.ts +2 -2
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/notifications-context.d.ts +16 -1
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +4 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/footer-waitlist-button.d.ts +21 -2
- package/dist/components/footer-waitlist-button.d.ts.map +1 -1
- package/dist/components/index.cjs +93 -96
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +17 -20
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +6 -5
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +5 -4
- package/dist/components/navigation/sticky-section-nav.d.ts.map +1 -1
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +144 -102
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +98 -56
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tickets/ticket-row.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -5
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -4
- package/dist/contexts/chat-runtime-context.d.ts +6 -3
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +3 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/index.cjs +8 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -4
- package/dist/nats/index.cjs +28 -346
- package/dist/nats/index.cjs.map +1 -1
- package/dist/nats/index.d.ts +3 -0
- package/dist/nats/index.d.ts.map +1 -1
- package/dist/nats/index.js +30 -346
- package/dist/nats/index.js.map +1 -1
- package/dist/nats/nats-provider.d.ts +28 -0
- package/dist/nats/nats-provider.d.ts.map +1 -0
- package/dist/nats/nats.d.ts +1 -0
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/nats/shared-connection.d.ts +73 -0
- package/dist/nats/shared-connection.d.ts.map +1 -0
- package/dist/nats/use-nats-subscription.d.ts +18 -0
- package/dist/nats/use-nats-subscription.d.ts.map +1 -0
- package/dist/utils/index.cjs +10 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +10 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scroll-into-view.d.ts +63 -0
- package/dist/utils/scroll-into-view.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/chat/hooks/use-chat-identity.ts +8 -7
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +60 -207
- package/src/components/chat/hooks/use-nats-dialog-subscription.ts +71 -214
- package/src/components/features/notifications/index.ts +2 -1
- package/src/components/features/notifications/notifications-context.tsx +104 -6
- package/src/components/features/notifications/types.ts +5 -0
- package/src/components/footer-waitlist-button.tsx +33 -16
- package/src/components/navigation/app-header.tsx +7 -9
- package/src/components/navigation/sticky-section-nav.tsx +6 -4
- package/src/components/tickets/help-center-card.tsx +55 -1
- package/src/components/tickets/help-center-list.tsx +9 -1
- package/src/components/tickets/ticket-detail-drawer.tsx +19 -4
- package/src/components/tickets/ticket-row.tsx +30 -19
- package/src/contexts/chat-runtime-context.tsx +6 -3
- package/src/nats/index.ts +3 -0
- package/src/nats/nats-provider.tsx +146 -0
- package/src/nats/nats.ts +2 -0
- package/src/nats/shared-connection.ts +285 -0
- package/src/nats/use-nats-subscription.ts +99 -0
- package/src/stories/EmbeddableChat.stories.tsx +1 -1
- package/src/utils/index.ts +12 -0
- package/src/utils/scroll-into-view.ts +74 -0
- package/dist/chunk-6RZYJICV.cjs.map +0 -1
- package/dist/chunk-7L4DWM7P.js.map +0 -1
- package/dist/chunk-EH3RWVF3.cjs.map +0 -1
- package/dist/chunk-R5RNRH62.js.map +0 -1
- package/dist/chunk-TWKPYZNQ.cjs.map +0 -1
- package/dist/chunk-UYQOPC57.js.map +0 -1
- /package/dist/{chunk-ATEUJQKU.js.map → chunk-3JWIJJ44.js.map} +0 -0
- /package/dist/{chunk-WJBPLMBX.js.map → chunk-IK2X5YJU.js.map} +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scrollElementIntoView` — canonical "smooth scroll element to top
|
|
3
|
+
* of viewport, account for sticky chrome, optionally adjust for
|
|
4
|
+
* known layout shifts" helper.
|
|
5
|
+
*
|
|
6
|
+
* Before this util existed, ~3 different call sites in the lib + hub
|
|
7
|
+
* had the same 5-line snippet copy-pasted with subtle differences:
|
|
8
|
+
*
|
|
9
|
+
* - `useUnifiedNav` same-URL re-scroll branch (hub)
|
|
10
|
+
* - `<HelpCenterCard>` click-to-expand (lib, with cross-row
|
|
11
|
+
* layout-shift adjustment)
|
|
12
|
+
* - Future ticket-row / docs anchor scrolls
|
|
13
|
+
*
|
|
14
|
+
* The canonical pattern is `window.scrollTo({top, behavior:'smooth'})`
|
|
15
|
+
* with a pre-computed pixel target — NOT `element.scrollIntoView()`.
|
|
16
|
+
* Pre-computing the target lets the browser run a clean uninterrupted
|
|
17
|
+
* smooth animation to a fixed pixel value. `scrollIntoView` re-targets
|
|
18
|
+
* continuously as the page layout shifts during the animation, which
|
|
19
|
+
* causes visible jitter when content above the target is also moving
|
|
20
|
+
* (a sibling collapsing, an async image loading, …).
|
|
21
|
+
*
|
|
22
|
+
* The `adjustTargetY` callback is the escape hatch for cases where
|
|
23
|
+
* the consumer KNOWS about an upcoming layout shift and can compute
|
|
24
|
+
* the correct FINAL target before the animation starts. Example: in
|
|
25
|
+
* `HelpCenterCard`, clicking row B while row A above is currently
|
|
26
|
+
* expanded — A's drawer collapses simultaneously with B's expansion,
|
|
27
|
+
* shifting B's tile up by A's drawer height. The consumer passes
|
|
28
|
+
* `adjustTargetY: raw => raw - getAboveDrawerHeight()` and the
|
|
29
|
+
* browser smooth-scrolls to the post-collapse position directly.
|
|
30
|
+
*/
|
|
31
|
+
export interface ScrollElementIntoViewOptions {
|
|
32
|
+
/** Pixels to subtract from the target element's `top` so it lands
|
|
33
|
+
* BELOW sticky chrome. Defaults to 0. Pass `96` (matches
|
|
34
|
+
* `scroll-mt-24`) for the standard hub header offset. */
|
|
35
|
+
headerOffset?: number;
|
|
36
|
+
/** Scroll animation style. Defaults to `'smooth'`. Use `'instant'`
|
|
37
|
+
* for imperative jumps where animation would feel laggy (deep
|
|
38
|
+
* link land, programmatic focus moves). */
|
|
39
|
+
behavior?: ScrollBehavior;
|
|
40
|
+
/** Optional adjustment applied to the computed pixel target. The
|
|
41
|
+
* callback receives the "raw" Y (`element.top + scrollY -
|
|
42
|
+
* headerOffset`) and returns the FINAL pixel target. Use this
|
|
43
|
+
* when the caller knows about a layout shift that will happen
|
|
44
|
+
* between the call and the animation completing — the browser's
|
|
45
|
+
* smooth-scroll commits to a single pixel value, so providing the
|
|
46
|
+
* post-shift target up front lands the element correctly even as
|
|
47
|
+
* content above it moves. */
|
|
48
|
+
adjustTargetY?: (rawTargetY: number) => number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Scroll the page so `target` lands at the top of the viewport,
|
|
52
|
+
* accounting for sticky chrome via `headerOffset`. Returns void; the
|
|
53
|
+
* scroll runs async via the browser's smooth-scroll engine.
|
|
54
|
+
*
|
|
55
|
+
* Accepts:
|
|
56
|
+
* - `HTMLElement` — direct reference (most common from `useRef`).
|
|
57
|
+
* - `null` / `undefined` — no-op so callers can pass refs without
|
|
58
|
+
* defensive branching.
|
|
59
|
+
*
|
|
60
|
+
* SSR-safe: short-circuits when `window` is undefined.
|
|
61
|
+
*/
|
|
62
|
+
export declare function scrollElementIntoView(target: HTMLElement | null | undefined, options?: ScrollElementIntoViewOptions): void;
|
|
63
|
+
//# sourceMappingURL=scroll-into-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-into-view.d.ts","sourceRoot":"","sources":["../../src/utils/scroll-into-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,MAAM,WAAW,4BAA4B;IAC3C;;8DAE0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;gDAE4C;IAC5C,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB;;;;;;;kCAO8B;IAC9B,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACtC,OAAO,GAAE,4BAAiC,GACzC,IAAI,CAON"}
|
package/package.json
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* (attachment button, drawer composer, /tickets gate) on chat-side
|
|
9
9
|
* identity tiers WITHOUT sending a chat message first.
|
|
10
10
|
*
|
|
11
|
-
* Server-side parity: the
|
|
12
|
-
*
|
|
11
|
+
* Server-side parity: the host's identity endpoint (hub default
|
|
12
|
+
* `/api/auth/identity`, override via `runtime.endpoints.identityUrl`)
|
|
13
|
+
* runs the same 3-tier `requireChatAuth` chain the chat itself uses.
|
|
13
14
|
* `attachmentsEnabled` is computed server-side as
|
|
14
15
|
* `authTier !== 'anon' AND isSelfScopedSource(source)` — single
|
|
15
16
|
* source of truth, consumers don't combine the fields themselves.
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* 3. The fetch is cheap and short — no perf justification for a
|
|
28
29
|
* cache layer
|
|
29
30
|
*
|
|
30
|
-
* Endpoint URL: read from `useRequiredChatRuntime().endpoints.
|
|
31
|
+
* Endpoint URL: read from `useRequiredChatRuntime().endpoints.identityUrl`
|
|
31
32
|
* so embedded apps with their own reverse-proxy topology can override.
|
|
32
33
|
*/
|
|
33
34
|
|
|
@@ -37,9 +38,9 @@ import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
|
37
38
|
import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Wire-shape for the
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Wire-shape for the identity route response. Mirrors the hub's
|
|
42
|
+
* `ChatIdentityResponse` (in `app/api/auth/identity/route.ts`) —
|
|
43
|
+
* kept in sync there. Lib-side declaration so the chat panel can
|
|
43
44
|
* compile without depending on hub-internal types.
|
|
44
45
|
*/
|
|
45
46
|
export interface ChatIdentityResponse {
|
|
@@ -87,7 +88,7 @@ const ANON_DEFAULTS: ChatIdentityResponse = {
|
|
|
87
88
|
|
|
88
89
|
export function useChatIdentity(): ChatIdentitySurface {
|
|
89
90
|
const runtime = useRequiredChatRuntime()
|
|
90
|
-
const url = runtime.endpoints.
|
|
91
|
+
const url = runtime.endpoints.identityUrl
|
|
91
92
|
// `getChatProxyAuth()` reads localStorage every render. If the user
|
|
92
93
|
// pastes bearer creds mid-session (via the `/debug` creds bar),
|
|
93
94
|
// their email arrives here and the effect's dep changes → refetch.
|
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
acquireClient as acquireSharedClient,
|
|
6
|
+
releaseClient as releaseSharedClient,
|
|
7
|
+
startConnectionLifecycle,
|
|
6
8
|
type JetStreamSubscriptionHandle,
|
|
7
9
|
type NatsClient,
|
|
10
|
+
type SharedConnection,
|
|
8
11
|
} from '../../../nats'
|
|
9
12
|
import {
|
|
10
13
|
type UseJetStreamDialogSubscriptionOptions,
|
|
11
14
|
type UseJetStreamDialogSubscriptionReturn,
|
|
12
|
-
NETWORK_CONFIG,
|
|
13
15
|
} from '../types'
|
|
14
16
|
|
|
15
|
-
type SharedConnection = {
|
|
16
|
-
wsUrl: string
|
|
17
|
-
client: NatsClient
|
|
18
|
-
connectPromise: Promise<void> | null
|
|
19
|
-
refCount: number
|
|
20
|
-
closeTimer: ReturnType<typeof setTimeout> | null
|
|
21
|
-
retryTimer: ReturnType<typeof setTimeout> | null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let shared: SharedConnection | null = null
|
|
25
|
-
|
|
26
17
|
const DEFAULT_INACTIVE_THRESHOLD_MS = 5 * 60_000
|
|
27
18
|
const DEFAULT_STREAM_NAME = 'CHAT_CHUNKS'
|
|
28
19
|
|
|
@@ -110,75 +101,23 @@ export function useJetStreamDialogSubscription({
|
|
|
110
101
|
|
|
111
102
|
const hadConnectionBeforeRef = useRef(false)
|
|
112
103
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (shared.closeTimer) clearTimeout(shared.closeTimer)
|
|
118
|
-
const old = shared
|
|
119
|
-
shared = null
|
|
120
|
-
void old.client.close().catch(() => {})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const { name = 'openframe-frontend-jetstream', user = 'machine', pass = '' } = clientConfig
|
|
124
|
-
|
|
125
|
-
const client = createNatsClient({
|
|
126
|
-
servers: url,
|
|
127
|
-
name,
|
|
128
|
-
user,
|
|
129
|
-
pass,
|
|
130
|
-
connectTimeoutMs: NETWORK_CONFIG.CONNECT_TIMEOUT_MS,
|
|
131
|
-
reconnect: false,
|
|
132
|
-
pingIntervalMs: NETWORK_CONFIG.PING_INTERVAL_MS,
|
|
133
|
-
maxPingOut: NETWORK_CONFIG.MAX_PING_OUT,
|
|
134
|
-
})
|
|
135
|
-
shared = {
|
|
136
|
-
wsUrl: url,
|
|
137
|
-
client,
|
|
138
|
-
connectPromise: null,
|
|
139
|
-
refCount: 0,
|
|
140
|
-
closeTimer: null,
|
|
141
|
-
retryTimer: null,
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
shared.refCount += 1
|
|
146
|
-
if (shared.closeTimer) {
|
|
147
|
-
clearTimeout(shared.closeTimer)
|
|
148
|
-
shared.closeTimer = null
|
|
149
|
-
}
|
|
150
|
-
return shared
|
|
151
|
-
},
|
|
152
|
-
[clientConfig],
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
const releaseClient = useCallback((url: string) => {
|
|
156
|
-
if (!shared || shared.wsUrl !== url) return
|
|
157
|
-
|
|
158
|
-
shared.refCount = Math.max(0, shared.refCount - 1)
|
|
159
|
-
if (shared.refCount > 0) return
|
|
160
|
-
|
|
161
|
-
shared.closeTimer = setTimeout(() => {
|
|
162
|
-
const s = shared
|
|
163
|
-
shared = null
|
|
164
|
-
if (s) {
|
|
165
|
-
if (s.retryTimer) {
|
|
166
|
-
clearTimeout(s.retryTimer)
|
|
167
|
-
s.retryTimer = null
|
|
168
|
-
}
|
|
169
|
-
void s.client.close().catch(() => {})
|
|
170
|
-
}
|
|
171
|
-
}, NETWORK_CONFIG.SHARED_CLOSE_DELAY_MS)
|
|
172
|
-
}, [])
|
|
104
|
+
const clientConfigRef = useRef(clientConfig)
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
clientConfigRef.current = clientConfig
|
|
107
|
+
}, [clientConfig])
|
|
173
108
|
|
|
174
109
|
const currentWsUrlRef = useRef<string>('')
|
|
175
110
|
|
|
176
|
-
//
|
|
111
|
+
// Resolve the URL synchronously each render so the effect depends on the URL string
|
|
112
|
+
// itself, not the (often inline-allocated) getNatsWsUrl callback identity. Otherwise
|
|
113
|
+
// every silent token rotation that rebuilds getNatsWsUrl in the caller would tear
|
|
114
|
+
// the WS down and reacquire even though the resolved URL hasn't changed.
|
|
115
|
+
const wsUrl = getNatsWsUrl()
|
|
116
|
+
|
|
177
117
|
useEffect(() => {
|
|
178
|
-
const wsUrl = getNatsWsUrl()
|
|
179
118
|
if (!enabled || !wsUrl) {
|
|
180
119
|
if (currentWsUrlRef.current && clientRef.current) {
|
|
181
|
-
|
|
120
|
+
releaseSharedClient(currentWsUrlRef.current)
|
|
182
121
|
clientRef.current = null
|
|
183
122
|
currentWsUrlRef.current = ''
|
|
184
123
|
setIsConnected(false)
|
|
@@ -199,166 +138,80 @@ export function useJetStreamDialogSubscription({
|
|
|
199
138
|
currentWsUrlRef.current !== wsUrl &&
|
|
200
139
|
clientRef.current
|
|
201
140
|
) {
|
|
202
|
-
|
|
141
|
+
releaseSharedClient(currentWsUrlRef.current)
|
|
203
142
|
clientRef.current = null
|
|
204
143
|
setIsConnected(false)
|
|
205
144
|
}
|
|
206
145
|
|
|
207
146
|
currentWsUrlRef.current = wsUrl
|
|
208
|
-
const
|
|
147
|
+
const cfg = clientConfigRef.current
|
|
148
|
+
const sharedConn = acquireSharedClient(wsUrl, {
|
|
149
|
+
name: cfg.name ?? 'openframe-frontend-jetstream',
|
|
150
|
+
user: cfg.user ?? 'machine',
|
|
151
|
+
pass: cfg.pass ?? '',
|
|
152
|
+
})
|
|
209
153
|
const client = sharedConn.client
|
|
210
154
|
|
|
211
155
|
clientRef.current = client
|
|
212
|
-
setIsConnected(
|
|
213
|
-
|
|
214
|
-
let closed = false
|
|
215
|
-
let retryAttempt = 0
|
|
216
|
-
|
|
217
|
-
function scheduleRetry() {
|
|
218
|
-
if (closed) return
|
|
219
|
-
if (shared !== sharedConn) return
|
|
220
|
-
|
|
221
|
-
if (sharedConn.retryTimer) {
|
|
222
|
-
clearTimeout(sharedConn.retryTimer)
|
|
223
|
-
sharedConn.retryTimer = null
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const cfg = reconnectionBackoffRef.current ?? {}
|
|
227
|
-
const fastRetries = cfg.fastRetries ?? 0
|
|
228
|
-
const fastDelay = cfg.fastRetryDelayMs ?? NETWORK_CONFIG.RETRY_INITIAL_DELAY_MS
|
|
229
|
-
const baseDelay = cfg.initialDelayMs ?? NETWORK_CONFIG.RETRY_INITIAL_DELAY_MS
|
|
230
|
-
const maxDelay = cfg.maxDelayMs ?? NETWORK_CONFIG.RETRY_MAX_DELAY_MS
|
|
231
|
-
const multiplier = cfg.multiplier ?? NETWORK_CONFIG.RETRY_BACKOFF_MULTIPLIER
|
|
232
|
-
|
|
233
|
-
const delay =
|
|
234
|
-
retryAttempt < fastRetries
|
|
235
|
-
? fastDelay
|
|
236
|
-
: Math.min(baseDelay * multiplier ** (retryAttempt - fastRetries), maxDelay)
|
|
237
|
-
const jitteredDelay = delay * (0.5 + Math.random() * 0.5)
|
|
238
|
-
retryAttempt++
|
|
239
|
-
|
|
240
|
-
sharedConn.retryTimer = setTimeout(async () => {
|
|
241
|
-
sharedConn.retryTimer = null
|
|
242
|
-
if (closed) return
|
|
243
|
-
if (shared !== sharedConn) return
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
await onBeforeReconnectRef.current?.()
|
|
247
|
-
} catch {
|
|
248
|
-
// Token refresh failed; still try to reconnect.
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (closed) return
|
|
252
|
-
if (shared !== sharedConn) return
|
|
253
|
-
|
|
254
|
-
const freshUrl = getNatsWsUrlRef.current()
|
|
255
|
-
if (freshUrl !== wsUrl) return
|
|
156
|
+
setIsConnected(client.isConnected())
|
|
256
157
|
|
|
158
|
+
const tearDownSubscription = () => {
|
|
159
|
+
if (subscriptionRef.current) {
|
|
257
160
|
try {
|
|
258
|
-
|
|
259
|
-
sharedConn.connectPromise = client.connect()
|
|
260
|
-
await sharedConn.connectPromise
|
|
261
|
-
if (!closed && shared === sharedConn) {
|
|
262
|
-
retryAttempt = 0
|
|
263
|
-
setIsConnected(true)
|
|
264
|
-
}
|
|
161
|
+
subscriptionRef.current.unsubscribe()
|
|
265
162
|
} catch {
|
|
266
|
-
|
|
267
|
-
if (!closed && shared === sharedConn) {
|
|
268
|
-
scheduleRetry()
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}, jitteredDelay)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const unsubscribeStatus = client.onStatus((event) => {
|
|
275
|
-
const connected = event.status === 'connected'
|
|
276
|
-
// `error` is a protocol-level signal (e.g. -ERR Permissions Violation when
|
|
277
|
-
// CONSUMER.CREATE is denied) that does NOT close the WebSocket. Treating
|
|
278
|
-
// it as a disconnect causes scheduleRetry() to fire on every -ERR, which
|
|
279
|
-
// re-runs onBeforeReconnect (auth refresh / `/api/me`) on a loop. Real
|
|
280
|
-
// transport loss arrives separately as `disconnected` or `closed`.
|
|
281
|
-
const disconnected = event.status === 'closed' || event.status === 'disconnected'
|
|
282
|
-
if (connected) {
|
|
283
|
-
setIsConnected(true)
|
|
284
|
-
if (hadConnectionBeforeRef.current) {
|
|
285
|
-
setReconnectionCount((c) => c + 1)
|
|
286
|
-
}
|
|
287
|
-
hadConnectionBeforeRef.current = true
|
|
288
|
-
retryAttempt = 0
|
|
289
|
-
onConnectRef.current?.()
|
|
290
|
-
}
|
|
291
|
-
if (event.status === 'error') {
|
|
292
|
-
// Subscription-level failures (e.g. consumer.get rejected by JetStream
|
|
293
|
-
// ACLs) already surface to the subscribe effect via the rejected
|
|
294
|
-
// promise; log here for diagnostics and let the existing WS connection
|
|
295
|
-
// keep running.
|
|
296
|
-
console.warn('[JetStream] NATS protocol error:', event.data)
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
if (disconnected) {
|
|
300
|
-
setIsConnected(false)
|
|
301
|
-
setIsSubscribed(false)
|
|
302
|
-
|
|
303
|
-
if (subscriptionRef.current) {
|
|
304
|
-
try {
|
|
305
|
-
subscriptionRef.current.unsubscribe()
|
|
306
|
-
} catch {
|
|
307
|
-
// ignore
|
|
308
|
-
}
|
|
309
|
-
subscriptionRef.current = null
|
|
163
|
+
// ignore
|
|
310
164
|
}
|
|
311
|
-
|
|
312
|
-
onDisconnectRef.current?.()
|
|
313
|
-
scheduleRetry()
|
|
165
|
+
subscriptionRef.current = null
|
|
314
166
|
}
|
|
315
|
-
}
|
|
167
|
+
}
|
|
316
168
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
169
|
+
const lifecycle = startConnectionLifecycle({
|
|
170
|
+
conn: sharedConn,
|
|
171
|
+
wsUrl,
|
|
172
|
+
onBeforeReconnect: () => onBeforeReconnectRef.current?.(),
|
|
173
|
+
backoff: reconnectionBackoffRef.current,
|
|
174
|
+
getFreshUrl: () => getNatsWsUrlRef.current(),
|
|
175
|
+
// JetStream emits 'error' for protocol-level failures (e.g. -ERR Permissions
|
|
176
|
+
// Violation when CONSUMER.CREATE is denied) without closing the WebSocket.
|
|
177
|
+
// Retrying on 'error' would loop onBeforeReconnect on every -ERR; let the
|
|
178
|
+
// subscribe effect surface those via its own rejected promise instead.
|
|
179
|
+
shouldRetryOn: (status) => status === 'closed' || status === 'disconnected',
|
|
180
|
+
onStatusChange: (status, evt) => {
|
|
181
|
+
if (status === 'connected') {
|
|
322
182
|
setIsConnected(true)
|
|
183
|
+
if (hadConnectionBeforeRef.current) {
|
|
184
|
+
setReconnectionCount((c) => c + 1)
|
|
185
|
+
}
|
|
323
186
|
hadConnectionBeforeRef.current = true
|
|
187
|
+
onConnectRef.current?.()
|
|
324
188
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
189
|
+
if (status === 'error') {
|
|
190
|
+
console.warn('[JetStream] NATS protocol error:', evt.data)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (status === 'closed' || status === 'disconnected') {
|
|
328
194
|
setIsConnected(false)
|
|
195
|
+
setIsSubscribed(false)
|
|
196
|
+
tearDownSubscription()
|
|
329
197
|
onDisconnectRef.current?.()
|
|
330
|
-
scheduleRetry()
|
|
331
198
|
}
|
|
332
|
-
}
|
|
333
|
-
})
|
|
199
|
+
},
|
|
200
|
+
})
|
|
334
201
|
|
|
335
202
|
return () => {
|
|
336
|
-
|
|
203
|
+
lifecycle.stop()
|
|
337
204
|
setIsConnected(false)
|
|
338
205
|
setIsSubscribed(false)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (sharedConn.retryTimer) {
|
|
342
|
-
clearTimeout(sharedConn.retryTimer)
|
|
343
|
-
sharedConn.retryTimer = null
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (subscriptionRef.current) {
|
|
347
|
-
try {
|
|
348
|
-
subscriptionRef.current.unsubscribe()
|
|
349
|
-
} catch {
|
|
350
|
-
// ignore
|
|
351
|
-
}
|
|
352
|
-
subscriptionRef.current = null
|
|
353
|
-
}
|
|
206
|
+
tearDownSubscription()
|
|
354
207
|
|
|
355
208
|
if (clientRef.current && currentWsUrlRef.current) {
|
|
356
|
-
|
|
209
|
+
releaseSharedClient(currentWsUrlRef.current)
|
|
357
210
|
clientRef.current = null
|
|
358
211
|
currentWsUrlRef.current = ''
|
|
359
212
|
}
|
|
360
213
|
}
|
|
361
|
-
}, [enabled,
|
|
214
|
+
}, [enabled, wsUrl])
|
|
362
215
|
|
|
363
216
|
// Subscription lifecycle: (re)create the ephemeral JetStream consumer whenever
|
|
364
217
|
// we transition into a connected state for a dialog, and whenever the dialog
|