doover-js 0.7.0 → 0.7.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.
@@ -3,6 +3,13 @@ import type { CreateMessageRequest, DataSeries, MessageStructure, UpdateMessageR
3
3
  import type { DooverRequestOptions } from "../client/request-options";
4
4
  export interface ListMessagesParams {
5
5
  before?: string;
6
+ /**
7
+ * Lower-bound cursor (exclusive). With `before` set (or defaulted) it merely
8
+ * floors the window. On its own — `after` set, `before` omitted — it pages
9
+ * *forward*: the server returns the `limit` **oldest** messages strictly
10
+ * newer than the cursor, oldest-first. This is the only way to walk history
11
+ * upward toward "now".
12
+ */
6
13
  after?: string;
7
14
  limit?: number;
8
15
  field_name?: string[];
@@ -5,15 +5,31 @@ const snowflake_1 = require("../utils/snowflake");
5
5
  const _args_1 = require("./_args");
6
6
  function resolveListMessagesParams(params) {
7
7
  const order = params?.order ?? "desc";
8
+ const hasAfter = params?.after !== undefined;
9
+ const hasBefore = params?.before !== undefined;
10
+ // The server keys pagination on whichever bound is sent: `before` walks
11
+ // newest-first downward, `after` alone walks oldest-first upward (the N
12
+ // oldest messages strictly newer than the cursor). Sending `after` without
13
+ // `before` is the only way to page forward; sending both, or neither, keeps
14
+ // the newest-first behaviour (defaulting `before` to "now" when unbounded).
15
+ if (hasAfter && !hasBefore) {
16
+ const query = {
17
+ after: params.after,
18
+ limit: params?.limit ?? 10,
19
+ };
20
+ if (params?.field_name !== undefined)
21
+ query.field_name = params.field_name;
22
+ return { query, order, serverAscending: true };
23
+ }
8
24
  const query = {
9
25
  before: params?.before ?? (0, snowflake_1.generateSnowflakeIdAtTime)(new Date()),
10
26
  limit: params?.limit ?? 10,
11
27
  };
12
- if (params?.after !== undefined)
28
+ if (hasAfter)
13
29
  query.after = params.after;
14
30
  if (params?.field_name !== undefined)
15
31
  query.field_name = params.field_name;
16
- return { query, order };
32
+ return { query, order, serverAscending: false };
17
33
  }
18
34
  class MessagesApi {
19
35
  constructor(rest) {
@@ -24,10 +40,13 @@ class MessagesApi {
24
40
  return this._listMessages(agentId, channelName, options);
25
41
  }
26
42
  async _listMessages(agentId, channelName, params) {
27
- const { query, order } = resolveListMessagesParams(params);
43
+ const { query, order, serverAscending } = resolveListMessagesParams(params);
28
44
  const response = await this.rest.get(`/agents/${agentId}/channels/${channelName}/messages`, query);
29
45
  const stamped = response.map(snowflake_1.addTimestampToMessage);
30
- return order === "asc" ? [...stamped].reverse() : stamped;
46
+ // The server returns newest-first for `before` and oldest-first for a bare
47
+ // `after`; reverse only when that native order differs from what was asked.
48
+ const wantAscending = order === "asc";
49
+ return serverAscending === wantAscending ? stamped : [...stamped].reverse();
31
50
  }
32
51
  async postMessage(...args) {
33
52
  if (typeof args[0] === "string") {
@@ -99,38 +99,68 @@ function useChannelMessages(identifier, options) {
99
99
  queryKey: key,
100
100
  enabled: !!agentId && !!channelName,
101
101
  staleTime: Infinity,
102
- initialPageParam: (anchor ?? initialBefore),
103
- getNextPageParam: (lastPage) => lastPage && lastPage.length > 0 ? lastPage[0]?.id : undefined,
102
+ initialPageParam: { before: anchor ?? initialBefore },
103
+ // Older (down): the next cursor is the oldest id currently held.
104
+ getNextPageParam: (lastPage) => lastPage && lastPage.length > 0
105
+ ? { before: lastPage[0]?.id }
106
+ : undefined,
107
+ // Newer (up): only for an anchored window — the live window already begins
108
+ // at "now", so it has nothing above to page into. `after` = newest id held;
109
+ // the server returns the oldest messages above it, walking up toward now.
110
+ // An empty page whose own cursor was already a forward `after` means the
111
+ // top is reached; an empty *initial* page (nothing below the anchor) still
112
+ // seeds the walk from the anchor, since messages may sit above it.
113
+ getPreviousPageParam: (firstPage, _all, firstPageParam) => {
114
+ if (!anchor)
115
+ return undefined;
116
+ if (firstPage && firstPage.length > 0) {
117
+ return { after: firstPage[firstPage.length - 1]?.id };
118
+ }
119
+ if (firstPageParam?.after !== undefined)
120
+ return undefined;
121
+ return { after: anchor };
122
+ },
104
123
  queryFn: async ({ pageParam }) => {
105
124
  if (!agentId || !channelName)
106
125
  return [];
107
126
  const id = { agentId, channelName };
108
- // Resolve `before` here rather than letting the API default it, so the
109
- // range store records the exact bound the request proved.
110
- const before = typeof pageParam === "string"
111
- ? pageParam
112
- : (0, snowflake_1.generateSnowflakeIdAtTime)(new Date());
127
+ const param = (pageParam ?? {});
113
128
  // Mirrors the API's own default; the store needs the limit actually applied
114
129
  const effectiveLimit = limit ?? DEFAULT_PAGE_LIMIT;
130
+ // Pass `{ sources }` as a trailing bag only when set — cast through
131
+ // `unknown` since the TypeScript overloads don't declare it.
132
+ const runList = (params) => sources
133
+ ? client.messages.listMessages(id, params, { sources })
134
+ : client.messages.listMessages(id, params);
135
+ // Forward (newer) page: `after` alone pages oldest-first upward. Bypasses
136
+ // the range store, which models before-anchored windows only.
137
+ if (param.after !== undefined && param.before === undefined) {
138
+ const page = await runList({
139
+ after: param.after,
140
+ ...(limit !== undefined ? { limit } : {}),
141
+ ...(fields && fields.length > 0 ? { field_name: fields } : {}),
142
+ order: "asc",
143
+ });
144
+ return page;
145
+ }
146
+ // Older page. Resolve `before` here rather than letting the API default
147
+ // it, so the range store records the exact bound the request proved.
148
+ const before = typeof param.before === "string"
149
+ ? param.before
150
+ : (0, snowflake_1.generateSnowflakeIdAtTime)(new Date());
115
151
  if (rangeCacheable) {
116
152
  const cached = store.read(BigInt(before), effectiveLimit);
117
153
  if (cached)
118
154
  return cached;
119
155
  }
120
156
  const requestedAt = Date.now();
121
- const params = {
157
+ const page = await runList({
122
158
  before,
123
159
  ...(limit !== undefined ? { limit } : {}),
124
160
  ...(fields && fields.length > 0 ? { field_name: fields } : {}),
125
161
  ...(after !== undefined ? { after } : {}),
126
162
  order: "asc",
127
- };
128
- // Pass `{ sources }` as a trailing bag only when set — cast through
129
- // `never` since the TypeScript overloads don't declare it.
130
- const sourcesArg = sources ? { sources } : undefined;
131
- const page = sourcesArg
132
- ? await client.messages.listMessages(id, params, sourcesArg)
133
- : await client.messages.listMessages(id, params);
163
+ });
134
164
  if (rangeCacheable) {
135
165
  store.record({
136
166
  before: BigInt(before),
@@ -229,6 +229,36 @@ function setupRest(responseFactory) {
229
229
  (0, chai_1.expect)(formData.get("json_payload")).to.equal(JSON.stringify({ data: { x: 1 } }));
230
230
  (0, chai_1.expect)(formData.get("attachment-0")).to.be.instanceOf(Blob);
231
231
  });
232
+ (0, mocha_1.it)("pages forward with a bare `after`: sends no `before`, keeps oldest-first", async () => {
233
+ const older = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00Z"));
234
+ const newer = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-02T00:00:00Z"));
235
+ // The `after`-only route returns oldest-first natively (scan_index_forward).
236
+ const nativeAsc = [
237
+ { id: older, author_id: "u", channel: { agent_id: "a1", name: "c1" }, data: {}, attachments: [] },
238
+ { id: newer, author_id: "u", channel: { agent_id: "a1", name: "c1" }, data: {}, attachments: [] },
239
+ ];
240
+ let lastQuery;
241
+ const { rest } = setupRest((url) => {
242
+ if (url.endsWith("/messages") || url.includes("/messages?")) {
243
+ lastQuery = new URL(url).searchParams;
244
+ return (0, helpers_1.createJsonResponse)(nativeAsc);
245
+ }
246
+ return (0, helpers_1.createJsonResponse)([]);
247
+ });
248
+ const api = new messages_api_1.MessagesApi(rest);
249
+ // asc: server order already matches → no reversal, no `before` sent.
250
+ const asc = await api.listMessages("a1", "c1", { after: older, limit: 5, order: "asc" });
251
+ (0, chai_1.expect)(lastQuery?.get("after")).to.equal(older);
252
+ (0, chai_1.expect)(lastQuery?.has("before"), "bare `after` must not send `before`").to.equal(false);
253
+ (0, chai_1.expect)(asc.map((m) => m.id)).to.deep.equal([older, newer]);
254
+ // desc over the same forward route reverses the native ascending page.
255
+ const desc = await api.listMessages("a1", "c1", { after: older, limit: 5, order: "desc" });
256
+ (0, chai_1.expect)(desc.map((m) => m.id)).to.deep.equal([newer, older]);
257
+ // With `before` present, `after` is only a floor and both bounds are sent.
258
+ await api.listMessages("a1", "c1", { before: newer, after: older, limit: 5, order: "asc" });
259
+ (0, chai_1.expect)(lastQuery?.get("before")).to.equal(newer);
260
+ (0, chai_1.expect)(lastQuery?.get("after")).to.equal(older);
261
+ });
232
262
  (0, mocha_1.it)("covers aggregates and alarms methods", async () => {
233
263
  const { rest, fetchMock } = setupRest((url) => {
234
264
  if (url.includes("/attachments/")) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const chai_1 = require("chai");
8
+ const mocha_1 = require("mocha");
9
+ const react_1 = require("@testing-library/react");
10
+ const react_query_1 = require("@tanstack/react-query");
11
+ const sinon_1 = __importDefault(require("sinon"));
12
+ const doover_client_1 = require("../client/doover-client");
13
+ const react_2 = require("../react");
14
+ const messageRangeStore_1 = require("../react/messageRangeStore");
15
+ const snowflake_1 = require("../utils/snowflake");
16
+ const helpers_1 = require("./helpers");
17
+ const AGENT = "agent-1";
18
+ const CHANNEL = "camera_1";
19
+ const NOW = Date.now();
20
+ /** `COUNT` messages, one per minute, ascending by id (index 0 = oldest). */
21
+ const COUNT = 60;
22
+ const ALL = Array.from({ length: COUNT }, (_, i) => {
23
+ const at = new Date(NOW - (COUNT - 1 - i) * 60000);
24
+ return {
25
+ id: (0, snowflake_1.generateSnowflakeIdAtTime)(at),
26
+ data: { i },
27
+ attachments: [],
28
+ author_id: "author",
29
+ channel: { agent_id: AGENT, name: CHANNEL },
30
+ };
31
+ }).sort((a, b) => (BigInt(a.id) < BigInt(b.id) ? -1 : 1));
32
+ /**
33
+ * Emulates the REST route's *native* ordering, which the SDK client then
34
+ * reverses per `order`: `before` returns the newest N below the cursor,
35
+ * newest-first; a bare `after` returns the oldest N above it, oldest-first.
36
+ */
37
+ function serveNative(query) {
38
+ const before = query.get("before");
39
+ const after = query.get("after");
40
+ const limit = Number(query.get("limit") ?? "10");
41
+ if (before && !after) {
42
+ const below = ALL.filter((m) => BigInt(m.id) < BigInt(before));
43
+ return below.slice(Math.max(0, below.length - limit)).reverse(); // newest-first
44
+ }
45
+ if (after && !before) {
46
+ const above = ALL.filter((m) => BigInt(m.id) > BigInt(after));
47
+ return above.slice(0, limit); // oldest-first
48
+ }
49
+ // both bounds: newest-first within (after, before]
50
+ const within = ALL.filter((m) => BigInt(m.id) < BigInt(before) && BigInt(m.id) > BigInt(after));
51
+ return within.slice(Math.max(0, within.length - limit)).reverse();
52
+ }
53
+ function makeClient() {
54
+ const calls = [];
55
+ const fetchMock = sinon_1.default.stub().callsFake(async (input) => {
56
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
57
+ if (url.includes("/messages")) {
58
+ const q = new URL(url).searchParams;
59
+ calls.push(q);
60
+ return (0, helpers_1.createJsonResponse)(serveNative(q));
61
+ }
62
+ return (0, helpers_1.createJsonResponse)({});
63
+ });
64
+ const client = new doover_client_1.DooverClient({
65
+ dataRestUrl: "https://api.example.com",
66
+ controlApiUrl: "https://control.example.com",
67
+ dataWssUrl: "wss://ws.example.com",
68
+ fetchImpl: fetchMock,
69
+ webSocketImpl: helpers_1.MockWebSocket,
70
+ disableBrowserLifecycleHooks: true,
71
+ });
72
+ return { client, calls };
73
+ }
74
+ function wrapper(client, queryClient) {
75
+ return function Wrapper({ children }) {
76
+ return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(react_2.DooverProvider, { client: client, children: children }) }));
77
+ };
78
+ }
79
+ const newQueryClient = () => new react_query_1.QueryClient({ defaultOptions: { queries: { retry: false } } });
80
+ const idAtIndex = (i) => ALL[i].id;
81
+ const timeOfIndex = (i) => NOW - (COUNT - 1 - i) * 60000;
82
+ /** A snowflake landing strictly between messages i and i+1 (30s after i). */
83
+ const anchorAfter = (i) => (0, snowflake_1.generateSnowflakeIdAtTime)(new Date(timeOfIndex(i) + 30000));
84
+ (0, mocha_1.describe)("useChannelMessages forward pagination", () => {
85
+ (0, mocha_1.beforeEach)(() => {
86
+ (0, helpers_1.installSessionStorageMock)();
87
+ (0, messageRangeStore_1.resetChannelRangeStores)();
88
+ });
89
+ (0, mocha_1.afterEach)(() => {
90
+ sinon_1.default.restore();
91
+ });
92
+ (0, mocha_1.it)("pages forward from an anchor into newer messages", async () => {
93
+ const { client } = makeClient();
94
+ const qc = newQueryClient();
95
+ // Anchor between index 29 and 30 — half the history is newer than it.
96
+ const anchor = anchorAfter(29);
97
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useChannelMessages)({ agentId: AGENT, channelName: CHANNEL }, { limit: 10, anchor }), { wrapper: wrapper(client, qc) });
98
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages).to.have.length(10));
99
+ // The anchored page is the 10 newest *below* the anchor: indices 20..29.
100
+ (0, chai_1.expect)(result.current.messages.every((m) => BigInt(m.id) < BigInt(anchor))).to.equal(true);
101
+ (0, chai_1.expect)(result.current.hasPreviousPage).to.equal(true);
102
+ await (0, react_1.act)(async () => {
103
+ await result.current.fetchPreviousPage();
104
+ });
105
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages.some((m) => BigInt(m.id) > BigInt(anchor))).to.equal(true));
106
+ // Forward page brought in the messages immediately above the anchor (index 30+).
107
+ (0, chai_1.expect)(result.current.messages.map((m) => m.data).some((d) => d.i >= 30)).to.equal(true);
108
+ });
109
+ (0, mocha_1.it)("exposes no previous page for an unanchored (live) window", async () => {
110
+ const { client } = makeClient();
111
+ const qc = newQueryClient();
112
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useChannelMessages)({ agentId: AGENT, channelName: CHANNEL }, { limit: 10 }), { wrapper: wrapper(client, qc) });
113
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages).to.have.length(10));
114
+ (0, chai_1.expect)(result.current.hasPreviousPage).to.equal(false);
115
+ });
116
+ (0, mocha_1.it)("recovers messages above an anchor that has nothing below it, and terminates", async () => {
117
+ const { client } = makeClient();
118
+ const qc = newQueryClient();
119
+ // Anchor older than every message: the initial before=anchor page is empty.
120
+ const anchor = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date(NOW - COUNT * 10 * 60000));
121
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useChannelMessages)({ agentId: AGENT, channelName: CHANNEL }, { limit: 10, anchor }), { wrapper: wrapper(client, qc) });
122
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.isLoading).to.equal(false));
123
+ (0, chai_1.expect)(result.current.messages).to.have.length(0);
124
+ // Empty below, but the walk still seeds from the anchor itself.
125
+ (0, chai_1.expect)(result.current.hasPreviousPage).to.equal(true);
126
+ // Drain forward until exhausted — must terminate, never loop.
127
+ for (let guard = 0; guard < 20 && result.current.hasPreviousPage; guard++) {
128
+ await (0, react_1.act)(async () => {
129
+ await result.current.fetchPreviousPage();
130
+ });
131
+ }
132
+ (0, chai_1.expect)(result.current.hasPreviousPage).to.equal(false);
133
+ // Every message above the anchor is recovered (order is the consumer's job).
134
+ const ids = result.current.messages.map((m) => m.id);
135
+ (0, chai_1.expect)(ids).to.have.length(COUNT);
136
+ (0, chai_1.expect)(ids).to.include(idAtIndex(0));
137
+ (0, chai_1.expect)(ids).to.include(idAtIndex(COUNT - 1));
138
+ });
139
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const chai_1 = require("chai");
8
+ const mocha_1 = require("mocha");
9
+ const react_1 = require("@testing-library/react");
10
+ const react_query_1 = require("@tanstack/react-query");
11
+ const doover_client_1 = require("../client/doover-client");
12
+ const react_2 = require("../react");
13
+ const helpers_1 = require("./helpers");
14
+ const sinon_1 = __importDefault(require("sinon"));
15
+ /**
16
+ * Fetch mock for the multi-agent messages endpoint. `pagesByRound` supplies
17
+ * one canned response per call, in order, so a test can hand back
18
+ * `next_cursors` on early rounds and drain on the last one.
19
+ */
20
+ function makeClient(pagesByRound) {
21
+ let round = 0;
22
+ const calls = [];
23
+ const fetchMock = sinon_1.default.stub().callsFake(async (input) => {
24
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
25
+ if (url.includes("/messages")) {
26
+ const parsed = new URL(url);
27
+ calls.push({
28
+ agent_id: parsed.searchParams.getAll("agent_id"),
29
+ agent_before: parsed.searchParams.getAll("agent_before"),
30
+ });
31
+ const page = pagesByRound[Math.min(round, pagesByRound.length - 1)];
32
+ round += 1;
33
+ return (0, helpers_1.createJsonResponse)(page);
34
+ }
35
+ return (0, helpers_1.createJsonResponse)({});
36
+ });
37
+ const client = new doover_client_1.DooverClient({
38
+ dataRestUrl: "https://api.example.com",
39
+ controlApiUrl: "https://control.example.com",
40
+ dataWssUrl: "wss://ws.example.com",
41
+ fetchImpl: fetchMock,
42
+ webSocketImpl: helpers_1.MockWebSocket,
43
+ disableBrowserLifecycleHooks: true,
44
+ });
45
+ return { client, calls };
46
+ }
47
+ function wrapper(client) {
48
+ const queryClient = new react_query_1.QueryClient({ defaultOptions: { queries: { retry: false } } });
49
+ return function Wrapper({ children }) {
50
+ return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(react_2.DooverProvider, { client: client, children: children }) }));
51
+ };
52
+ }
53
+ function msg(id, agentId) {
54
+ return { id, channel: { agent_id: agentId, name: "tag_values" }, data: {} };
55
+ }
56
+ (0, mocha_1.describe)("useMultiAgentChannelMessages autoPaginate", () => {
57
+ (0, mocha_1.beforeEach)(() => {
58
+ (0, helpers_1.installSessionStorageMock)();
59
+ helpers_1.MockWebSocket.reset();
60
+ });
61
+ (0, mocha_1.afterEach)(() => {
62
+ sinon_1.default.restore();
63
+ });
64
+ (0, mocha_1.it)("walks back per-agent via next_cursors until the window drains", async () => {
65
+ const { client, calls } = makeClient([
66
+ // round 1: a1 hit its limit (cursor returned), a2 drained already
67
+ { results: [msg("100", "a1"), msg("99", "a2")], next_cursors: { a1: "90" } },
68
+ // round 2: a1 drains
69
+ { results: [msg("89", "a1")], next_cursors: {} },
70
+ ]);
71
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useMultiAgentChannelMessages)("tag_values", ["a1", "a2"], {
72
+ after: "1",
73
+ agentMessageLimit: 2,
74
+ autoPaginate: true,
75
+ liveUpdates: false,
76
+ }), { wrapper: wrapper(client) });
77
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages).to.have.length(3));
78
+ (0, chai_1.expect)(result.current.hasNextPage).to.equal(false);
79
+ // Second request must resume ONLY a1, using its own cursor as agent_before.
80
+ (0, chai_1.expect)(calls).to.have.length(2);
81
+ (0, chai_1.expect)(calls[1].agent_id).to.deep.equal(["a1"]);
82
+ (0, chai_1.expect)(calls[1].agent_before).to.deep.equal(["90"]);
83
+ });
84
+ (0, mocha_1.it)("does not paginate when autoPaginate is off", async () => {
85
+ const { client, calls } = makeClient([
86
+ { results: [msg("100", "a1")], next_cursors: { a1: "90" } },
87
+ ]);
88
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useMultiAgentChannelMessages)("tag_values", ["a1"], {
89
+ agentMessageLimit: 1,
90
+ liveUpdates: false,
91
+ }), { wrapper: wrapper(client) });
92
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages).to.have.length(1));
93
+ // hasNextPage is exposed, but nothing auto-fetches the second page.
94
+ (0, chai_1.expect)(result.current.hasNextPage).to.equal(true);
95
+ (0, chai_1.expect)(calls).to.have.length(1);
96
+ });
97
+ (0, mocha_1.it)("dedupes messages that repeat across pages", async () => {
98
+ const { client } = makeClient([
99
+ { results: [msg("100", "a1")], next_cursors: { a1: "100" } },
100
+ // overlaps the previous page's id 100 (legacy-style re-return)
101
+ { results: [msg("100", "a1"), msg("90", "a1")], next_cursors: {} },
102
+ ]);
103
+ const { result } = (0, react_1.renderHook)(() => (0, react_2.useMultiAgentChannelMessages)("tag_values", ["a1"], {
104
+ after: "1",
105
+ agentMessageLimit: 1,
106
+ autoPaginate: true,
107
+ liveUpdates: false,
108
+ }), { wrapper: wrapper(client) });
109
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages).to.have.length(2));
110
+ (0, chai_1.expect)(result.current.hasNextPage).to.equal(false);
111
+ (0, chai_1.expect)(result.current.messages.map((m) => m.id)).to.deep.equal(["100", "90"]);
112
+ });
113
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",