@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.
Files changed (91) hide show
  1. package/dist/{chunk-ZFBLC5GV.cjs → chunk-35XIT2CF.cjs} +17 -17
  2. package/dist/{chunk-ZFBLC5GV.cjs.map → chunk-35XIT2CF.cjs.map} +1 -1
  3. package/dist/{chunk-QKFBZLIR.js → chunk-3JWIJJ44.js} +2 -2
  4. package/dist/chunk-CZR7ARBA.js +698 -0
  5. package/dist/chunk-CZR7ARBA.js.map +1 -0
  6. package/dist/{chunk-UYQOPC57.js → chunk-HICZPTRR.js} +4 -351
  7. package/dist/chunk-HICZPTRR.js.map +1 -0
  8. package/dist/{chunk-5BNWGK6D.js → chunk-IK2X5YJU.js} +3 -3
  9. package/dist/{chunk-VTUIMMHO.cjs → chunk-OTKJASSX.cjs} +26 -26
  10. package/dist/{chunk-VTUIMMHO.cjs.map → chunk-OTKJASSX.cjs.map} +1 -1
  11. package/dist/chunk-OZ3GH6OQ.cjs +698 -0
  12. package/dist/chunk-OZ3GH6OQ.cjs.map +1 -0
  13. package/dist/{chunk-EH3RWVF3.cjs → chunk-WT5JV2GS.cjs} +8 -355
  14. package/dist/chunk-WT5JV2GS.cjs.map +1 -0
  15. package/dist/{chunk-WI76ZUBE.cjs → chunk-ZDF6F7ED.cjs} +544 -678
  16. package/dist/chunk-ZDF6F7ED.cjs.map +1 -0
  17. package/dist/{chunk-3E5ANY55.js → chunk-ZTJVRSN5.js} +409 -543
  18. package/dist/{chunk-3E5ANY55.js.map → chunk-ZTJVRSN5.js.map} +1 -1
  19. package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
  20. package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts +0 -9
  21. package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
  22. package/dist/components/chat/index.cjs +5 -4
  23. package/dist/components/chat/index.cjs.map +1 -1
  24. package/dist/components/chat/index.js +4 -3
  25. package/dist/components/contact/index.cjs +6 -5
  26. package/dist/components/contact/index.cjs.map +1 -1
  27. package/dist/components/contact/index.js +5 -4
  28. package/dist/components/features/index.cjs +5 -4
  29. package/dist/components/features/index.cjs.map +1 -1
  30. package/dist/components/features/index.js +4 -3
  31. package/dist/components/features/notifications/index.d.ts +2 -2
  32. package/dist/components/features/notifications/index.d.ts.map +1 -1
  33. package/dist/components/features/notifications/notifications-context.d.ts +16 -1
  34. package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
  35. package/dist/components/features/notifications/types.d.ts +4 -0
  36. package/dist/components/features/notifications/types.d.ts.map +1 -1
  37. package/dist/components/index.cjs +55 -54
  38. package/dist/components/index.cjs.map +1 -1
  39. package/dist/components/index.js +7 -6
  40. package/dist/components/index.js.map +1 -1
  41. package/dist/components/navigation/app-header.d.ts.map +1 -1
  42. package/dist/components/navigation/index.cjs +5 -4
  43. package/dist/components/navigation/index.cjs.map +1 -1
  44. package/dist/components/navigation/index.js +4 -3
  45. package/dist/components/tickets/index.cjs +66 -65
  46. package/dist/components/tickets/index.cjs.map +1 -1
  47. package/dist/components/tickets/index.js +6 -5
  48. package/dist/components/tickets/index.js.map +1 -1
  49. package/dist/components/ui/index.cjs +5 -4
  50. package/dist/components/ui/index.cjs.map +1 -1
  51. package/dist/components/ui/index.js +4 -3
  52. package/dist/embed-shims/index.cjs +3 -3
  53. package/dist/embed-shims/index.cjs.map +1 -1
  54. package/dist/embed-shims/index.js +4 -4
  55. package/dist/hooks/index.cjs +3 -2
  56. package/dist/hooks/index.cjs.map +1 -1
  57. package/dist/hooks/index.js +2 -1
  58. package/dist/index.cjs +5 -4
  59. package/dist/index.cjs.map +1 -1
  60. package/dist/index.js +4 -3
  61. package/dist/nats/index.cjs +28 -346
  62. package/dist/nats/index.cjs.map +1 -1
  63. package/dist/nats/index.d.ts +3 -0
  64. package/dist/nats/index.d.ts.map +1 -1
  65. package/dist/nats/index.js +30 -346
  66. package/dist/nats/index.js.map +1 -1
  67. package/dist/nats/nats-provider.d.ts +28 -0
  68. package/dist/nats/nats-provider.d.ts.map +1 -0
  69. package/dist/nats/nats.d.ts +1 -0
  70. package/dist/nats/nats.d.ts.map +1 -1
  71. package/dist/nats/shared-connection.d.ts +73 -0
  72. package/dist/nats/shared-connection.d.ts.map +1 -0
  73. package/dist/nats/use-nats-subscription.d.ts +18 -0
  74. package/dist/nats/use-nats-subscription.d.ts.map +1 -0
  75. package/package.json +1 -1
  76. package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +60 -207
  77. package/src/components/chat/hooks/use-nats-dialog-subscription.ts +71 -214
  78. package/src/components/features/notifications/index.ts +2 -1
  79. package/src/components/features/notifications/notifications-context.tsx +104 -6
  80. package/src/components/features/notifications/types.ts +5 -0
  81. package/src/components/navigation/app-header.tsx +7 -9
  82. package/src/nats/index.ts +3 -0
  83. package/src/nats/nats-provider.tsx +146 -0
  84. package/src/nats/nats.ts +2 -0
  85. package/src/nats/shared-connection.ts +285 -0
  86. package/src/nats/use-nats-subscription.ts +99 -0
  87. package/dist/chunk-EH3RWVF3.cjs.map +0 -1
  88. package/dist/chunk-UYQOPC57.js.map +0 -1
  89. package/dist/chunk-WI76ZUBE.cjs.map +0 -1
  90. /package/dist/{chunk-QKFBZLIR.js.map → chunk-3JWIJJ44.js.map} +0 -0
  91. /package/dist/{chunk-5BNWGK6D.js.map → chunk-IK2X5YJU.js.map} +0 -0
@@ -1,28 +1,19 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from 'react'
3
+ import { useEffect, useRef, useState } from 'react'
4
4
  import {
5
- createNatsClient,
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 acquireClient = useCallback(
114
- (url: string): SharedConnection => {
115
- if (shared?.wsUrl !== url) {
116
- if (shared) {
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
- // Connection lifecycle: acquire/release the shared client based on enabled + URL.
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
- releaseClient(currentWsUrlRef.current)
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
- releaseClient(currentWsUrlRef.current)
141
+ releaseSharedClient(currentWsUrlRef.current)
203
142
  clientRef.current = null
204
143
  setIsConnected(false)
205
144
  }
206
145
 
207
146
  currentWsUrlRef.current = wsUrl
208
- const sharedConn = acquireClient(wsUrl)
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(false)
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
- sharedConn.connectPromise = null
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
- sharedConn.connectPromise = null
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
- ;(async () => {
318
- try {
319
- sharedConn.connectPromise ||= client.connect()
320
- await sharedConn.connectPromise
321
- if (!closed) {
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
- } catch {
326
- sharedConn.connectPromise = null
327
- if (!closed) {
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
- closed = true
203
+ lifecycle.stop()
337
204
  setIsConnected(false)
338
205
  setIsSubscribed(false)
339
- unsubscribeStatus()
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
- releaseClient(currentWsUrlRef.current)
209
+ releaseSharedClient(currentWsUrlRef.current)
357
210
  clientRef.current = null
358
211
  currentWsUrlRef.current = ''
359
212
  }
360
213
  }
361
- }, [enabled, getNatsWsUrl, acquireClient, releaseClient])
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