doover-js 0.5.1 → 0.5.3

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.
@@ -2,6 +2,15 @@ import type { AgentAggregate, BatchMessagesResponse } from "../types/openapi";
2
2
  import type { RestClient } from "../http/rest-client";
3
3
  export interface MultiAgentMessagesParams {
4
4
  agent_id: string[];
5
+ /**
6
+ * Per-agent `before` cursors, parallel to `agent_id`. When set, must
7
+ * be the same length as `agent_id`; each agent then uses its own
8
+ * cursor as the upper bound instead of the global `before`. This is
9
+ * how paginating clients should resume from a previous response's
10
+ * `next_cursors` map — each agent paginates independently with no
11
+ * inter-agent "watermark retain".
12
+ */
13
+ agent_before?: string[];
5
14
  before?: string;
6
15
  after?: string;
7
16
  limit?: number;
@@ -21,6 +30,14 @@ export declare class AgentsApi {
21
30
  private readonly rest;
22
31
  constructor(rest: RestClient);
23
32
  getMultiAgentMessages(channelName: string, params: MultiAgentMessagesParams): Promise<BatchMessagesResponse>;
33
+ /**
34
+ * Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
35
+ * request's URL stays under CloudFront's hard 8,192-byte URL quota — each
36
+ * agent contributes ~30 chars (`agent_id=<snowflake>&`), so the chunk size
37
+ * below caps the query string at ~7.5KB even before the base URL. Chunks
38
+ * are fetched in parallel and merged into one `{ results, count }` so
39
+ * callers never have to think about the limit.
40
+ */
24
41
  getMultiAgentAggregates(channelName: string, params: MultiAgentAggregatesParams): Promise<{
25
42
  results: AgentAggregate[];
26
43
  count: number;
@@ -13,8 +13,37 @@ class AgentsApi {
13
13
  results: response.results.map((message) => "timestamp" in message ? message : (0, snowflake_1.addTimestampToMessage)(message)),
14
14
  };
15
15
  }
16
- getMultiAgentAggregates(channelName, params) {
17
- return this.rest.get(`/agents/channels/${channelName}/aggregates`, params);
16
+ /**
17
+ * Batch-fetch aggregates for many agents. Auto-chunks `agent_id` so each
18
+ * request's URL stays under CloudFront's hard 8,192-byte URL quota — each
19
+ * agent contributes ~30 chars (`agent_id=<snowflake>&`), so the chunk size
20
+ * below caps the query string at ~7.5KB even before the base URL. Chunks
21
+ * are fetched in parallel and merged into one `{ results, count }` so
22
+ * callers never have to think about the limit.
23
+ */
24
+ async getMultiAgentAggregates(channelName, params) {
25
+ const path = `/agents/channels/${channelName}/aggregates`;
26
+ const { agent_id, ...rest } = params;
27
+ if (agent_id.length <= MULTI_AGENT_CHUNK_SIZE) {
28
+ return this.rest.get(path, params);
29
+ }
30
+ const chunks = [];
31
+ for (let i = 0; i < agent_id.length; i += MULTI_AGENT_CHUNK_SIZE) {
32
+ chunks.push(agent_id.slice(i, i + MULTI_AGENT_CHUNK_SIZE));
33
+ }
34
+ const responses = await Promise.all(chunks.map((chunk) => this.rest.get(path, {
35
+ ...rest,
36
+ agent_id: chunk,
37
+ })));
38
+ return {
39
+ results: responses.flatMap((r) => r.results),
40
+ count: responses.reduce((acc, r) => acc + r.count, 0),
41
+ };
18
42
  }
19
43
  }
20
44
  exports.AgentsApi = AgentsApi;
45
+ // CloudFront fronts the channels-rest API with a hard 8,192-byte URL quota.
46
+ // Each agent adds ~30 chars (`agent_id=<19-digit snowflake>&`), so 250 IDs
47
+ // is ~7.5KB of query string — comfortably under the cap with room for the
48
+ // base URL and any `field_name` params.
49
+ const MULTI_AGENT_CHUNK_SIZE = 250;
@@ -16,7 +16,12 @@ export interface UseChannelAggregateResult<TData> extends Omit<UseQueryResult<Ag
16
16
  data: TData | undefined;
17
17
  /** Attachments (files/blobs) on the aggregate. */
18
18
  attachments: Aggregate["attachments"] | undefined;
19
- /** Server timestamp (epoch seconds) of the last aggregate update. */
19
+ /**
20
+ * Server timestamp (epoch **milliseconds**) of the last aggregate update —
21
+ * matches the `Date` / `dayjs()` constructor. Use `dayjs(last_updated)`, not
22
+ * `dayjs.unix(last_updated)` (the latter assumes seconds and lands ~58000
23
+ * years in the future).
24
+ */
20
25
  last_updated: number | null | undefined;
21
26
  }
22
27
  export interface UseChannelAggregateOptions {
@@ -61,6 +61,27 @@ function setupRest(responseFactory) {
61
61
  (0, chai_1.expect)(fetchMock.getCall(0).args[0]).to.equal("https://api.example.com/agents/channels/c1/messages?agent_id=a1&agent_id=a2&limit=5");
62
62
  (0, chai_1.expect)(fetchMock.getCall(1).args[0]).to.equal("https://api.example.com/agents/channels/c1/aggregates?agent_id=a1");
63
63
  });
64
+ (0, mocha_1.it)("chunks getMultiAgentAggregates over the per-request agent cap", async () => {
65
+ const { rest, fetchMock } = setupRest((url) => {
66
+ const ids = [...new URL(url).searchParams.getAll("agent_id")];
67
+ return (0, helpers_1.createJsonResponse)({
68
+ results: ids.map((id) => ({ agent_id: id, data: {}, attachments: [] })),
69
+ count: ids.length,
70
+ });
71
+ });
72
+ const api = new agents_api_1.AgentsApi(rest);
73
+ // 600 agents → 3 chunks of 250/250/100.
74
+ const agentIds = Array.from({ length: 600 }, (_, i) => `a${i}`);
75
+ const aggregates = await api.getMultiAgentAggregates("c1", {
76
+ agent_id: agentIds,
77
+ });
78
+ (0, chai_1.expect)(fetchMock.callCount).to.equal(3);
79
+ (0, chai_1.expect)(aggregates.results).to.have.length(600);
80
+ (0, chai_1.expect)(aggregates.count).to.equal(600);
81
+ // Every agent is represented exactly once across the merged chunks.
82
+ const returnedIds = aggregates.results.map((r) => r.agent_id).sort();
83
+ (0, chai_1.expect)(returnedIds).to.deep.equal([...agentIds].sort());
84
+ });
64
85
  (0, mocha_1.it)("covers channels methods", async () => {
65
86
  const { rest, fetchMock } = setupRest(() => (0, helpers_1.createJsonResponse)([]));
66
87
  const api = new channels_api_1.ChannelsApi(rest);
@@ -4,16 +4,28 @@ export interface BatchMessagesResponse {
4
4
  results: MessageStructure[];
5
5
  count: number;
6
6
  /**
7
- * Snowflake cursor for the next sub-page — non-null means at least one
8
- * agent returned a full `agent_message_limit` worth and may have more
9
- * older messages within the window. Re-request with `before=<next>`.
7
+ * Per-agent resume cursors. Any agent whose fetch hit the per-agent
8
+ * limit before draining the window maps to the oldest snowflake we
9
+ * did return for it. Pass these back as `agent_before` (parallel to
10
+ * `agent_id`) on the next call to continue paginating each agent
11
+ * independently. Empty (or missing, on older servers) when the
12
+ * window is fully drained.
13
+ *
14
+ * This is the canonical pagination signal — `next` and
15
+ * `at_limit_agent_ids` are kept populated for back-compat with
16
+ * pre-`next_cursors` clients.
17
+ */
18
+ next_cursors?: Record<string, string>;
19
+ /**
20
+ * Legacy single-cursor pagination signal — the most-recent (highest
21
+ * snowflake id) oldest-cursor across at-limit agents. Clients
22
+ * paginating with a single global `before` should pass this back;
23
+ * prefer `next_cursors` + `agent_before` for new code.
10
24
  */
11
25
  next?: string | null;
12
26
  /**
13
- * Agent ids that returned a full `agent_message_limit` worth in this
14
- * response — those that may have more older messages within the window.
15
- * Pass these (and only these) as `agent_id` on the next sub-page so the
16
- * server doesn't repeat DDB lookups for already-exhausted agents.
27
+ * Legacy: agent ids with possibly-more older messages — equivalent to
28
+ * `Object.keys(next_cursors)`. Prefer `next_cursors` for new code.
17
29
  */
18
30
  at_limit_agent_ids?: string[];
19
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",