@tai42/studio-sdk 0.5.0 → 0.5.1
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/hooks/useSse.d.ts.map +1 -1
- package/dist/hooks/useSse.js +81 -17
- package/dist/hooks/useSse.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/useSse.ts +93 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSse.d.ts","sourceRoot":"","sources":["../../src/hooks/useSse.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useSse.d.ts","sourceRoot":"","sources":["../../src/hooks/useSse.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKrD,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAU7E,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAC3C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAC9B;AA+HD,wBAAgB,qBAAqB,IAAI,uBAAuB,CAgN/D"}
|
package/dist/hooks/useSse.js
CHANGED
|
@@ -6,10 +6,23 @@
|
|
|
6
6
|
* `interaction.answered` / `interaction.removed` / `interaction.backlog_done`.
|
|
7
7
|
* The `add` frame (backlog on connect + live tail) carries the full question;
|
|
8
8
|
* `answered` / `removed` frames carry only ids. On (re)connect the server
|
|
9
|
-
* replays the current
|
|
10
|
-
* as authoritative
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* replays the current PENDING set as the backlog (answered items are NOT in it);
|
|
10
|
+
* this hook treats that backlog as authoritative for the PENDING set only — a
|
|
11
|
+
* still-pending entry not re-seen by `interaction.backlog_done` is dropped
|
|
12
|
+
* (removed while the client was disconnected), while a locally-ANSWERED entry is
|
|
13
|
+
* exempt from that reconcile so it survives reconnect (reconcile-deleting it
|
|
14
|
+
* would vanish an answered card on the next reconnect, since the pending-only
|
|
15
|
+
* backlog never replays it).
|
|
16
|
+
*
|
|
17
|
+
* ANSWERED-CARD LIFECYCLE: the server emits no removal for an answered card —
|
|
18
|
+
* only `interaction.answered`, never `interaction.removed` for it — so answered
|
|
19
|
+
* cards would otherwise pile up unbounded in the always-mounted badge/inbox. This
|
|
20
|
+
* hook AGES them out itself: an answered card is dropped once it is older than
|
|
21
|
+
* `ANSWERED_RETENTION_MS`, with `ANSWERED_CAP` as a hard newest-N flood backstop.
|
|
22
|
+
* Both are swept lazily on frame/reconnect ticks (the stream carries no idle
|
|
23
|
+
* heartbeat frame), never a per-item timer.
|
|
24
|
+
*
|
|
25
|
+
* `add` frames are at-least-once, so entries are de-duplicated by
|
|
13
26
|
* `interaction_id`, and a redelivered `add` preserves the client `answered` flag.
|
|
14
27
|
*
|
|
15
28
|
* The `Interaction` type is imported type-only (no runtime dep on api-client);
|
|
@@ -29,6 +42,15 @@ import { useOnUnauthorized } from './useUnauthorized';
|
|
|
29
42
|
// clients apart rather than retrying in lockstep.
|
|
30
43
|
const RECONNECT_BASE_MS = 1500;
|
|
31
44
|
const RECONNECT_CAP_MS = 30000;
|
|
45
|
+
// The server emits no removal for an answered card — only `interaction.answered`,
|
|
46
|
+
// never `interaction.removed` for it — so answered cards would otherwise pile up
|
|
47
|
+
// unbounded in the always-mounted badge/inbox. The client ages them out: drop an
|
|
48
|
+
// answered card ANSWERED_RETENTION_MS after its answered frame (a "recently
|
|
49
|
+
// answered" window), keeping at most ANSWERED_CAP as a hard flood backstop. Swept
|
|
50
|
+
// lazily on frame/reconnect ticks (the stream carries no idle heartbeat frame),
|
|
51
|
+
// never a per-item timer.
|
|
52
|
+
const ANSWERED_RETENTION_MS = 10 * 60 * 1000;
|
|
53
|
+
const ANSWERED_CAP = 200;
|
|
32
54
|
// The wire `answer_format` values. Duplicated from the api-client enum by design:
|
|
33
55
|
// the SDK is the leaf and must not import api-client at runtime. The `satisfies`
|
|
34
56
|
// pin against `Interaction['answer_format']` fails to compile if a listed value
|
|
@@ -146,21 +168,50 @@ export function useInteractionsStream() {
|
|
|
146
168
|
useEffect(() => {
|
|
147
169
|
const controller = new AbortController();
|
|
148
170
|
const aborted = () => controller.signal.aborted;
|
|
171
|
+
// The published list carries the public shape only — the internal
|
|
172
|
+
// `answeredAt` aging stamp is stripped here.
|
|
173
|
+
const toPublicList = () => Array.from(mapRef.current.values(), ({ answeredAt: _answeredAt, ...rest }) => rest);
|
|
174
|
+
// Age answered cards out (the server sends no removal for them): drop any past
|
|
175
|
+
// the retention window, then — as a flood backstop — the oldest beyond the
|
|
176
|
+
// count cap. Pending items (`answeredAt === null`) are never touched. Called on
|
|
177
|
+
// every frame/reconnect tick so the always-mounted badge cannot grow unbounded.
|
|
178
|
+
const sweepAnswered = () => {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const answered = [];
|
|
181
|
+
for (const entry of mapRef.current.values()) {
|
|
182
|
+
if (entry.answeredAt !== null) {
|
|
183
|
+
answered.push({ id: entry.interaction_id, answeredAt: entry.answeredAt });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const { id, answeredAt } of answered) {
|
|
187
|
+
if (now - answeredAt >= ANSWERED_RETENTION_MS)
|
|
188
|
+
mapRef.current.delete(id);
|
|
189
|
+
}
|
|
190
|
+
const live = answered.filter(({ id }) => mapRef.current.has(id));
|
|
191
|
+
if (live.length > ANSWERED_CAP) {
|
|
192
|
+
live.sort((a, b) => a.answeredAt - b.answeredAt); // oldest first
|
|
193
|
+
for (const { id } of live.slice(0, live.length - ANSWERED_CAP)) {
|
|
194
|
+
mapRef.current.delete(id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
149
198
|
// A successful data frame rebuilds the list and clears any transient
|
|
150
199
|
// frame-parse error — one bad frame on a healthy stream must not stick.
|
|
151
200
|
const publish = () => {
|
|
201
|
+
sweepAnswered();
|
|
152
202
|
setState((prev) => ({
|
|
153
203
|
...prev,
|
|
154
|
-
interactions:
|
|
204
|
+
interactions: toPublicList(),
|
|
155
205
|
error: null,
|
|
156
206
|
}));
|
|
157
207
|
};
|
|
158
208
|
const surfaceMalformed = (event) => {
|
|
159
209
|
setState((prev) => ({ ...prev, error: new Error(`malformed interaction frame: ${event}`) }));
|
|
160
210
|
};
|
|
161
|
-
// Per-connection reconcile state: the replayed backlog is
|
|
162
|
-
//
|
|
163
|
-
// removed
|
|
211
|
+
// Per-connection reconcile state: the replayed backlog is the PENDING set
|
|
212
|
+
// only, so a still-pending id not re-seen by `interaction.backlog_done` is
|
|
213
|
+
// dropped (removed while disconnected); a locally-answered id is exempt — it
|
|
214
|
+
// survives reconnect and is aged out client-side (see `sweepAnswered`).
|
|
164
215
|
let reconciling = false;
|
|
165
216
|
let seenInBacklog = new Set();
|
|
166
217
|
// Consecutive failed reconnect attempts. Reset only when a connection proves
|
|
@@ -175,8 +226,13 @@ export function useInteractionsStream() {
|
|
|
175
226
|
const applyFrame = (event, data) => {
|
|
176
227
|
if (event === 'interaction.backlog_done') {
|
|
177
228
|
if (reconciling) {
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
// The pending-only backlog is authoritative for pending items alone:
|
|
230
|
+
// drop a still-pending id it did not replay (removed while
|
|
231
|
+
// disconnected), but NEVER an answered id — that is terminal and the
|
|
232
|
+
// backlog never carries it, so reconcile-deleting it would vanish an
|
|
233
|
+
// answered card on every reconnect.
|
|
234
|
+
for (const [id, item] of mapRef.current) {
|
|
235
|
+
if (!seenInBacklog.has(id) && !item.answered)
|
|
180
236
|
mapRef.current.delete(id);
|
|
181
237
|
}
|
|
182
238
|
reconciling = false;
|
|
@@ -184,9 +240,10 @@ export function useInteractionsStream() {
|
|
|
184
240
|
// A completed backlog is proof the connection is healthy — clear the
|
|
185
241
|
// reconnect backoff so the next drop reconnects promptly.
|
|
186
242
|
reconnectAttempt = 0;
|
|
243
|
+
sweepAnswered();
|
|
187
244
|
setState((prev) => ({
|
|
188
245
|
...prev,
|
|
189
|
-
interactions:
|
|
246
|
+
interactions: toPublicList(),
|
|
190
247
|
backlogLoaded: true,
|
|
191
248
|
}));
|
|
192
249
|
return;
|
|
@@ -209,10 +266,14 @@ export function useInteractionsStream() {
|
|
|
209
266
|
}
|
|
210
267
|
// The answered frame carries only ids — flip the existing item's flag,
|
|
211
268
|
// keeping its question/format. An answered event for an item we never saw
|
|
212
|
-
// has nothing to show.
|
|
269
|
+
// has nothing to show. Stamp the answered time on the FIRST answered frame
|
|
270
|
+
// only; a redelivered answered keeps the original stamp so the retention
|
|
271
|
+
// window is not extended.
|
|
213
272
|
const existing = mapRef.current.get(id);
|
|
214
|
-
if (existing)
|
|
215
|
-
|
|
273
|
+
if (existing) {
|
|
274
|
+
const answeredAt = existing.answered ? existing.answeredAt : Date.now();
|
|
275
|
+
mapRef.current.set(id, { ...existing, answered: true, answeredAt });
|
|
276
|
+
}
|
|
216
277
|
publish();
|
|
217
278
|
return;
|
|
218
279
|
}
|
|
@@ -224,12 +285,15 @@ export function useInteractionsStream() {
|
|
|
224
285
|
}
|
|
225
286
|
if (reconciling)
|
|
226
287
|
seenInBacklog.add(interaction.interaction_id);
|
|
227
|
-
// Dedupe by id; a redelivered add preserves the client answered flag
|
|
228
|
-
// prior `answered` is not undone by an at-least-once
|
|
288
|
+
// Dedupe by id; a redelivered add preserves the client answered flag (and
|
|
289
|
+
// its aging stamp) so a prior `answered` is not undone by an at-least-once
|
|
290
|
+
// replay. A fresh (pending) add carries no stamp until its answered frame.
|
|
229
291
|
const existing = mapRef.current.get(interaction.interaction_id);
|
|
292
|
+
const answered = existing?.answered ?? false;
|
|
230
293
|
mapRef.current.set(interaction.interaction_id, {
|
|
231
294
|
...interaction,
|
|
232
|
-
answered
|
|
295
|
+
answered,
|
|
296
|
+
answeredAt: answered ? (existing?.answeredAt ?? null) : null,
|
|
233
297
|
});
|
|
234
298
|
publish();
|
|
235
299
|
};
|
package/dist/hooks/useSse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSse.js","sourceRoot":"","sources":["../../src/hooks/useSse.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useSse.js","sourceRoot":"","sources":["../../src/hooks/useSse.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tai42/studio-sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The Studio plugin contract, shared UI components, and hooks that every feature builds on.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "tai42 <oss@tai42.ai>",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"@radix-ui/react-select": "^2.3.2",
|
|
90
90
|
"@radix-ui/react-tabs": "^1.1.16",
|
|
91
91
|
"@radix-ui/react-tooltip": "^1.2.11",
|
|
92
|
-
"@tai42/api-client": "0.5.
|
|
92
|
+
"@tai42/api-client": "0.5.1"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
95
|
"build": "tsc -b tsconfig.build.json && node scripts/copy-assets.mjs",
|
package/src/hooks/useSse.ts
CHANGED
|
@@ -6,10 +6,23 @@
|
|
|
6
6
|
* `interaction.answered` / `interaction.removed` / `interaction.backlog_done`.
|
|
7
7
|
* The `add` frame (backlog on connect + live tail) carries the full question;
|
|
8
8
|
* `answered` / `removed` frames carry only ids. On (re)connect the server
|
|
9
|
-
* replays the current
|
|
10
|
-
* as authoritative
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* replays the current PENDING set as the backlog (answered items are NOT in it);
|
|
10
|
+
* this hook treats that backlog as authoritative for the PENDING set only — a
|
|
11
|
+
* still-pending entry not re-seen by `interaction.backlog_done` is dropped
|
|
12
|
+
* (removed while the client was disconnected), while a locally-ANSWERED entry is
|
|
13
|
+
* exempt from that reconcile so it survives reconnect (reconcile-deleting it
|
|
14
|
+
* would vanish an answered card on the next reconnect, since the pending-only
|
|
15
|
+
* backlog never replays it).
|
|
16
|
+
*
|
|
17
|
+
* ANSWERED-CARD LIFECYCLE: the server emits no removal for an answered card —
|
|
18
|
+
* only `interaction.answered`, never `interaction.removed` for it — so answered
|
|
19
|
+
* cards would otherwise pile up unbounded in the always-mounted badge/inbox. This
|
|
20
|
+
* hook AGES them out itself: an answered card is dropped once it is older than
|
|
21
|
+
* `ANSWERED_RETENTION_MS`, with `ANSWERED_CAP` as a hard newest-N flood backstop.
|
|
22
|
+
* Both are swept lazily on frame/reconnect ticks (the stream carries no idle
|
|
23
|
+
* heartbeat frame), never a per-item timer.
|
|
24
|
+
*
|
|
25
|
+
* `add` frames are at-least-once, so entries are de-duplicated by
|
|
13
26
|
* `interaction_id`, and a redelivered `add` preserves the client `answered` flag.
|
|
14
27
|
*
|
|
15
28
|
* The `Interaction` type is imported type-only (no runtime dep on api-client);
|
|
@@ -26,6 +39,14 @@ import { useOnUnauthorized } from './useUnauthorized';
|
|
|
26
39
|
/** A live interaction plus the client-maintained `answered` flag. */
|
|
27
40
|
export type StreamInteraction = Interaction & { readonly answered: boolean };
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The internal map value: a `StreamInteraction` plus the wall-clock ms of its
|
|
44
|
+
* answered frame (`null` while pending), read only by the client-side aging
|
|
45
|
+
* sweep. `answeredAt` is stripped before the interaction is published — it is not
|
|
46
|
+
* part of the public `StreamInteraction` contract.
|
|
47
|
+
*/
|
|
48
|
+
type TrackedInteraction = StreamInteraction & { readonly answeredAt: number | null };
|
|
49
|
+
|
|
29
50
|
export interface InteractionsStreamState {
|
|
30
51
|
readonly interactions: StreamInteraction[];
|
|
31
52
|
readonly connected: boolean;
|
|
@@ -43,6 +64,16 @@ export interface InteractionsStreamState {
|
|
|
43
64
|
const RECONNECT_BASE_MS = 1500;
|
|
44
65
|
const RECONNECT_CAP_MS = 30000;
|
|
45
66
|
|
|
67
|
+
// The server emits no removal for an answered card — only `interaction.answered`,
|
|
68
|
+
// never `interaction.removed` for it — so answered cards would otherwise pile up
|
|
69
|
+
// unbounded in the always-mounted badge/inbox. The client ages them out: drop an
|
|
70
|
+
// answered card ANSWERED_RETENTION_MS after its answered frame (a "recently
|
|
71
|
+
// answered" window), keeping at most ANSWERED_CAP as a hard flood backstop. Swept
|
|
72
|
+
// lazily on frame/reconnect ticks (the stream carries no idle heartbeat frame),
|
|
73
|
+
// never a per-item timer.
|
|
74
|
+
const ANSWERED_RETENTION_MS = 10 * 60 * 1000;
|
|
75
|
+
const ANSWERED_CAP = 200;
|
|
76
|
+
|
|
46
77
|
// The wire `answer_format` values. Duplicated from the api-client enum by design:
|
|
47
78
|
// the SDK is the leaf and must not import api-client at runtime. The `satisfies`
|
|
48
79
|
// pin against `Interaction['answer_format']` fails to compile if a listed value
|
|
@@ -159,18 +190,48 @@ export function useInteractionsStream(): InteractionsStreamState {
|
|
|
159
190
|
});
|
|
160
191
|
// Source of truth: a dedupe map keyed by interaction_id, rebuilt into the
|
|
161
192
|
// ordered array on every change.
|
|
162
|
-
const mapRef = useRef<Map<string,
|
|
193
|
+
const mapRef = useRef<Map<string, TrackedInteraction>>(new Map());
|
|
163
194
|
|
|
164
195
|
useEffect(() => {
|
|
165
196
|
const controller = new AbortController();
|
|
166
197
|
const aborted = () => controller.signal.aborted;
|
|
167
198
|
|
|
199
|
+
// The published list carries the public shape only — the internal
|
|
200
|
+
// `answeredAt` aging stamp is stripped here.
|
|
201
|
+
const toPublicList = (): StreamInteraction[] =>
|
|
202
|
+
Array.from(mapRef.current.values(), ({ answeredAt: _answeredAt, ...rest }) => rest);
|
|
203
|
+
|
|
204
|
+
// Age answered cards out (the server sends no removal for them): drop any past
|
|
205
|
+
// the retention window, then — as a flood backstop — the oldest beyond the
|
|
206
|
+
// count cap. Pending items (`answeredAt === null`) are never touched. Called on
|
|
207
|
+
// every frame/reconnect tick so the always-mounted badge cannot grow unbounded.
|
|
208
|
+
const sweepAnswered = () => {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const answered: { id: string; answeredAt: number }[] = [];
|
|
211
|
+
for (const entry of mapRef.current.values()) {
|
|
212
|
+
if (entry.answeredAt !== null) {
|
|
213
|
+
answered.push({ id: entry.interaction_id, answeredAt: entry.answeredAt });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const { id, answeredAt } of answered) {
|
|
217
|
+
if (now - answeredAt >= ANSWERED_RETENTION_MS) mapRef.current.delete(id);
|
|
218
|
+
}
|
|
219
|
+
const live = answered.filter(({ id }) => mapRef.current.has(id));
|
|
220
|
+
if (live.length > ANSWERED_CAP) {
|
|
221
|
+
live.sort((a, b) => a.answeredAt - b.answeredAt); // oldest first
|
|
222
|
+
for (const { id } of live.slice(0, live.length - ANSWERED_CAP)) {
|
|
223
|
+
mapRef.current.delete(id);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
168
228
|
// A successful data frame rebuilds the list and clears any transient
|
|
169
229
|
// frame-parse error — one bad frame on a healthy stream must not stick.
|
|
170
230
|
const publish = () => {
|
|
231
|
+
sweepAnswered();
|
|
171
232
|
setState((prev) => ({
|
|
172
233
|
...prev,
|
|
173
|
-
interactions:
|
|
234
|
+
interactions: toPublicList(),
|
|
174
235
|
error: null,
|
|
175
236
|
}));
|
|
176
237
|
};
|
|
@@ -179,9 +240,10 @@ export function useInteractionsStream(): InteractionsStreamState {
|
|
|
179
240
|
setState((prev) => ({ ...prev, error: new Error(`malformed interaction frame: ${event}`) }));
|
|
180
241
|
};
|
|
181
242
|
|
|
182
|
-
// Per-connection reconcile state: the replayed backlog is
|
|
183
|
-
//
|
|
184
|
-
// removed
|
|
243
|
+
// Per-connection reconcile state: the replayed backlog is the PENDING set
|
|
244
|
+
// only, so a still-pending id not re-seen by `interaction.backlog_done` is
|
|
245
|
+
// dropped (removed while disconnected); a locally-answered id is exempt — it
|
|
246
|
+
// survives reconnect and is aged out client-side (see `sweepAnswered`).
|
|
185
247
|
let reconciling = false;
|
|
186
248
|
let seenInBacklog = new Set<string>();
|
|
187
249
|
// Consecutive failed reconnect attempts. Reset only when a connection proves
|
|
@@ -197,17 +259,23 @@ export function useInteractionsStream(): InteractionsStreamState {
|
|
|
197
259
|
const applyFrame = (event: string, data: string) => {
|
|
198
260
|
if (event === 'interaction.backlog_done') {
|
|
199
261
|
if (reconciling) {
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
// The pending-only backlog is authoritative for pending items alone:
|
|
263
|
+
// drop a still-pending id it did not replay (removed while
|
|
264
|
+
// disconnected), but NEVER an answered id — that is terminal and the
|
|
265
|
+
// backlog never carries it, so reconcile-deleting it would vanish an
|
|
266
|
+
// answered card on every reconnect.
|
|
267
|
+
for (const [id, item] of mapRef.current) {
|
|
268
|
+
if (!seenInBacklog.has(id) && !item.answered) mapRef.current.delete(id);
|
|
202
269
|
}
|
|
203
270
|
reconciling = false;
|
|
204
271
|
}
|
|
205
272
|
// A completed backlog is proof the connection is healthy — clear the
|
|
206
273
|
// reconnect backoff so the next drop reconnects promptly.
|
|
207
274
|
reconnectAttempt = 0;
|
|
275
|
+
sweepAnswered();
|
|
208
276
|
setState((prev) => ({
|
|
209
277
|
...prev,
|
|
210
|
-
interactions:
|
|
278
|
+
interactions: toPublicList(),
|
|
211
279
|
backlogLoaded: true,
|
|
212
280
|
}));
|
|
213
281
|
return;
|
|
@@ -230,9 +298,14 @@ export function useInteractionsStream(): InteractionsStreamState {
|
|
|
230
298
|
}
|
|
231
299
|
// The answered frame carries only ids — flip the existing item's flag,
|
|
232
300
|
// keeping its question/format. An answered event for an item we never saw
|
|
233
|
-
// has nothing to show.
|
|
301
|
+
// has nothing to show. Stamp the answered time on the FIRST answered frame
|
|
302
|
+
// only; a redelivered answered keeps the original stamp so the retention
|
|
303
|
+
// window is not extended.
|
|
234
304
|
const existing = mapRef.current.get(id);
|
|
235
|
-
if (existing)
|
|
305
|
+
if (existing) {
|
|
306
|
+
const answeredAt = existing.answered ? existing.answeredAt : Date.now();
|
|
307
|
+
mapRef.current.set(id, { ...existing, answered: true, answeredAt });
|
|
308
|
+
}
|
|
236
309
|
publish();
|
|
237
310
|
return;
|
|
238
311
|
}
|
|
@@ -243,12 +316,15 @@ export function useInteractionsStream(): InteractionsStreamState {
|
|
|
243
316
|
return;
|
|
244
317
|
}
|
|
245
318
|
if (reconciling) seenInBacklog.add(interaction.interaction_id);
|
|
246
|
-
// Dedupe by id; a redelivered add preserves the client answered flag
|
|
247
|
-
// prior `answered` is not undone by an at-least-once
|
|
319
|
+
// Dedupe by id; a redelivered add preserves the client answered flag (and
|
|
320
|
+
// its aging stamp) so a prior `answered` is not undone by an at-least-once
|
|
321
|
+
// replay. A fresh (pending) add carries no stamp until its answered frame.
|
|
248
322
|
const existing = mapRef.current.get(interaction.interaction_id);
|
|
323
|
+
const answered = existing?.answered ?? false;
|
|
249
324
|
mapRef.current.set(interaction.interaction_id, {
|
|
250
325
|
...interaction,
|
|
251
|
-
answered
|
|
326
|
+
answered,
|
|
327
|
+
answeredAt: answered ? (existing?.answeredAt ?? null) : null,
|
|
252
328
|
});
|
|
253
329
|
publish();
|
|
254
330
|
};
|