@flamingo-stack/openframe-frontend-core 0.0.212 → 0.0.213
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-ZFBLC5GV.cjs → chunk-35XIT2CF.cjs} +17 -17
- package/dist/{chunk-ZFBLC5GV.cjs.map → chunk-35XIT2CF.cjs.map} +1 -1
- package/dist/{chunk-QKFBZLIR.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-5BNWGK6D.js → chunk-IK2X5YJU.js} +3 -3
- package/dist/{chunk-VTUIMMHO.cjs → chunk-OTKJASSX.cjs} +26 -26
- package/dist/{chunk-VTUIMMHO.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-EH3RWVF3.cjs → chunk-WT5JV2GS.cjs} +8 -355
- package/dist/chunk-WT5JV2GS.cjs.map +1 -0
- package/dist/{chunk-WI76ZUBE.cjs → chunk-ZDF6F7ED.cjs} +544 -678
- package/dist/chunk-ZDF6F7ED.cjs.map +1 -0
- package/dist/{chunk-3E5ANY55.js → chunk-ZTJVRSN5.js} +409 -543
- package/dist/{chunk-3E5ANY55.js.map → chunk-ZTJVRSN5.js.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 +5 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +4 -3
- package/dist/components/contact/index.cjs +6 -5
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +5 -4
- package/dist/components/features/index.cjs +5 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +4 -3
- 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/index.cjs +55 -54
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +7 -6
- 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 +5 -4
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +4 -3
- package/dist/components/tickets/index.cjs +66 -65
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +6 -5
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +5 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +4 -3
- 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 +5 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -3
- 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/package.json +1 -1
- 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/navigation/app-header.tsx +7 -9
- 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/dist/chunk-EH3RWVF3.cjs.map +0 -1
- package/dist/chunk-UYQOPC57.js.map +0 -1
- package/dist/chunk-WI76ZUBE.cjs.map +0 -1
- /package/dist/{chunk-QKFBZLIR.js.map → chunk-3JWIJJ44.js.map} +0 -0
- /package/dist/{chunk-5BNWGK6D.js.map → chunk-IK2X5YJU.js.map} +0 -0
|
@@ -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
|