doover-js 0.2.0 → 0.4.0

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.
@@ -6,6 +6,7 @@ export { useAgentConnections, agentConnectionsQueryKey, } from "./useAgentConnec
6
6
  export { useChannelSubscription, } from "./useChannelSubscription";
7
7
  export type { ChannelSubscriptionHandlers } from "./useChannelSubscription";
8
8
  export { useChannelAggregate, channelAggregateQueryKey, } from "./useChannelAggregate";
9
+ export type { UseChannelAggregateResult } from "./useChannelAggregate";
9
10
  export { useAgentChannel } from "./useAgentChannel";
10
11
  export { useSendMessage } from "./useSendMessage";
11
12
  export { useUpdateAggregate } from "./useUpdateAggregate";
@@ -4,4 +4,4 @@ import type { Aggregate } from "../types/common";
4
4
  * and channel name as separate arguments — the most common shape at call
5
5
  * sites where the identifier isn't already a `ChannelIdentifier`.
6
6
  */
7
- export declare function useAgentChannel<TData extends Aggregate["data"] = Aggregate["data"]>(agentId: string | undefined, channelName: string): import("@tanstack/react-query").UseQueryResult<Aggregate<TData> | undefined>;
7
+ export declare function useAgentChannel<TData = Aggregate["data"]>(agentId: string | undefined, channelName: string): import("./useChannelAggregate").UseChannelAggregateResult<TData>;
@@ -2,12 +2,32 @@ import { type UseQueryResult } from "@tanstack/react-query";
2
2
  import type { Aggregate } from "../types/common";
3
3
  import type { ChannelIdentifier } from "../types/viewer";
4
4
  export declare function channelAggregateQueryKey(agentId: string | undefined, channelName: string | undefined): readonly ["doover", "agent", string | undefined, "channel", string | undefined];
5
+ /**
6
+ * Shape returned by `useChannelAggregate`. Shadows the react-query
7
+ * `data` field with the unwrapped aggregate payload (`TData`) so the
8
+ * common call-site pattern — `const { data } = useChannelAggregate(…)` —
9
+ * just works without a secondary `.data` access or cast. The
10
+ * `attachments` and `last_updated` slots are hoisted alongside for the
11
+ * same reason; everything else (isLoading, isError, refetch, status,
12
+ * …) passes through from react-query unchanged.
13
+ */
14
+ export interface UseChannelAggregateResult<TData> extends Omit<UseQueryResult<Aggregate<TData> | undefined>, "data"> {
15
+ /** The aggregate payload. `undefined` until the first fetch lands. */
16
+ data: TData | undefined;
17
+ /** Attachments (files/blobs) on the aggregate. */
18
+ attachments: Aggregate["attachments"] | undefined;
19
+ /** Server timestamp (epoch seconds) of the last aggregate update. */
20
+ last_updated: number | null | undefined;
21
+ }
5
22
  /**
6
23
  * Fetch a channel's aggregate and keep it live via the gateway. Incoming
7
24
  * `channelSync` and `aggregateUpdate` events patch the react-query cache
8
25
  * under `channelAggregateQueryKey(agentId, channelName)`.
9
26
  *
10
27
  * Typed on `TData` — the shape of `aggregate.data`. Defaults to the generic
11
- * `Record<string, JSONValue>` used by `Aggregate`.
28
+ * `Record<string, JSONValue>` used by `Aggregate`. No `extends` constraint
29
+ * on the generic: typed aggregate interfaces (e.g. `{ enabled: boolean }`)
30
+ * don't have the implicit index signature that `Aggregate["data"]` carries
31
+ * and enforcing compatibility there just forces `as` casts at call sites.
12
32
  */
13
- export declare function useChannelAggregate<TData extends Aggregate["data"] = Aggregate["data"]>(identifier: ChannelIdentifier): UseQueryResult<Aggregate<TData> | undefined>;
33
+ export declare function useChannelAggregate<TData = Aggregate["data"]>(identifier: ChannelIdentifier): UseChannelAggregateResult<TData>;
@@ -15,7 +15,10 @@ function channelAggregateQueryKey(agentId, channelName) {
15
15
  * under `channelAggregateQueryKey(agentId, channelName)`.
16
16
  *
17
17
  * Typed on `TData` — the shape of `aggregate.data`. Defaults to the generic
18
- * `Record<string, JSONValue>` used by `Aggregate`.
18
+ * `Record<string, JSONValue>` used by `Aggregate`. No `extends` constraint
19
+ * on the generic: typed aggregate interfaces (e.g. `{ enabled: boolean }`)
20
+ * don't have the implicit index signature that `Aggregate["data"]` carries
21
+ * and enforcing compatibility there just forces `as` casts at call sites.
19
22
  */
20
23
  function useChannelAggregate(identifier) {
21
24
  const client = (0, context_1.useDooverClient)();
@@ -29,10 +32,18 @@ function useChannelAggregate(identifier) {
29
32
  // eslint-disable-next-line react-hooks/exhaustive-deps
30
33
  [queryClient, agentId, channelName]);
31
34
  (0, useChannelSubscription_1.useChannelSubscription)(identifier, { onAggregate });
32
- return (0, react_query_1.useQuery)({
35
+ const query = (0, react_query_1.useQuery)({
33
36
  queryKey: key,
34
37
  enabled: !!agentId && !!channelName,
35
38
  staleTime: Infinity,
36
39
  queryFn: () => client.viewer.getAggregate(identifier),
37
40
  });
41
+ const aggregate = query.data;
42
+ const { data: _ignored, ...rest } = query;
43
+ return {
44
+ ...rest,
45
+ data: aggregate?.data,
46
+ attachments: aggregate?.attachments,
47
+ last_updated: aggregate?.last_updated,
48
+ };
38
49
  }
@@ -6,6 +6,19 @@ export interface UseChannelMessagesOptions {
6
6
  limit?: number;
7
7
  /** If false, skip subscribing for live message-create updates. Defaults true. */
8
8
  liveUpdates?: boolean;
9
+ /**
10
+ * Restrict the returned messages to those whose payload contains any of
11
+ * these top-level field names. Passed through as `field_name` on the
12
+ * REST call.
13
+ */
14
+ fields?: string[];
15
+ /**
16
+ * Optional first-page `before` cursor (snowflake id). Defaults to
17
+ * unset, which returns the latest messages. Use this when you need to
18
+ * guard against client-side clock skew (seed with a slightly-future
19
+ * snowflake so server-stamped messages don't get missed).
20
+ */
21
+ initialBefore?: string;
9
22
  }
10
23
  type Page<TData> = MessageStructure<TData>[];
11
24
  export interface UseChannelMessagesResult<TData> extends Omit<UseInfiniteQueryResult<InfiniteData<Page<TData>>>, "data"> {
@@ -20,6 +20,8 @@ function useChannelMessages(identifier, options) {
20
20
  const { agentId, channelName } = identifier;
21
21
  const limit = options?.limit;
22
22
  const liveUpdates = options?.liveUpdates ?? true;
23
+ const fields = options?.fields;
24
+ const initialBefore = options?.initialBefore;
23
25
  const key = channelMessagesQueryKey(agentId, channelName);
24
26
  const onMessage = (0, react_1.useCallback)((message) => {
25
27
  queryClient.setQueryData(key, (current) => {
@@ -41,12 +43,13 @@ function useChannelMessages(identifier, options) {
41
43
  queryKey: key,
42
44
  enabled: !!agentId && !!channelName,
43
45
  staleTime: Infinity,
44
- initialPageParam: undefined,
46
+ initialPageParam: initialBefore,
45
47
  getNextPageParam: (lastPage) => lastPage && lastPage.length > 0 ? lastPage[0]?.id : undefined,
46
48
  queryFn: async ({ pageParam }) => {
47
49
  const page = await client.viewer.getMessages(identifier, {
48
50
  ...(typeof pageParam === "string" ? { before: pageParam } : {}),
49
51
  ...(limit !== undefined ? { limit } : {}),
52
+ ...(fields && fields.length > 0 ? { field_name: fields } : {}),
50
53
  });
51
54
  return (page ?? []);
52
55
  },
@@ -5,6 +5,7 @@ exports.useMultiAgentAggregates = useMultiAgentAggregates;
5
5
  const react_1 = require("react");
6
6
  const react_query_1 = require("@tanstack/react-query");
7
7
  const context_1 = require("./context");
8
+ const useChannelAggregate_1 = require("./useChannelAggregate");
8
9
  function multiAgentAggregatesQueryKey(channelName, agentIds) {
9
10
  return [
10
11
  "doover",
@@ -28,7 +29,18 @@ function useMultiAgentAggregates(channelName, agentIds, options) {
28
29
  queryKey: key,
29
30
  enabled: agentIds.length > 0,
30
31
  staleTime: Infinity,
31
- queryFn: () => client.agents.getMultiAgentAggregates(channelName, { agent_id: agentIds }),
32
+ queryFn: async () => {
33
+ const response = await client.agents.getMultiAgentAggregates(channelName, { agent_id: agentIds });
34
+ // Seed the per-agent `channelAggregateQueryKey` cache so that
35
+ // sibling `useChannelAggregate(id, channelName)` calls for any
36
+ // of these agents get an instant cache hit rather than issuing
37
+ // a second fetch for data we already have.
38
+ for (const result of response.results) {
39
+ const { agent_id, ...aggregate } = result;
40
+ queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(agent_id, channelName), aggregate);
41
+ }
42
+ return response;
43
+ },
32
44
  });
33
45
  const patchAgentAggregate = (0, react_1.useCallback)((agentId, aggregate) => {
34
46
  queryClient.setQueryData(key, (current) => {
@@ -44,6 +56,9 @@ function useMultiAgentAggregates(channelName, agentIds, options) {
44
56
  : current.results.map((r, i) => (i === idx ? next : r));
45
57
  return { ...current, results };
46
58
  });
59
+ // Mirror live updates into the per-agent cache so sibling
60
+ // `useChannelAggregate` consumers see them too.
61
+ queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(agentId, channelName), aggregate);
47
62
  },
48
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
49
64
  [queryClient, channelName, agentIds.join(",")]);
@@ -5,6 +5,13 @@ export interface UseMultiAgentChannelMessagesOptions {
5
5
  limit?: number;
6
6
  /** If false, skip live subscriptions per agent. Defaults true. */
7
7
  liveUpdates?: boolean;
8
+ /**
9
+ * Restrict the returned messages to those whose payload contains any
10
+ * of these top-level field names. Forwarded as `field_name`.
11
+ */
12
+ fields?: string[];
13
+ /** Optional first-page `before` cursor (snowflake id). */
14
+ initialBefore?: string;
8
15
  }
9
16
  interface Page {
10
17
  results: MessageStructure[];
@@ -19,6 +19,8 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
19
19
  const queryClient = (0, react_query_1.useQueryClient)();
20
20
  const limit = options?.limit;
21
21
  const liveUpdates = options?.liveUpdates ?? true;
22
+ const fields = options?.fields;
23
+ const initialBefore = options?.initialBefore;
22
24
  const key = multiAgentChannelMessagesQueryKey(channelName, agentIds);
23
25
  const prependMessage = (0, react_1.useCallback)((message) => {
24
26
  queryClient.setQueryData(key, (current) => {
@@ -62,13 +64,14 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
62
64
  queryKey: key,
63
65
  enabled: agentIds.length > 0,
64
66
  staleTime: Infinity,
65
- initialPageParam: undefined,
67
+ initialPageParam: initialBefore,
66
68
  getNextPageParam: (lastPage) => lastPage?.next,
67
69
  queryFn: async ({ pageParam }) => {
68
70
  const page = await client.agents.getMultiAgentMessages(channelName, {
69
71
  agent_id: agentIds,
70
72
  ...(typeof pageParam === "string" ? { before: pageParam } : {}),
71
73
  ...(limit !== undefined ? { limit } : {}),
74
+ ...(fields && fields.length > 0 ? { field_name: fields } : {}),
72
75
  });
73
76
  return page;
74
77
  },
@@ -1,9 +1,16 @@
1
1
  import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { AggregateMutationParams } from "../apis/aggregates-api";
2
3
  import type { Aggregate } from "../types/common";
3
4
  import type { ChannelIdentifier } from "../types/viewer";
4
5
  export interface UseUpdateAggregateOptions {
5
6
  /** If true, use PUT semantics (replace) instead of PATCH (merge). */
6
7
  replace?: boolean;
8
+ /**
9
+ * Forwarded as query params to the aggregate endpoint:
10
+ * `log_update` (record a history message), `clear_attachments`,
11
+ * `suppress_response`. See `AggregateMutationParams`.
12
+ */
13
+ params?: AggregateMutationParams;
7
14
  }
8
15
  /**
9
16
  * Mutation wrapping `DooverDataProvider.updateAggregate` (PATCH) or
@@ -14,10 +14,11 @@ function useUpdateAggregate(identifier, options) {
14
14
  const client = (0, context_1.useDooverClient)();
15
15
  const queryClient = (0, react_query_1.useQueryClient)();
16
16
  const replace = options?.replace ?? false;
17
+ const params = options?.params;
17
18
  return (0, react_query_1.useMutation)({
18
19
  mutationFn: (data) => replace
19
- ? client.viewer.putAggregate(identifier, data)
20
- : client.viewer.updateAggregate(identifier, data),
20
+ ? client.viewer.putAggregate(identifier, data, params)
21
+ : client.viewer.updateAggregate(identifier, data, params),
21
22
  onSuccess: (aggregate) => {
22
23
  queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(identifier.agentId, identifier.channelName), aggregate);
23
24
  },
@@ -1,17 +1,33 @@
1
1
  import { type UseMutationResult } from "@tanstack/react-query";
2
2
  import type { MessageBody, MessageMutationParams } from "../apis/messages-api";
3
3
  import type { ChannelIdentifier } from "../types/viewer";
4
- export interface UseUpdateMessageOptions {
5
- /** If true, use PUT (replace) instead of PATCH (merge). Default PATCH. */
4
+ export interface UpdateMessageCallOptions {
5
+ /**
6
+ * If true, use PUT (replace) instead of PATCH (merge). Falls back to
7
+ * the hook-level `replace` option, which itself defaults to PATCH.
8
+ */
6
9
  replace?: boolean;
10
+ /**
11
+ * Query params forwarded to the message endpoint. Merged over the
12
+ * hook-level `params` option (per-call keys win).
13
+ */
7
14
  params?: MessageMutationParams;
8
15
  }
16
+ export interface UseUpdateMessageOptions extends UpdateMessageCallOptions {
17
+ }
9
18
  export interface UpdateMessageVariables {
10
19
  messageId: string;
11
20
  body: MessageBody;
21
+ /**
22
+ * Per-call overrides. Extensible — add new knobs here without breaking
23
+ * the variables shape.
24
+ */
25
+ options?: UpdateMessageCallOptions;
12
26
  }
13
27
  /**
14
28
  * Mutation wrapping `MessagesApi.patchMessage` or `putMessage`. Bound to a
15
- * single channel; the message id and body are passed per-call.
29
+ * single channel; the message id and body are passed per-call. Each
30
+ * invocation can override `replace` / `params` via `variables.options` for
31
+ * mutation sites that sometimes PUT and sometimes PATCH the same channel.
16
32
  */
17
33
  export declare function useUpdateMessage(identifier: ChannelIdentifier, options?: UseUpdateMessageOptions): UseMutationResult<unknown, Error, UpdateMessageVariables>;
@@ -5,17 +5,23 @@ const react_query_1 = require("@tanstack/react-query");
5
5
  const context_1 = require("./context");
6
6
  /**
7
7
  * Mutation wrapping `MessagesApi.patchMessage` or `putMessage`. Bound to a
8
- * single channel; the message id and body are passed per-call.
8
+ * single channel; the message id and body are passed per-call. Each
9
+ * invocation can override `replace` / `params` via `variables.options` for
10
+ * mutation sites that sometimes PUT and sometimes PATCH the same channel.
9
11
  */
10
12
  function useUpdateMessage(identifier, options) {
11
13
  const client = (0, context_1.useDooverClient)();
12
- const replace = options?.replace ?? false;
13
- const params = options?.params;
14
+ const defaultReplace = options?.replace ?? false;
15
+ const defaultParams = options?.params;
14
16
  return (0, react_query_1.useMutation)({
15
- mutationFn: ({ messageId, body }) => {
17
+ mutationFn: ({ messageId, body, options: call }) => {
16
18
  if (!identifier.agentId || !identifier.channelName) {
17
19
  throw new Error("useUpdateMessage requires both agentId and channelName on the identifier.");
18
20
  }
21
+ const replace = call?.replace ?? defaultReplace;
22
+ const params = call?.params || defaultParams
23
+ ? { ...defaultParams, ...call?.params }
24
+ : undefined;
19
25
  return replace
20
26
  ? client.messages.putMessage(identifier.agentId, identifier.channelName, messageId, body, params)
21
27
  : client.messages.patchMessage(identifier.agentId, identifier.channelName, messageId, body, params);
@@ -131,7 +131,8 @@ function wrapper(client) {
131
131
  wrapper: Wrapper,
132
132
  });
133
133
  await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.isSuccess).to.equal(true));
134
- (0, chai_1.expect)(result.current.data?.data).to.deep.equal({ x: 1 });
134
+ (0, chai_1.expect)(result.current.data).to.deep.equal({ x: 1 });
135
+ (0, chai_1.expect)(result.current.attachments).to.deep.equal([]);
135
136
  // Drive a live aggregate update.
136
137
  await (0, react_1.act)(async () => {
137
138
  const ws = helpers_1.MockWebSocket.instances[0];
@@ -154,7 +155,7 @@ function wrapper(client) {
154
155
  },
155
156
  });
156
157
  });
157
- await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.data?.data).to.deep.equal({ x: 2 }));
158
+ await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.data).to.deep.equal({ x: 2 }));
158
159
  });
159
160
  (0, mocha_1.it)("useSendMessage posts via MessagesApi", async () => {
160
161
  const fetchMock = (0, helpers_1.createFetchMock)((url) => {
@@ -1,5 +1,5 @@
1
1
  import { AgentsApi } from "../apis/agents-api";
2
- import { AggregatesApi } from "../apis/aggregates-api";
2
+ import { AggregatesApi, type AggregateMutationParams } from "../apis/aggregates-api";
3
3
  import { ChannelsApi } from "../apis/channels-api";
4
4
  import { ConnectionsApi } from "../apis/connections-api";
5
5
  import { MessagesApi, type ListMessagesParams } from "../apis/messages-api";
@@ -68,8 +68,8 @@ export declare class DooverDataProvider implements ChannelsDataProvider<ChannelI
68
68
  sendRPC<TRequest = object, TResponse = object, TPending = undefined>(identifier: ChannelIdentifier, rpcRequest: RpcRequest<TRequest>, options?: {
69
69
  onStatus?: (status: RpcStatus<TPending>) => void;
70
70
  }): Promise<TResponse>;
71
- updateAggregate(identifier: ChannelIdentifier, message: object): Promise<Aggregate>;
72
- putAggregate(identifier: ChannelIdentifier, message: object): Promise<Aggregate>;
71
+ updateAggregate(identifier: ChannelIdentifier, message: object, params?: AggregateMutationParams): Promise<Aggregate>;
72
+ putAggregate(identifier: ChannelIdentifier, message: object, params?: AggregateMutationParams): Promise<Aggregate>;
73
73
  getChannelSubscriptions(identifier: ChannelIdentifier): Promise<SubscriptionDetails[]>;
74
74
  getAgentConnections(identifier: ChannelIdentifier): Promise<ConnectionDetails[]>;
75
75
  getIdentifierFromPath(path: string, searchParams: URLSearchParams): {
@@ -228,13 +228,13 @@ class DooverDataProvider {
228
228
  });
229
229
  });
230
230
  }
231
- updateAggregate(identifier, message) {
231
+ updateAggregate(identifier, message, params) {
232
232
  const validated = this.requireChannel(identifier);
233
- return this.aggregatesApi.patchAggregate(validated.agentId, validated.channelName, message);
233
+ return this.aggregatesApi.patchAggregate(validated.agentId, validated.channelName, message, params);
234
234
  }
235
- putAggregate(identifier, message) {
235
+ putAggregate(identifier, message, params) {
236
236
  const validated = this.requireChannel(identifier);
237
- return this.aggregatesApi.putAggregate(validated.agentId, validated.channelName, message);
237
+ return this.aggregatesApi.putAggregate(validated.agentId, validated.channelName, message, params);
238
238
  }
239
239
  getChannelSubscriptions(identifier) {
240
240
  const validated = this.requireChannel(identifier);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",