doover-js 0.9.0 → 0.9.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/react/messageRangeStore.d.ts +6 -0
- package/dist/react/messageRangeStore.js +12 -0
- package/dist/react/useChannelMessages.d.ts +3 -2
- package/dist/react/useChannelMessages.js +37 -3
- package/dist/test/message-range-store.test.js +16 -0
- package/dist/test/react.test.js +23 -1
- package/package.json +1 -1
|
@@ -65,6 +65,12 @@ export declare class ChannelRangeStore {
|
|
|
65
65
|
* between them, and claiming coverage across it would hide messages forever.
|
|
66
66
|
*/
|
|
67
67
|
recordLive(message: MessageStructure): void;
|
|
68
|
+
/**
|
|
69
|
+
* Replace a message already held by one or more proven ranges. MessageUpdate
|
|
70
|
+
* carries the complete server representation, so both data and attachments
|
|
71
|
+
* supersede the cached create-time copy without changing range coverage.
|
|
72
|
+
*/
|
|
73
|
+
recordUpdate(message: MessageStructure): void;
|
|
68
74
|
/**
|
|
69
75
|
* Stop trusting the live feed to extend coverage — call when the socket drops.
|
|
70
76
|
* Messages sent while disconnected would leave a hole, so segments go back to
|
|
@@ -69,6 +69,18 @@ class ChannelRangeStore {
|
|
|
69
69
|
if (id >= tip.hi)
|
|
70
70
|
tip.hi = id + 1n;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Replace a message already held by one or more proven ranges. MessageUpdate
|
|
74
|
+
* carries the complete server representation, so both data and attachments
|
|
75
|
+
* supersede the cached create-time copy without changing range coverage.
|
|
76
|
+
*/
|
|
77
|
+
recordUpdate(message) {
|
|
78
|
+
for (const segment of this.segments) {
|
|
79
|
+
const index = segment.messages.findIndex((item) => item.id === message.id);
|
|
80
|
+
if (index >= 0)
|
|
81
|
+
segment.messages[index] = message;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
72
84
|
/**
|
|
73
85
|
* Stop trusting the live feed to extend coverage — call when the socket drops.
|
|
74
86
|
* Messages sent while disconnected would leave a hole, so segments go back to
|
|
@@ -75,8 +75,9 @@ export interface UseChannelMessagesResult<TData> extends Omit<UseInfiniteQueryRe
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* Paginated infinite query over `DooverDataProvider.getMessages`, with live
|
|
78
|
-
* `messageCreate` pushes
|
|
79
|
-
*
|
|
78
|
+
* `messageCreate` pushes are appended to the newest page and `messageUpdate`
|
|
79
|
+
* pushes replace matching cached messages. The "next" page fetches older
|
|
80
|
+
* messages (cursor = oldest-loaded message id).
|
|
80
81
|
*/
|
|
81
82
|
export declare function useChannelMessages<TData = unknown>(identifier: ChannelIdentifier, options?: UseChannelMessagesOptions): UseChannelMessagesResult<TData>;
|
|
82
83
|
export {};
|
|
@@ -33,8 +33,9 @@ function channelMessagesQueryKey(agentId, channelName, fields, sources, anchor)
|
|
|
33
33
|
const DEFAULT_PAGE_LIMIT = 10;
|
|
34
34
|
/**
|
|
35
35
|
* Paginated infinite query over `DooverDataProvider.getMessages`, with live
|
|
36
|
-
* `messageCreate` pushes
|
|
37
|
-
*
|
|
36
|
+
* `messageCreate` pushes are appended to the newest page and `messageUpdate`
|
|
37
|
+
* pushes replace matching cached messages. The "next" page fetches older
|
|
38
|
+
* messages (cursor = oldest-loaded message id).
|
|
38
39
|
*/
|
|
39
40
|
function useChannelMessages(identifier, options) {
|
|
40
41
|
const client = (0, context_1.useDooverClient)();
|
|
@@ -94,7 +95,40 @@ function useChannelMessages(identifier, options) {
|
|
|
94
95
|
},
|
|
95
96
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
97
|
[queryClient, agentId, channelName, fields?.join(","), sources?.join(",")]);
|
|
97
|
-
(0,
|
|
98
|
+
const onMessageUpdate = (0, react_1.useCallback)((message) => {
|
|
99
|
+
// A filtered stream must not accept an update that does not belong to it.
|
|
100
|
+
// Newly qualifying messages are left for a refetch because inserting one
|
|
101
|
+
// into an arbitrary cursor page could invalidate that page's boundaries.
|
|
102
|
+
if (fields && fields.length > 0) {
|
|
103
|
+
const data = message.data;
|
|
104
|
+
if (!data ||
|
|
105
|
+
typeof data !== "object" ||
|
|
106
|
+
!fields.some((field) => field in data)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (rangeCacheable)
|
|
111
|
+
store.recordUpdate(message);
|
|
112
|
+
queryClient.setQueryData(key, (current) => {
|
|
113
|
+
if (!current)
|
|
114
|
+
return current;
|
|
115
|
+
let changed = false;
|
|
116
|
+
const typed = message;
|
|
117
|
+
const pages = current.pages.map((page) => page.map((item) => {
|
|
118
|
+
if (item.id !== typed.id)
|
|
119
|
+
return item;
|
|
120
|
+
changed = true;
|
|
121
|
+
return typed;
|
|
122
|
+
}));
|
|
123
|
+
return changed ? { ...current, pages } : current;
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
[queryClient, agentId, channelName, fields?.join(","), sources?.join(",")]);
|
|
128
|
+
(0, useChannelSubscription_1.useChannelSubscription)(liveUpdates ? identifier : undefined, {
|
|
129
|
+
onMessage,
|
|
130
|
+
onMessageUpdate,
|
|
131
|
+
});
|
|
98
132
|
const query = (0, react_query_1.useInfiniteQuery)({
|
|
99
133
|
queryKey: key,
|
|
100
134
|
enabled: !!agentId && !!channelName,
|
|
@@ -95,6 +95,22 @@ const ids = (messages) => (messages ?? []).map((m) => m.id);
|
|
|
95
95
|
(0, chai_1.expect)(store.snapshot()).to.have.length(1);
|
|
96
96
|
(0, chai_1.expect)(ids(store.read(idAt(0), 10))).to.deep.equal(ids(page));
|
|
97
97
|
});
|
|
98
|
+
(0, mocha_1.it)("replaces an updated message without changing range coverage", () => {
|
|
99
|
+
const page = makeMessages(5, 20);
|
|
100
|
+
store.record({ before: idAt(0), limit: 5, page });
|
|
101
|
+
const before = store.snapshot();
|
|
102
|
+
const updated = {
|
|
103
|
+
...page[2],
|
|
104
|
+
data: { i: 2, analysed_by: "detector" },
|
|
105
|
+
attachments: [{ id: "updated-image" }],
|
|
106
|
+
};
|
|
107
|
+
store.recordUpdate(updated);
|
|
108
|
+
const read = store.read(idAt(0), 5);
|
|
109
|
+
(0, chai_1.expect)(read[2]).to.equal(updated);
|
|
110
|
+
(0, chai_1.expect)(ids(read)).to.deep.equal(ids(page));
|
|
111
|
+
(0, chai_1.expect)(store.snapshot().map(({ lo, hi, atStart }) => ({ lo, hi, atStart })))
|
|
112
|
+
.to.deep.equal(before.map(({ lo, hi, atStart }) => ({ lo, hi, atStart })));
|
|
113
|
+
});
|
|
98
114
|
(0, mocha_1.it)("keeps disjoint ranges apart rather than claiming the gap", () => {
|
|
99
115
|
const ancient = makeMessages(5, 500);
|
|
100
116
|
const recent = makeMessages(5, 20);
|
package/dist/test/react.test.js
CHANGED
|
@@ -208,7 +208,7 @@ function wrapper(client) {
|
|
|
208
208
|
const cached = queryClient.getQueryData((0, react_2.channelAggregateQueryKey)("a1", "ui_cmds"));
|
|
209
209
|
(0, chai_1.expect)(cached).to.deep.include({ data: { y: 2 }, attachments: [] });
|
|
210
210
|
});
|
|
211
|
-
(0, mocha_1.it)("useChannelMessages
|
|
211
|
+
(0, mocha_1.it)("useChannelMessages applies live creates and updates", async () => {
|
|
212
212
|
const oldId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2025-12-31T23:59:00.000Z"));
|
|
213
213
|
const newId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:01.000Z"));
|
|
214
214
|
const liveId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:02.000Z"));
|
|
@@ -269,6 +269,28 @@ function wrapper(client) {
|
|
|
269
269
|
newId,
|
|
270
270
|
liveId,
|
|
271
271
|
]));
|
|
272
|
+
await (0, react_1.act)(async () => {
|
|
273
|
+
helpers_1.MockWebSocket.instances[0].receive({
|
|
274
|
+
op: 0,
|
|
275
|
+
t: "MessageUpdate",
|
|
276
|
+
d: {
|
|
277
|
+
message: {
|
|
278
|
+
id: newId,
|
|
279
|
+
author_id: "u1",
|
|
280
|
+
channel: { agent_id: "a1", name: "notes" },
|
|
281
|
+
data: { v: 22, analysed_by: "detector" },
|
|
282
|
+
attachments: [{ id: "analysed-image" }],
|
|
283
|
+
},
|
|
284
|
+
request_data: {},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
await (0, react_1.waitFor)(() => {
|
|
289
|
+
const updated = result.current.messages.find((message) => message.id === newId);
|
|
290
|
+
(0, chai_1.expect)(updated?.data).to.deep.equal({ v: 22, analysed_by: "detector" });
|
|
291
|
+
(0, chai_1.expect)(updated?.attachments).to.have.length(1);
|
|
292
|
+
(0, chai_1.expect)(updated?.attachments?.[0]).to.deep.include({ id: "analysed-image" });
|
|
293
|
+
});
|
|
272
294
|
});
|
|
273
295
|
(0, mocha_1.it)("useChannelMessage seeds via REST and patches on MessageUpdate", async () => {
|
|
274
296
|
const messageId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|