@flamingo-stack/openframe-frontend-core 0.0.750 → 0.0.751
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-45YRDD7W.cjs → chunk-4WNBVNGP.cjs} +13 -13
- package/dist/{chunk-4O2KYY4K.cjs → chunk-53I4YHRT.cjs} +3 -3
- package/dist/{chunk-HOEVD6EN.cjs → chunk-5P7WBA2U.cjs} +4 -4
- package/dist/{chunk-CNUZLOVA.cjs → chunk-5WC535NJ.cjs} +17 -17
- package/dist/{chunk-BO6ZOAPB.cjs → chunk-77IKUBSH.cjs} +6 -6
- package/dist/{chunk-E3KFEOFA.cjs → chunk-CVBHB4OZ.cjs} +4 -4
- package/dist/{chunk-JPUTILTV.cjs → chunk-ESRA7NUI.cjs} +41 -41
- package/dist/{chunk-2J2OBP4N.cjs → chunk-H52GKJ36.cjs} +9 -9
- package/dist/{chunk-G7SBHMRR.cjs → chunk-KHKPLOU4.cjs} +69 -39
- package/dist/{chunk-6KEJQR6L.cjs → chunk-LIGDPFDM.cjs} +131 -131
- package/dist/{chunk-YL62VAHC.cjs → chunk-MJFTWHV6.cjs} +4 -4
- package/dist/{chunk-AW6GP3JK.cjs → chunk-MU7FFN6M.cjs} +34 -34
- package/dist/{chunk-7RCTOWBA.cjs → chunk-NOUROZYH.cjs} +13 -13
- package/dist/{chunk-KVSEWACW.cjs → chunk-QOONZMSQ.cjs} +9 -9
- package/dist/{chunk-FP6HAJQV.cjs → chunk-RVLQHSID.cjs} +70 -70
- package/dist/{chunk-GVETSV7H.cjs → chunk-SDOCZTST.cjs} +32 -32
- package/dist/{chunk-V3UJ6NDR.cjs → chunk-TPH4SRR2.cjs} +6 -6
- package/dist/{chunk-4FWURR3W.cjs → chunk-VUWPLQBY.cjs} +2 -2
- package/dist/{chunk-WZX26ODJ.cjs → chunk-X4LAMJHD.cjs} +54 -35
- package/dist/{cloud-tenants-E4JSG3GH.cjs → cloud-tenants-JDPC2DYF.cjs} +6 -6
- package/dist/components/case-studies/index.cjs +16 -16
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts +7 -5
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.js +33 -14
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/types/api.types.d.ts +27 -8
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +2 -1
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/embeds/index.cjs +7 -7
- package/dist/components/faq/index.cjs +8 -8
- package/dist/components/features/index.cjs +6 -6
- package/dist/components/help-center-pages/index.cjs +67 -67
- package/dist/components/index.cjs +39 -39
- package/dist/components/meeting-scheduler/index.cjs +29 -29
- package/dist/components/navigation/index.cjs +9 -9
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/product-demo/index.cjs +7 -7
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/ui/index.cjs +6 -6
- package/dist/{devices-AVRQGB2N.cjs → devices-ADQWD37J.cjs} +7 -7
- package/dist/{embeddable-chat-SNYQSMS7.cjs → embeddable-chat-7YF2JMPS.cjs} +65 -65
- package/dist/hooks/index.cjs +3 -3
- package/dist/index.cjs +10 -10
- package/dist/{knowledge-S3CNISQH.cjs → knowledge-TNS64EGK.cjs} +7 -7
- package/dist/{logs-WDLOKDXT.cjs → logs-754MDDE4.cjs} +14 -14
- package/dist/nats/index.cjs +2 -2
- package/dist/nats/nats.d.ts +33 -7
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/nats/nats.js +41 -11
- package/dist/{policies-STML7NEZ.cjs → policies-Q23DPIH6.cjs} +10 -10
- package/dist/{scripts-HO53VYGX.cjs → scripts-DCYLK2RZ.cjs} +11 -11
- package/dist/{software-update-4VTK7LWQ.cjs → software-update-XCMGUIER.cjs} +9 -9
- package/package.json +1 -1
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +59 -23
- package/src/components/chat/types/api.types.ts +27 -8
- package/src/components/chat/types/network.types.ts +2 -1
- package/src/nats/nats.ts +91 -20
|
@@ -49,14 +49,16 @@ const RECOVERY_REPORT_FLOOR_MS = 5_000;
|
|
|
49
49
|
* - Subject: `chat.{dialogId}.{topic}`
|
|
50
50
|
* - When `optStartSeq` is a number, the consumer resumes at `optStartSeq + 1`
|
|
51
51
|
* (`DeliverPolicy.ByStartSequence`). When null/undefined, it live-tails
|
|
52
|
-
* (`DeliverPolicy.New`).
|
|
52
|
+
* (`DeliverPolicy.New`) from whenever the server creates the consumer.
|
|
53
53
|
* - On reconnect, the consumer is recreated starting from the highest stream
|
|
54
54
|
* sequence we've already observed + 1, so no chunk is replayed or skipped.
|
|
55
|
-
* - Consumer is ephemeral with `AckPolicy.None` and
|
|
56
|
-
* inactivity threshold (
|
|
55
|
+
* - Consumer is ephemeral with `AckPolicy.None` and nats.ws's default
|
|
56
|
+
* inactivity threshold (`inactiveThresholdMs` only reaches a live-tail
|
|
57
|
+
* consumer — see the option).
|
|
57
58
|
* - `reconnectionCount` also counts consumer recreations and post-absence
|
|
58
|
-
* resyncs, not just NATS reconnects — see the counter's docs.
|
|
59
|
-
*
|
|
59
|
+
* resyncs, not just NATS reconnects — see the counter's docs.
|
|
60
|
+
* - `caughtUpCount` is when callers refetch persisted history after a gap —
|
|
61
|
+
* not `reconnectionCount` (see its docs).
|
|
60
62
|
*/
|
|
61
63
|
export function useJetStreamDialogSubscription({
|
|
62
64
|
enabled,
|
|
@@ -84,6 +86,11 @@ export function useJetStreamDialogSubscription({
|
|
|
84
86
|
// churn — and churn that can feed itself, since each new consumer can report
|
|
85
87
|
// its own recoveries. Callers see the two summed (see the return).
|
|
86
88
|
const [recoveryCount, setRecoveryCount] = useState(0);
|
|
89
|
+
const [caughtUpCount, setCaughtUpCount] = useState(0);
|
|
90
|
+
// The dialog this hook last opened a tail for, and the reconnection count it
|
|
91
|
+
// opened it at: a consumer created for the same dialog at a higher count is
|
|
92
|
+
// one coming back after a gap, which owes the caller a history refetch.
|
|
93
|
+
const tailOpenedAtRef = useRef<{ dialogId: string; count: number } | null>(null);
|
|
87
94
|
// The highest sequence seen, TAGGED with the dialog it belongs to. The tag is
|
|
88
95
|
// what makes the per-dialog reset implicit — a sequence from another
|
|
89
96
|
// conversation simply stops matching — so the dialog-change effect below no
|
|
@@ -381,11 +388,28 @@ export function useJetStreamDialogSubscription({
|
|
|
381
388
|
const initialOptStart = optStartSeqRef.current;
|
|
382
389
|
const startSeq = resumeSeq != null ? resumeSeq + 1 : initialOptStart != null ? initialOptStart + 1 : undefined;
|
|
383
390
|
|
|
391
|
+
// A first consumer reports nothing: the caller has just loaded the history
|
|
392
|
+
// it resumes after. A reconnect or resync recreates it at a higher count,
|
|
393
|
+
// and from then on the caller's history may predate a gap.
|
|
394
|
+
const opened = tailOpenedAtRef.current;
|
|
395
|
+
let owesCatchUp = opened !== null && opened.dialogId === dialogId && reconnectionCount > opened.count;
|
|
396
|
+
if (!owesCatchUp) tailOpenedAtRef.current = { dialogId, count: reconnectionCount };
|
|
397
|
+
|
|
384
398
|
let cancelled = false;
|
|
399
|
+
// Settles an owed catch-up once the replay is in, or once there can be none
|
|
400
|
+
// (a failed resume, below) — never as the tail comes back: a refetch that
|
|
401
|
+
// lands first merges without the turns the replay is still delivering, and
|
|
402
|
+
// they then render twice.
|
|
403
|
+
const settleCatchUp = () => {
|
|
404
|
+
if (cancelled || !owesCatchUp) return;
|
|
405
|
+
owesCatchUp = false;
|
|
406
|
+
setCaughtUpCount(c => c + 1);
|
|
407
|
+
};
|
|
385
408
|
|
|
386
409
|
void (async () => {
|
|
410
|
+
let handle: JetStreamSubscriptionHandle;
|
|
387
411
|
try {
|
|
388
|
-
|
|
412
|
+
handle = await client.subscribeJetStreamOrdered(
|
|
389
413
|
msg => {
|
|
390
414
|
if (cancelled) return;
|
|
391
415
|
const streamSeq = msg.info.streamSequence;
|
|
@@ -424,27 +448,39 @@ export function useJetStreamDialogSubscription({
|
|
|
424
448
|
const now = Date.now();
|
|
425
449
|
if (now - lastRecoveryReportRef.current < RECOVERY_REPORT_FLOOR_MS) return;
|
|
426
450
|
lastRecoveryReportRef.current = now;
|
|
451
|
+
owesCatchUp = true;
|
|
427
452
|
setRecoveryCount(c => c + 1);
|
|
428
453
|
},
|
|
454
|
+
onCaughtUp: settleCatchUp,
|
|
429
455
|
},
|
|
430
456
|
);
|
|
457
|
+
} catch {
|
|
458
|
+
if (cancelled) return;
|
|
459
|
+
setIsSubscribed(false);
|
|
460
|
+
// With no consumer the refetch cannot wait for a replay. The next resume
|
|
461
|
+
// still owes one (`tailOpenedAtRef` did not move), and its own catch-up
|
|
462
|
+
// refetch clears what its replay re-renders meanwhile.
|
|
463
|
+
settleCatchUp();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
431
466
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
return;
|
|
467
|
+
if (cancelled) {
|
|
468
|
+
try {
|
|
469
|
+
handle.unsubscribe();
|
|
470
|
+
} catch {
|
|
471
|
+
// ignore
|
|
439
472
|
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
440
475
|
|
|
441
|
-
|
|
442
|
-
|
|
476
|
+
subscriptionRef.current = handle;
|
|
477
|
+
setIsSubscribed(true);
|
|
478
|
+
// A host callback, outside the subscribe's catch: a throw there must not
|
|
479
|
+
// read as a failed subscribe and settle the catch-up before the replay.
|
|
480
|
+
try {
|
|
443
481
|
onSubscribedRef.current?.();
|
|
444
|
-
} catch {
|
|
445
|
-
|
|
446
|
-
setIsSubscribed(false);
|
|
447
|
-
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.warn('[JetStream] onSubscribed threw:', e);
|
|
448
484
|
}
|
|
449
485
|
})();
|
|
450
486
|
|
|
@@ -464,14 +500,14 @@ export function useJetStreamDialogSubscription({
|
|
|
464
500
|
}, [enabled, dialogId, isConnected, streamName, topic, reconnectionCount]);
|
|
465
501
|
|
|
466
502
|
// One counter for every way the tail comes back — reconnect, consumer
|
|
467
|
-
// recreation, post-absence resync
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// fires for either.
|
|
503
|
+
// recreation, post-absence resync. The last two are the quiet ones; no
|
|
504
|
+
// connection event fires for either. `caughtUpCount` counts the same events,
|
|
505
|
+
// each once its replay is in or once it could not be resumed (see its docs).
|
|
471
506
|
return {
|
|
472
507
|
isConnected,
|
|
473
508
|
isSubscribed,
|
|
474
509
|
reconnectionCount: reconnectionCount + recoveryCount,
|
|
510
|
+
caughtUpCount,
|
|
475
511
|
currentStreamSeq,
|
|
476
512
|
};
|
|
477
513
|
}
|
|
@@ -103,8 +103,9 @@ export interface UseJetStreamDialogSubscriptionOptions {
|
|
|
103
103
|
/** Single topic to subscribe to. */
|
|
104
104
|
topic: NatsMessageType;
|
|
105
105
|
/**
|
|
106
|
-
* Resume from this JetStream sequence + 1
|
|
107
|
-
* DeliverPolicy.New
|
|
106
|
+
* Resume from this JetStream sequence + 1 (0 replays everything the stream retains for the
|
|
107
|
+
* dialog). When null/undefined, the consumer starts with DeliverPolicy.New: live tail only,
|
|
108
|
+
* from whenever the server creates the consumer — after `onSubscribed` has fired.
|
|
108
109
|
*/
|
|
109
110
|
optStartSeq?: number | null;
|
|
110
111
|
onEvent?: (payload: unknown, messageType: NatsMessageType) => void;
|
|
@@ -127,8 +128,10 @@ export interface UseJetStreamDialogSubscriptionOptions {
|
|
|
127
128
|
maxDelayMs?: number;
|
|
128
129
|
multiplier?: number;
|
|
129
130
|
};
|
|
130
|
-
/** Consumer inactivity threshold in ms before NATS auto-cleans it
|
|
131
|
-
*
|
|
131
|
+
/** Consumer inactivity threshold in ms before NATS auto-cleans it, passed to the client
|
|
132
|
+
* (see `JetStreamOrderedSubscribeOptions.inactiveThresholdMs`). It takes effect only
|
|
133
|
+
* while the hook live-tails (`optStartSeq` null, nothing seen yet): every other consumer
|
|
134
|
+
* it opens starts from a sequence ≥ 1, which nats.ws gives its own default. */
|
|
132
135
|
inactiveThresholdMs?: number;
|
|
133
136
|
/**
|
|
134
137
|
* A counter the host bumps when it knows, from outside the page, that the tail
|
|
@@ -140,7 +143,8 @@ export interface UseJetStreamDialogSubscriptionOptions {
|
|
|
140
143
|
* either kind of knowledge bumps this instead of hoping the page noticed.
|
|
141
144
|
*
|
|
142
145
|
* Treated exactly like a reconnect: the consumer is rebuilt from the highest
|
|
143
|
-
* sequence already seen,
|
|
146
|
+
* sequence already seen, `reconnectionCount` moves, and `caughtUpCount` moves
|
|
147
|
+
* once the rebuilt consumer has replayed what it missed, so callers refetch
|
|
144
148
|
* persisted history. Any increase counts; the value itself means nothing, and
|
|
145
149
|
* an increase arriving alongside a `visibilitychange` costs one rebuild, not
|
|
146
150
|
* two.
|
|
@@ -156,11 +160,26 @@ export interface UseJetStreamDialogSubscriptionReturn {
|
|
|
156
160
|
* JetStream ordered-consumer recreation, the page returning to view after
|
|
157
161
|
* being hidden long enough to have missed something, or a host reporting the
|
|
158
162
|
* same via `resyncSignal`. All of them mean the same thing to the caller —
|
|
159
|
-
* the tail may have skipped messages
|
|
160
|
-
*
|
|
161
|
-
*
|
|
163
|
+
* the tail may have skipped messages. One absence counts once even when two
|
|
164
|
+
* sources report it. Starts at 0.
|
|
165
|
+
*
|
|
166
|
+
* Not the moment to refetch persisted history: the replay is still arriving
|
|
167
|
+
* when this moves. See `caughtUpCount`.
|
|
162
168
|
*/
|
|
163
169
|
reconnectionCount: number;
|
|
170
|
+
/**
|
|
171
|
+
* Incremented once a tail that came back — any of the events
|
|
172
|
+
* `reconnectionCount` counts, recoveries past its report floor — has
|
|
173
|
+
* delivered everything the stream still held for the dialog. Refetch
|
|
174
|
+
* persisted history on this: the gap may predate what the stream retains,
|
|
175
|
+
* and only a refetch that lands after the replay merges against the turns it
|
|
176
|
+
* re-delivered. One that lands first merges without them, and they then
|
|
177
|
+
* render a second time. Also moves when such a tail could not be resumed at
|
|
178
|
+
* all, so history is not left at its pre-gap snapshot: the next resume then
|
|
179
|
+
* replays the gap over that refetch, and its own increment's refetch clears
|
|
180
|
+
* the copies. Never moves for the first consumer of a dialog. Starts at 0.
|
|
181
|
+
*/
|
|
182
|
+
caughtUpCount: number;
|
|
164
183
|
/** Highest JetStream stream sequence observed so far (null before first delivery). */
|
|
165
184
|
currentStreamSeq: number | null;
|
|
166
185
|
}
|
|
@@ -36,7 +36,8 @@ export interface ChunkData {
|
|
|
36
36
|
sequenceId?: number;
|
|
37
37
|
/** JetStream stream sequence, populated from JsMsg.info.streamSequence when delivered via JetStream. */
|
|
38
38
|
streamSeq?: number;
|
|
39
|
-
/**
|
|
39
|
+
/** @deprecated Not set by the ai-agent: its chunks carry no stream state, which exists only
|
|
40
|
+
* as the GraphQL `Dialog.streamState` field (resolved from the dialog lock). Read that instead. */
|
|
40
41
|
streamState?: 'IDLE' | 'STREAMING';
|
|
41
42
|
type: string;
|
|
42
43
|
text?: string;
|
package/src/nats/nats.ts
CHANGED
|
@@ -19,14 +19,25 @@ export type JetStreamDeliverPolicy = 'new' | 'byStartSequence';
|
|
|
19
19
|
export interface JetStreamOrderedSubscribeOptions {
|
|
20
20
|
streamName: string;
|
|
21
21
|
filterSubject: string;
|
|
22
|
+
/**
|
|
23
|
+
* `'byStartSequence'` delivers every retained message from `optStartSeq` on.
|
|
24
|
+
* `'new'` delivers only what is published after the server creates the
|
|
25
|
+
* consumer, which happens after the returned promise resolves: a caller that
|
|
26
|
+
* acts once subscribed and must see everything that follows uses a start
|
|
27
|
+
* sequence instead.
|
|
28
|
+
*/
|
|
22
29
|
deliverPolicy: JetStreamDeliverPolicy;
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* First stream sequence to deliver, for `'byStartSequence'`; ignored for
|
|
32
|
+
* `'new'`. Missing or 0 starts at the first message the stream retains.
|
|
33
|
+
*/
|
|
24
34
|
optStartSeq?: number;
|
|
25
35
|
/**
|
|
26
36
|
* Auto-cleanup the ephemeral consumer after this idle time. Defaults to
|
|
27
|
-
* nats.ws's own.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
37
|
+
* nats.ws's own. nats.ws reads it only while it builds the consumer at the
|
|
38
|
+
* start it was asked for — `'new'`, or `optStartSeq` 0/unset — and has
|
|
39
|
+
* delivered nothing yet. A consumer created from `optStartSeq` ≥ 1, and any
|
|
40
|
+
* recreation after a delivery, gets the library default.
|
|
30
41
|
*/
|
|
31
42
|
inactiveThresholdMs?: number;
|
|
32
43
|
/** AbortSignal to tear down the consumer. */
|
|
@@ -39,10 +50,25 @@ export interface JetStreamOrderedSubscribeOptions {
|
|
|
39
50
|
* but everything that aged out of the stream while it was gone is
|
|
40
51
|
* unrecoverable from the tail, and no connection event announces it (the
|
|
41
52
|
* WebSocket never dropped). Callers that keep persisted history alongside
|
|
42
|
-
* the live tail must refetch it
|
|
43
|
-
*
|
|
53
|
+
* the live tail must refetch it, or they keep showing a snapshot from before
|
|
54
|
+
* the gap — once the recreated consumer has caught up (`onCaughtUp`), not
|
|
55
|
+
* here.
|
|
44
56
|
*/
|
|
45
57
|
onRecovered?: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* The consumer has delivered everything the stream held for the subject when
|
|
60
|
+
* it was created. Fires once for the first consumer and once more for each
|
|
61
|
+
* one nats.ws recreates (`onRecovered`): after `onMessage` has handled the
|
|
62
|
+
* last message of that backlog, straight away when it was empty, or when
|
|
63
|
+
* consuming failed and nothing more will arrive. In that last case a later
|
|
64
|
+
* consumer replays the gap over a refetch made in the meantime, and the
|
|
65
|
+
* refetch on ITS `onCaughtUp` clears the copies.
|
|
66
|
+
*
|
|
67
|
+
* This is the moment to refetch persisted history after a gap. A refetch
|
|
68
|
+
* that lands while the backlog is still arriving merges without the turns it
|
|
69
|
+
* is about to replay, and the replay then renders them a second time.
|
|
70
|
+
*/
|
|
71
|
+
onCaughtUp?: () => void;
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
export interface JetStreamSubscriptionHandle {
|
|
@@ -187,7 +213,7 @@ export interface NatsClient {
|
|
|
187
213
|
/**
|
|
188
214
|
* Subscribe to a JetStream subject via an ephemeral OrderedConsumer (no acks).
|
|
189
215
|
* Use `optStartSeq` with `deliverPolicy: 'byStartSequence'` to resume from a known offset,
|
|
190
|
-
* or `deliverPolicy: 'new'` to live-tail.
|
|
216
|
+
* or `deliverPolicy: 'new'` to live-tail (see that option for where it starts).
|
|
191
217
|
*/
|
|
192
218
|
subscribeJetStreamOrdered(
|
|
193
219
|
onMessage: (msg: JsMsg) => void | Promise<void>,
|
|
@@ -355,21 +381,14 @@ function mapNatsTypeToStatus(
|
|
|
355
381
|
function watchForRecovery(
|
|
356
382
|
iter: ConsumerMessages,
|
|
357
383
|
recreatedEvent: ConsumerEvents.OrderedConsumerRecreated,
|
|
358
|
-
|
|
384
|
+
onRecreated: (consumerName: string) => void,
|
|
359
385
|
): void {
|
|
360
|
-
if (!onRecovered) return;
|
|
361
386
|
void (async () => {
|
|
362
387
|
try {
|
|
363
388
|
const status = await iter.status();
|
|
364
389
|
for await (const event of status) {
|
|
365
390
|
if (event.type !== recreatedEvent) continue;
|
|
366
|
-
|
|
367
|
-
onRecovered();
|
|
368
|
-
} catch (e) {
|
|
369
|
-
// A caller that throws must not take the watch down with it — this
|
|
370
|
-
// loop is the only thing reporting recoveries for this subscription.
|
|
371
|
-
console.warn('[nats] onRecovered threw:', e);
|
|
372
|
-
}
|
|
391
|
+
onRecreated(String(event.data));
|
|
373
392
|
}
|
|
374
393
|
} catch {
|
|
375
394
|
// Status ends with the subscription — nothing left to report.
|
|
@@ -377,6 +396,16 @@ function watchForRecovery(
|
|
|
377
396
|
})();
|
|
378
397
|
}
|
|
379
398
|
|
|
399
|
+
/** A caller that throws must not take down the loop reporting to it — that
|
|
400
|
+
* loop is the only thing reporting these events for its subscription. */
|
|
401
|
+
function invokeCallback(name: string, callback: (() => void) | undefined): void {
|
|
402
|
+
try {
|
|
403
|
+
callback?.();
|
|
404
|
+
} catch (e) {
|
|
405
|
+
console.warn(`[nats] ${name} threw:`, e);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
380
409
|
export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
381
410
|
let nc: NatsConnection | null = null;
|
|
382
411
|
let statusLoopAbort: AbortController | null = null;
|
|
@@ -632,8 +661,11 @@ export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
|
632
661
|
const js = conn.jetstream();
|
|
633
662
|
const consumer: Consumer = await js.consumers.get(opts.streamName, {
|
|
634
663
|
filterSubjects: opts.filterSubject,
|
|
635
|
-
|
|
636
|
-
|
|
664
|
+
// nats.ws turns a start sequence of 0 into 1, so a 'new' that fell back to
|
|
665
|
+
// StartSequence replayed everything the stream retains for the subject.
|
|
666
|
+
...(opts.deliverPolicy === 'new'
|
|
667
|
+
? { deliver_policy: nats.DeliverPolicy.New }
|
|
668
|
+
: { deliver_policy: nats.DeliverPolicy.StartSequence, opt_start_seq: opts.optStartSeq ?? 0 }),
|
|
637
669
|
// Milliseconds, NOT nanoseconds: nats.ws runs this through its own
|
|
638
670
|
// `nanos()` before it reaches the server. Pre-converting here multiplied
|
|
639
671
|
// it by 1e6 twice, putting the threshold ~9.5 years out — so the ephemeral
|
|
@@ -673,6 +705,25 @@ export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
|
673
705
|
return { unsubscribe() {} };
|
|
674
706
|
}
|
|
675
707
|
|
|
708
|
+
// Whether the consumer delivering now still owes `onCaughtUp` its backlog.
|
|
709
|
+
let backlogOwed = true;
|
|
710
|
+
const reportCaughtUp = () => {
|
|
711
|
+
if (!backlogOwed || closed) return;
|
|
712
|
+
backlogOwed = false;
|
|
713
|
+
invokeCallback('onCaughtUp', opts.onCaughtUp);
|
|
714
|
+
};
|
|
715
|
+
// An empty backlog has no message to say so: it shows only in the answer to
|
|
716
|
+
// CONSUMER.CREATE, which `info(true)` returns without asking the server again.
|
|
717
|
+
const reportIfNothingPending = async (consumerName?: string) => {
|
|
718
|
+
try {
|
|
719
|
+
const info = await consumer.info(true);
|
|
720
|
+
if (consumerName !== undefined && info.name !== consumerName) return;
|
|
721
|
+
if (info.num_pending === 0) reportCaughtUp();
|
|
722
|
+
} catch {
|
|
723
|
+
// A failed lookup reports nothing: the next message with `pending === 0` does instead.
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
676
727
|
(async () => {
|
|
677
728
|
try {
|
|
678
729
|
const iter = await consumer.consume();
|
|
@@ -685,7 +736,19 @@ export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
|
685
736
|
return;
|
|
686
737
|
}
|
|
687
738
|
iterRef.current = iter;
|
|
688
|
-
|
|
739
|
+
if (opts.onRecovered || opts.onCaughtUp) {
|
|
740
|
+
watchForRecovery(iter, nats.ConsumerEvents.OrderedConsumerRecreated, consumerName => {
|
|
741
|
+
backlogOwed = true;
|
|
742
|
+
invokeCallback('onRecovered', opts.onRecovered);
|
|
743
|
+
// nats.ws announces the recreated consumer before it records it as
|
|
744
|
+
// the current one (a promise continuation does that), so its answer
|
|
745
|
+
// is read once those have run — and only if it is the one announced.
|
|
746
|
+
setTimeout(() => {
|
|
747
|
+
void reportIfNothingPending(consumerName);
|
|
748
|
+
}, 0);
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
if (opts.onCaughtUp) await reportIfNothingPending();
|
|
689
752
|
for await (const msg of iter) {
|
|
690
753
|
if (closed) break;
|
|
691
754
|
try {
|
|
@@ -693,9 +756,17 @@ export function createNatsClient(options: NatsClientOptions): NatsClient {
|
|
|
693
756
|
} catch (e) {
|
|
694
757
|
emitStatus({ status: 'error', data: e });
|
|
695
758
|
}
|
|
759
|
+
// `pending` is what the stream still held for this consumer when it
|
|
760
|
+
// sent this message: 0 means the backlog ends here.
|
|
761
|
+
if (msg.info.pending === 0) reportCaughtUp();
|
|
696
762
|
}
|
|
697
763
|
} catch (e) {
|
|
698
|
-
if (!closed)
|
|
764
|
+
if (!closed) {
|
|
765
|
+
emitStatus({ status: 'error', data: e });
|
|
766
|
+
// This consumer delivers nothing more: settle what it owed rather than
|
|
767
|
+
// leave the caller's history at its pre-gap snapshot (see `onCaughtUp`).
|
|
768
|
+
reportCaughtUp();
|
|
769
|
+
}
|
|
699
770
|
}
|
|
700
771
|
})().catch((e: unknown) => emitStatus({ status: 'error', data: e }));
|
|
701
772
|
|