@flamingo-stack/openframe-frontend-core 0.0.554 → 0.0.555-snapshot.20260818112641
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-P6LUXP5E.js → chunk-5756Q6UN.js} +3 -3
- package/dist/{chunk-44QNFLHC.js → chunk-5EES5HD4.js} +6 -6
- package/dist/{chunk-VIDE4YOQ.js → chunk-5JWLZJK5.js} +2 -2
- package/dist/{chunk-RMGX6BVQ.js → chunk-5M4PS3XX.js} +3 -3
- package/dist/{chunk-4KIPZULI.cjs → chunk-7Q7GFIFB.cjs} +7 -7
- package/dist/{chunk-4KIPZULI.cjs.map → chunk-7Q7GFIFB.cjs.map} +1 -1
- package/dist/{chunk-JJKK7E66.cjs → chunk-BNMKHG2L.cjs} +4 -4
- package/dist/{chunk-JJKK7E66.cjs.map → chunk-BNMKHG2L.cjs.map} +1 -1
- package/dist/{chunk-ULH6JHWZ.cjs → chunk-BZKUID4R.cjs} +14 -14
- package/dist/{chunk-ULH6JHWZ.cjs.map → chunk-BZKUID4R.cjs.map} +1 -1
- package/dist/{chunk-CWLV6DEK.cjs → chunk-C74PZU6M.cjs} +32 -32
- package/dist/{chunk-CWLV6DEK.cjs.map → chunk-C74PZU6M.cjs.map} +1 -1
- package/dist/{chunk-67Q4QQBA.js → chunk-CGUSNV6R.js} +8 -8
- package/dist/{chunk-PAW27EWH.cjs → chunk-CRGIXNUE.cjs} +23 -23
- package/dist/{chunk-PAW27EWH.cjs.map → chunk-CRGIXNUE.cjs.map} +1 -1
- package/dist/{chunk-Q6RC3YGW.cjs → chunk-DFJ7TWH6.cjs} +3 -3
- package/dist/{chunk-Q6RC3YGW.cjs.map → chunk-DFJ7TWH6.cjs.map} +1 -1
- package/dist/{chunk-5VICQQM7.js → chunk-FLSEJSTN.js} +2 -2
- package/dist/{chunk-PDVSL7OW.js → chunk-FZT7BYNF.js} +2 -2
- package/dist/{chunk-J355HL3X.cjs → chunk-HOEOGFQM.cjs} +97 -97
- package/dist/{chunk-J355HL3X.cjs.map → chunk-HOEOGFQM.cjs.map} +1 -1
- package/dist/{chunk-HO6IG73U.cjs → chunk-HYABNMBN.cjs} +76 -76
- package/dist/{chunk-HO6IG73U.cjs.map → chunk-HYABNMBN.cjs.map} +1 -1
- package/dist/{chunk-QH3QWY3N.cjs → chunk-ICG7JRYT.cjs} +40 -40
- package/dist/{chunk-QH3QWY3N.cjs.map → chunk-ICG7JRYT.cjs.map} +1 -1
- package/dist/{chunk-CGCQ4HA5.cjs → chunk-JDUPAHJR.cjs} +18 -18
- package/dist/{chunk-CGCQ4HA5.cjs.map → chunk-JDUPAHJR.cjs.map} +1 -1
- package/dist/{chunk-QA7NU73D.js → chunk-JRWOHH7K.js} +2 -2
- package/dist/{chunk-FHS2SA5F.js → chunk-JXJ4FTKN.js} +3 -3
- package/dist/{chunk-AQFYI3HH.cjs → chunk-LZD3VSPS.cjs} +12 -12
- package/dist/{chunk-AQFYI3HH.cjs.map → chunk-LZD3VSPS.cjs.map} +1 -1
- package/dist/{chunk-WZWQWZFJ.cjs → chunk-Q5SZ2QTZ.cjs} +42 -42
- package/dist/{chunk-WZWQWZFJ.cjs.map → chunk-Q5SZ2QTZ.cjs.map} +1 -1
- package/dist/{chunk-GSONSIWG.js → chunk-R7A7WHXP.js} +60 -35
- package/dist/chunk-R7A7WHXP.js.map +1 -0
- package/dist/{chunk-2HWN6CYI.cjs → chunk-S4LL6QI6.cjs} +176 -168
- package/dist/chunk-S4LL6QI6.cjs.map +1 -0
- package/dist/{chunk-KQKU4VXB.cjs → chunk-STHVHAV4.cjs} +82 -57
- package/dist/chunk-STHVHAV4.cjs.map +1 -0
- package/dist/{chunk-PFQVCHDE.cjs → chunk-SXMJLNOF.cjs} +3 -3
- package/dist/{chunk-PFQVCHDE.cjs.map → chunk-SXMJLNOF.cjs.map} +1 -1
- package/dist/{chunk-SBXQW72U.js → chunk-VF4DCOPD.js} +5 -5
- package/dist/{chunk-QTO3WYB2.cjs → chunk-VJTD2DGF.cjs} +3 -3
- package/dist/{chunk-QTO3WYB2.cjs.map → chunk-VJTD2DGF.cjs.map} +1 -1
- package/dist/{chunk-VGLDA77S.js → chunk-VRX6Y6A7.js} +2 -2
- package/dist/{chunk-U72YYXBT.js → chunk-VXNMOGRP.js} +6 -6
- package/dist/{chunk-OR67WC32.js → chunk-WBQ3NOH7.js} +2 -2
- package/dist/{chunk-DXAUNMFM.js → chunk-WX5UCLL7.js} +2 -2
- package/dist/{chunk-ZGLMZM2I.js → chunk-XGBVHQP7.js} +32 -24
- package/dist/{chunk-ZGLMZM2I.js.map → chunk-XGBVHQP7.js.map} +1 -1
- package/dist/{chunk-FVXND3SO.js → chunk-YM2WHYYK.js} +4 -4
- package/dist/{chunk-XMB5P2OS.cjs → chunk-Z3R3LOC5.cjs} +5 -5
- package/dist/{chunk-XMB5P2OS.cjs.map → chunk-Z3R3LOC5.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +1 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +7 -5
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +25 -25
- package/dist/components/help-center-pages/index.js +16 -16
- package/dist/components/index.cjs +138 -138
- package/dist/components/index.js +14 -14
- package/dist/components/meeting-scheduler/index.cjs +35 -35
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +4 -4
- package/dist/index.js +3 -3
- package/dist/nats/index.cjs +2 -2
- package/dist/nats/index.js +1 -1
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/nats/shared-connection.d.ts +13 -6
- package/dist/nats/shared-connection.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/ticket-event.test.ts +52 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +15 -11
- package/src/components/chat/utils/message-segment-accumulator.ts +31 -17
- package/src/nats/__tests__/reconnect-lifecycle.test.tsx +814 -0
- package/src/nats/nats.ts +56 -13
- package/src/nats/shared-connection.ts +132 -36
- package/dist/chunk-2HWN6CYI.cjs.map +0 -1
- package/dist/chunk-GSONSIWG.js.map +0 -1
- package/dist/chunk-KQKU4VXB.cjs.map +0 -1
- /package/dist/{chunk-P6LUXP5E.js.map → chunk-5756Q6UN.js.map} +0 -0
- /package/dist/{chunk-44QNFLHC.js.map → chunk-5EES5HD4.js.map} +0 -0
- /package/dist/{chunk-VIDE4YOQ.js.map → chunk-5JWLZJK5.js.map} +0 -0
- /package/dist/{chunk-RMGX6BVQ.js.map → chunk-5M4PS3XX.js.map} +0 -0
- /package/dist/{chunk-67Q4QQBA.js.map → chunk-CGUSNV6R.js.map} +0 -0
- /package/dist/{chunk-5VICQQM7.js.map → chunk-FLSEJSTN.js.map} +0 -0
- /package/dist/{chunk-PDVSL7OW.js.map → chunk-FZT7BYNF.js.map} +0 -0
- /package/dist/{chunk-QA7NU73D.js.map → chunk-JRWOHH7K.js.map} +0 -0
- /package/dist/{chunk-FHS2SA5F.js.map → chunk-JXJ4FTKN.js.map} +0 -0
- /package/dist/{chunk-SBXQW72U.js.map → chunk-VF4DCOPD.js.map} +0 -0
- /package/dist/{chunk-VGLDA77S.js.map → chunk-VRX6Y6A7.js.map} +0 -0
- /package/dist/{chunk-U72YYXBT.js.map → chunk-VXNMOGRP.js.map} +0 -0
- /package/dist/{chunk-OR67WC32.js.map → chunk-WBQ3NOH7.js.map} +0 -0
- /package/dist/{chunk-DXAUNMFM.js.map → chunk-WX5UCLL7.js.map} +0 -0
- /package/dist/{chunk-FVXND3SO.js.map → chunk-YM2WHYYK.js.map} +0 -0
package/src/nats/nats.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type {
|
|
|
3
3
|
Consumer,
|
|
4
4
|
ConsumerEvents,
|
|
5
5
|
ConsumerMessages,
|
|
6
|
+
DebugEvents,
|
|
7
|
+
Events,
|
|
6
8
|
JsMsg,
|
|
7
9
|
MsgHdrs as NatsHeaders,
|
|
8
10
|
Msg,
|
|
@@ -278,14 +280,58 @@ function mapOptionsToConnectionOptions(
|
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
282
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Map a nats.ws status event onto our own status vocabulary.
|
|
285
|
+
*
|
|
286
|
+
* Exact enum matches, never substring tests. Substring matching reported
|
|
287
|
+
* `staleConnection` and `client initiated reconnect` as CONNECTED — both
|
|
288
|
+
* contain "connect" and neither contains "disconnect" — so the wrapper
|
|
289
|
+
* announced a live connection at the exact moment the client had given up on
|
|
290
|
+
* one. Downstream that is worse than silence: `NatsProvider` reads a
|
|
291
|
+
* `connected` that follows an earlier connection as a RECONNECT and bumps
|
|
292
|
+
* `reconnectionCount`, which every subscriber takes as "the tail came back" —
|
|
293
|
+
* resubscribing against a dying client and refetching persisted history.
|
|
294
|
+
* `staleConnection` is the one an idle mobile app hits routinely: no pong
|
|
295
|
+
* within `pingInterval * maxPingOut`, i.e. after a spell in the background.
|
|
296
|
+
*
|
|
297
|
+
* `StaleConnection` maps to `disconnected` rather than being ignored on the
|
|
298
|
+
* grounds that `Disconnect` follows it anyway — it does not always. nats.ws
|
|
299
|
+
* tears the transport down by draining `bufferedAmount` first, and a
|
|
300
|
+
* black-holed socket (no FIN, which is how a mobile link usually dies) never
|
|
301
|
+
* drains, so `Disconnect` can fail to arrive at all. Reporting it is then the
|
|
302
|
+
* only honest answer available: consumers stop trusting a tail the client has
|
|
303
|
+
* already given up on. It does NOT by itself recover that connection — the
|
|
304
|
+
* retry it arms short-circuits, because a protocol that never closed still
|
|
305
|
+
* reports `isConnected()`. Getting the socket back from that state needs a
|
|
306
|
+
* force-close path the client does not currently expose.
|
|
307
|
+
*
|
|
308
|
+
* The enums arrive as arguments rather than through a module-scope import
|
|
309
|
+
* because `nats.ws` is loaded lazily (see {@link importNats}) to keep this
|
|
310
|
+
* module safe to import from a Next server bundle. A hand-copied literal is
|
|
311
|
+
* what this is undoing, so it is not reintroduced here.
|
|
312
|
+
*/
|
|
313
|
+
function mapNatsTypeToStatus(
|
|
314
|
+
type: Events | DebugEvents,
|
|
315
|
+
events: typeof import('nats.ws').Events,
|
|
316
|
+
debugEvents: typeof import('nats.ws').DebugEvents,
|
|
317
|
+
): NatsStatus | null {
|
|
318
|
+
switch (type) {
|
|
319
|
+
case events.Disconnect:
|
|
320
|
+
return 'disconnected'
|
|
321
|
+
case events.Reconnect:
|
|
322
|
+
return 'connected'
|
|
323
|
+
case events.Error:
|
|
324
|
+
return 'error'
|
|
325
|
+
case debugEvents.Reconnecting:
|
|
326
|
+
case debugEvents.ClientInitiatedReconnect:
|
|
327
|
+
return 'reconnecting'
|
|
328
|
+
case debugEvents.StaleConnection:
|
|
329
|
+
return 'disconnected'
|
|
330
|
+
// Events.Update (cluster gossip), Events.LDM and DebugEvents.PingTimer say
|
|
331
|
+
// nothing about reachability.
|
|
332
|
+
default:
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
289
335
|
}
|
|
290
336
|
|
|
291
337
|
/**
|
|
@@ -372,15 +418,12 @@ export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
|
372
418
|
try {
|
|
373
419
|
for await (const s of conn.status()) {
|
|
374
420
|
if (signal.aborted) return
|
|
375
|
-
const mapped = mapNatsTypeToStatus(
|
|
421
|
+
const mapped = mapNatsTypeToStatus(s.type, nats.Events, nats.DebugEvents)
|
|
376
422
|
if (mapped) {
|
|
377
423
|
if (mapped === 'connected' && backoff) {
|
|
378
424
|
backoff.reset()
|
|
379
425
|
}
|
|
380
|
-
emitStatus({ status: mapped, data:
|
|
381
|
-
if (mapped === 'closed') {
|
|
382
|
-
nc = null
|
|
383
|
-
}
|
|
426
|
+
emitStatus({ status: mapped, data: s.data })
|
|
384
427
|
}
|
|
385
428
|
}
|
|
386
429
|
} catch (e) {
|
|
@@ -26,17 +26,24 @@ export interface NatsReconnectionBackoff {
|
|
|
26
26
|
export interface SharedConnection {
|
|
27
27
|
wsUrl: string
|
|
28
28
|
client: NatsClient
|
|
29
|
-
connectPromise: Promise<void> | null
|
|
30
29
|
refCount: number
|
|
31
30
|
closeTimer: ReturnType<typeof setTimeout> | null
|
|
32
31
|
retryTimer: ReturnType<typeof setTimeout> | null
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* The lifecycle driving reconnect, held as its own `scheduleRetry`. When set,
|
|
34
|
+
* other consumers observe status only and skip their own scheduleRetry —
|
|
35
|
+
* otherwise every disconnect starts one backoff schedule per attached
|
|
36
|
+
* consumer, all dialling the same connection on their own clocks.
|
|
38
37
|
*/
|
|
39
|
-
retryOwner:
|
|
38
|
+
retryOwner: (() => void) | null
|
|
39
|
+
/**
|
|
40
|
+
* The `scheduleRetry` of every lifecycle currently observing this connection,
|
|
41
|
+
* in attach order — the same value that goes into `retryOwner`, so a
|
|
42
|
+
* lifecycle has one identity rather than two. Only the owner drives
|
|
43
|
+
* reconnect, so when it gives the loop up the roster is what a successor is
|
|
44
|
+
* found in; see {@link handOffRetry}.
|
|
45
|
+
*/
|
|
46
|
+
retrySchedulers: Set<() => void>
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export interface AcquireClientOptions {
|
|
@@ -99,11 +106,11 @@ export function acquireClient(url: string, opts?: AcquireClientOptions): SharedC
|
|
|
99
106
|
conn = {
|
|
100
107
|
wsUrl: url,
|
|
101
108
|
client,
|
|
102
|
-
connectPromise: null,
|
|
103
109
|
refCount: 0,
|
|
104
110
|
closeTimer: null,
|
|
105
111
|
retryTimer: null,
|
|
106
112
|
retryOwner: null,
|
|
113
|
+
retrySchedulers: new Set(),
|
|
107
114
|
}
|
|
108
115
|
connections.set(url, conn)
|
|
109
116
|
}
|
|
@@ -146,11 +153,13 @@ export function getSharedConnectionFor(url: string | null | undefined): SharedCo
|
|
|
146
153
|
|
|
147
154
|
// ---------------------------------------------------------------------------
|
|
148
155
|
// Connection lifecycle: retry + status loop, shared by NatsProvider and the
|
|
149
|
-
// chat hooks.
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
156
|
+
// chat hooks. Every consumer that wants to observe or drive reconnect calls
|
|
157
|
+
// startConnectionLifecycle(). The first to claim retryOwner runs the actual
|
|
158
|
+
// retry loop; later attachers observe status only. When the owner gives the
|
|
159
|
+
// loop up it is handed to a survivor if the connection still needs one; while
|
|
160
|
+
// the connection is healthy no handoff is needed, because the next status
|
|
161
|
+
// event lets a survivor claim ownership opportunistically inside
|
|
162
|
+
// scheduleRetry.
|
|
154
163
|
// ---------------------------------------------------------------------------
|
|
155
164
|
|
|
156
165
|
export interface ConnectionLifecycleOptions {
|
|
@@ -176,14 +185,30 @@ export interface ConnectionLifecycleHandle {
|
|
|
176
185
|
|
|
177
186
|
const defaultShouldRetryOn = (status: NatsStatus) => status === 'closed' || status === 'disconnected'
|
|
178
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Offer the retry loop to another lifecycle on this connection.
|
|
190
|
+
*
|
|
191
|
+
* A retry is only ever armed from a status event or a failed dial, and a
|
|
192
|
+
* connection that is already down produces neither on its own — so whenever
|
|
193
|
+
* the owner gives the loop up while the connection still needs dialling, a
|
|
194
|
+
* successor has to be pushed rather than left waiting for an event that is not
|
|
195
|
+
* coming.
|
|
196
|
+
*
|
|
197
|
+
* Candidates that cannot service this URL decline inside `scheduleRetry`, so
|
|
198
|
+
* this walks the roster until one actually takes it. Callers must clear any
|
|
199
|
+
* timer of their own first: a successor that claims while a timer is still
|
|
200
|
+
* armed will not arm one, and the departing owner's timer no longer speaks for
|
|
201
|
+
* the connection.
|
|
202
|
+
*/
|
|
203
|
+
function handOffRetry(conn: SharedConnection): void {
|
|
204
|
+
for (const takeOver of conn.retrySchedulers) {
|
|
205
|
+
takeOver()
|
|
206
|
+
if (conn.retryOwner) return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
179
210
|
export function startConnectionLifecycle(options: ConnectionLifecycleOptions): ConnectionLifecycleHandle {
|
|
180
211
|
const { conn, wsUrl } = options
|
|
181
|
-
// Each lifecycle gets its own identity for retry ownership. The first lifecycle to claim
|
|
182
|
-
// an unowned connection drives reconnect; later attachers observe status only until the
|
|
183
|
-
// owner releases (see opportunistic claim in scheduleRetry).
|
|
184
|
-
const ownerToken = {}
|
|
185
|
-
if (!conn.retryOwner) conn.retryOwner = ownerToken
|
|
186
|
-
|
|
187
212
|
let closed = false
|
|
188
213
|
let retryAttempt = 0
|
|
189
214
|
|
|
@@ -195,16 +220,33 @@ export function startConnectionLifecycle(options: ConnectionLifecycleOptions): C
|
|
|
195
220
|
}
|
|
196
221
|
}
|
|
197
222
|
|
|
223
|
+
// A lifecycle's identity IS its `scheduleRetry`: that function goes on the
|
|
224
|
+
// connection's roster and, for whichever lifecycle holds the loop, into
|
|
225
|
+
// `retryOwner` — one identity, not two.
|
|
226
|
+
conn.retrySchedulers.add(scheduleRetry)
|
|
227
|
+
if (!conn.retryOwner) conn.retryOwner = scheduleRetry
|
|
228
|
+
|
|
198
229
|
function scheduleRetry() {
|
|
199
230
|
if (closed) return
|
|
200
231
|
if (getSharedConnectionFor(wsUrl) !== conn) return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
232
|
+
// This lifecycle now wants a different URL, so it must not drive this
|
|
233
|
+
// connection — not from its own status events, and not as a handoff
|
|
234
|
+
// successor. Without the guard, two consumers that had both moved on
|
|
235
|
+
// passed ownership back and forth indefinitely, running onBeforeReconnect
|
|
236
|
+
// — a token refresh in the real callers — on every pass and dialling
|
|
237
|
+
// nothing. The armed callback re-checks, because the URL can move between
|
|
238
|
+
// arming and firing.
|
|
239
|
+
if (options.getFreshUrl() !== wsUrl) return
|
|
240
|
+
if (!conn.retryOwner) conn.retryOwner = scheduleRetry
|
|
241
|
+
if (conn.retryOwner !== scheduleRetry) return
|
|
242
|
+
|
|
243
|
+
// One outage can raise more than one status: nats.ws reports
|
|
244
|
+
// `staleConnection` and then, once the transport is down, `disconnect`.
|
|
245
|
+
// Re-arming on the second would spend a backoff tier on the same outage —
|
|
246
|
+
// on the defaults, waiting 2000ms where the caller asked for 1000. The
|
|
247
|
+
// armed timer re-validates everything when it fires, so the first one
|
|
248
|
+
// scheduled for an outage is always the right one to keep.
|
|
249
|
+
if (conn.retryTimer) return
|
|
208
250
|
|
|
209
251
|
const cfg = options.backoff ?? {}
|
|
210
252
|
const fastRetries = cfg.fastRetries ?? 0
|
|
@@ -225,6 +267,16 @@ export function startConnectionLifecycle(options: ConnectionLifecycleOptions): C
|
|
|
225
267
|
if (closed) return
|
|
226
268
|
if (getSharedConnectionFor(wsUrl) !== conn) return
|
|
227
269
|
|
|
270
|
+
// Nothing to reconnect. Reached when the dial that was already in flight
|
|
271
|
+
// succeeded while this retry sat in its backoff — a handoff during a
|
|
272
|
+
// healthy first connect is enough to get here — and `onBeforeReconnect`
|
|
273
|
+
// is a token refresh in the real callers, so it must not run on a
|
|
274
|
+
// connection that came back on its own.
|
|
275
|
+
if (conn.client.isConnected()) {
|
|
276
|
+
retryAttempt = 0
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
228
280
|
try {
|
|
229
281
|
await options.onBeforeReconnect?.()
|
|
230
282
|
} catch {
|
|
@@ -234,17 +286,33 @@ export function startConnectionLifecycle(options: ConnectionLifecycleOptions): C
|
|
|
234
286
|
if (getSharedConnectionFor(wsUrl) !== conn) return
|
|
235
287
|
|
|
236
288
|
const freshUrl = options.getFreshUrl()
|
|
237
|
-
if (freshUrl !== wsUrl)
|
|
289
|
+
if (freshUrl !== wsUrl) {
|
|
290
|
+
// This lifecycle has moved to a different URL, so it must not dial this
|
|
291
|
+
// connection again while holding the loop — that is the dead end the
|
|
292
|
+
// rest of this module exists to avoid. Give the loop up and push it to
|
|
293
|
+
// someone still on this URL.
|
|
294
|
+
if (conn.retryOwner === scheduleRetry) {
|
|
295
|
+
conn.retryOwner = null
|
|
296
|
+
// A status that landed while the refresh above was awaited can have
|
|
297
|
+
// armed a new timer under this ownership. It belongs to a lifecycle
|
|
298
|
+
// that is leaving, and a successor claiming while it is armed would
|
|
299
|
+
// return early believing the loop is covered — then this timer fires,
|
|
300
|
+
// finds someone else owns the loop, and bails without handing on.
|
|
301
|
+
if (conn.retryTimer) {
|
|
302
|
+
clearTimeout(conn.retryTimer)
|
|
303
|
+
conn.retryTimer = null
|
|
304
|
+
}
|
|
305
|
+
handOffRetry(conn)
|
|
306
|
+
}
|
|
307
|
+
return
|
|
308
|
+
}
|
|
238
309
|
|
|
239
310
|
try {
|
|
240
|
-
conn.
|
|
241
|
-
conn.connectPromise = conn.client.connect()
|
|
242
|
-
await conn.connectPromise
|
|
311
|
+
await conn.client.connect()
|
|
243
312
|
if (!closed && getSharedConnectionFor(wsUrl) === conn) {
|
|
244
313
|
retryAttempt = 0
|
|
245
314
|
}
|
|
246
315
|
} catch {
|
|
247
|
-
conn.connectPromise = null
|
|
248
316
|
if (!closed && getSharedConnectionFor(wsUrl) === conn) {
|
|
249
317
|
scheduleRetry()
|
|
250
318
|
}
|
|
@@ -269,12 +337,20 @@ export function startConnectionLifecycle(options: ConnectionLifecycleOptions): C
|
|
|
269
337
|
emitSynthetic('connected')
|
|
270
338
|
}
|
|
271
339
|
|
|
340
|
+
// Dial through the client rather than caching a promise on the connection.
|
|
341
|
+
// A `conn.connectPromise` used to be assigned here with `||=` and then left
|
|
342
|
+
// in place after a SUCCESSFUL connect, so it doubled as "this connection has
|
|
343
|
+
// been dialled at some point": a consumer that acquired a still-cached
|
|
344
|
+
// connection whose socket had since died joined that long-settled promise
|
|
345
|
+
// instead of dialling. No connect, no failure, so nothing scheduled a retry,
|
|
346
|
+
// and no status event was coming to arm one either — the connection stayed
|
|
347
|
+
// dead for as long as anything held a reference to it. `NatsClient.connect()`
|
|
348
|
+
// already short-circuits on a live connection and joins its own in-flight
|
|
349
|
+
// dial, so the second copy of that bookkeeping bought nothing.
|
|
272
350
|
void (async () => {
|
|
273
351
|
try {
|
|
274
|
-
|
|
275
|
-
await conn.connectPromise
|
|
352
|
+
await conn.client.connect()
|
|
276
353
|
} catch {
|
|
277
|
-
conn.connectPromise = null
|
|
278
354
|
if (closed) return
|
|
279
355
|
|
|
280
356
|
emitSynthetic('disconnected')
|
|
@@ -286,13 +362,33 @@ export function startConnectionLifecycle(options: ConnectionLifecycleOptions): C
|
|
|
286
362
|
stop() {
|
|
287
363
|
closed = true
|
|
288
364
|
unsubStatus()
|
|
365
|
+
conn.retrySchedulers.delete(scheduleRetry)
|
|
366
|
+
|
|
367
|
+
if (conn.retryOwner !== scheduleRetry) {
|
|
368
|
+
// Not ours to cancel. `retryTimer` lives on the CONNECTION, and
|
|
369
|
+
// clearing it unconditionally cancelled the owner's armed retry — so
|
|
370
|
+
// any consumer merely re-running its effect (an `enabled` toggle, a
|
|
371
|
+
// dialog closing, a route change) during a backoff killed reconnect
|
|
372
|
+
// for everyone. Nothing rescheduled it either, for the reason
|
|
373
|
+
// handOffRetry exists. The connection stayed down for good.
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
conn.retryOwner = null
|
|
378
|
+
const wasRetrying = conn.retryTimer !== null
|
|
289
379
|
if (conn.retryTimer) {
|
|
290
380
|
clearTimeout(conn.retryTimer)
|
|
291
381
|
conn.retryTimer = null
|
|
292
382
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
383
|
+
|
|
384
|
+
// A healthy connection with nothing armed needs no successor: the next
|
|
385
|
+
// disconnect lets a survivor claim opportunistically. `wasRetrying` is
|
|
386
|
+
// read before the clearTimeout above, and the remaining case — the timer
|
|
387
|
+
// already fired and its callback is mid-dial — is caught by the
|
|
388
|
+
// connection not being connected. The successor backs off on its OWN
|
|
389
|
+
// attempt counter, which is zero unless it has driven the loop before.
|
|
390
|
+
if (!wasRetrying && conn.client.isConnected()) return
|
|
391
|
+
handOffRetry(conn)
|
|
296
392
|
},
|
|
297
393
|
}
|
|
298
394
|
}
|