doover-js 0.2.0-alpha.0 → 0.2.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.
Files changed (59) hide show
  1. package/dist/apis/connections-api.d.ts +6 -0
  2. package/dist/apis/connections-api.js +8 -0
  3. package/dist/client/doover-client.d.ts +13 -0
  4. package/dist/client/doover-client.js +30 -2
  5. package/dist/client/singleton.d.ts +26 -0
  6. package/dist/client/singleton.js +59 -0
  7. package/dist/client/stats.d.ts +55 -0
  8. package/dist/client/stats.js +97 -0
  9. package/dist/gateway/gateway-client.d.ts +30 -6
  10. package/dist/gateway/gateway-client.js +125 -46
  11. package/dist/gateway/types.d.ts +0 -1
  12. package/dist/http/rest-client.d.ts +11 -0
  13. package/dist/http/rest-client.js +17 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +7 -1
  16. package/dist/react/context.d.ts +19 -0
  17. package/dist/react/context.js +28 -0
  18. package/dist/react/index.d.ts +24 -0
  19. package/dist/react/index.js +40 -0
  20. package/dist/react/sharedQueryClient.d.ts +21 -0
  21. package/dist/react/sharedQueryClient.js +39 -0
  22. package/dist/react/useAgentChannel.d.ts +7 -0
  23. package/dist/react/useAgentChannel.js +12 -0
  24. package/dist/react/useAgentConnections.d.ts +8 -0
  25. package/dist/react/useAgentConnections.js +24 -0
  26. package/dist/react/useChannelAggregate.d.ts +13 -0
  27. package/dist/react/useChannelAggregate.js +38 -0
  28. package/dist/react/useChannelMessages.d.ts +23 -0
  29. package/dist/react/useChannelMessages.js +56 -0
  30. package/dist/react/useChannelSubscription.d.ts +16 -0
  31. package/dist/react/useChannelSubscription.js +43 -0
  32. package/dist/react/useConnectionState.d.ts +21 -0
  33. package/dist/react/useConnectionState.js +43 -0
  34. package/dist/react/useMultiAgentAggregates.d.ts +22 -0
  35. package/dist/react/useMultiAgentAggregates.js +82 -0
  36. package/dist/react/useMultiAgentChannelMessages.d.ts +18 -0
  37. package/dist/react/useMultiAgentChannelMessages.js +78 -0
  38. package/dist/react/useSendMessage.d.ts +9 -0
  39. package/dist/react/useSendMessage.js +16 -0
  40. package/dist/react/useSendRpc.d.ts +61 -0
  41. package/dist/react/useSendRpc.js +158 -0
  42. package/dist/react/useTurnCredentials.d.ts +12 -0
  43. package/dist/react/useTurnCredentials.js +25 -0
  44. package/dist/react/useUpdateAggregate.d.ts +14 -0
  45. package/dist/react/useUpdateAggregate.js +25 -0
  46. package/dist/react/useUpdateMessage.d.ts +17 -0
  47. package/dist/react/useUpdateMessage.js +24 -0
  48. package/dist/test/apis.test.js +7 -1
  49. package/dist/test/doover-client.test.js +13 -0
  50. package/dist/test/doover-data-provider.test.js +155 -0
  51. package/dist/test/gateway-client.test.js +122 -8
  52. package/dist/test/react.test.d.ts +1 -0
  53. package/dist/test/react.test.js +353 -0
  54. package/dist/test/singleton.test.d.ts +1 -0
  55. package/dist/test/singleton.test.js +62 -0
  56. package/dist/types/common.d.ts +40 -0
  57. package/dist/viewer/doover-data-provider.d.ts +32 -4
  58. package/dist/viewer/doover-data-provider.js +92 -5
  59. package/package.json +28 -2
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.multiAgentAggregatesQueryKey = multiAgentAggregatesQueryKey;
4
+ exports.useMultiAgentAggregates = useMultiAgentAggregates;
5
+ const react_1 = require("react");
6
+ const react_query_1 = require("@tanstack/react-query");
7
+ const context_1 = require("./context");
8
+ function multiAgentAggregatesQueryKey(channelName, agentIds) {
9
+ return [
10
+ "doover",
11
+ "channel",
12
+ channelName,
13
+ "aggregates",
14
+ [...agentIds].sort().join(","),
15
+ ];
16
+ }
17
+ /**
18
+ * Batch-fetch aggregates for a channel across many agents, then keep each
19
+ * agent's slot live via its own gateway subscription. Reconciles per-agent
20
+ * `channelSync`/`aggregateUpdate` events into the batched query cache.
21
+ */
22
+ function useMultiAgentAggregates(channelName, agentIds, options) {
23
+ const client = (0, context_1.useDooverClient)();
24
+ const queryClient = (0, react_query_1.useQueryClient)();
25
+ const liveUpdates = options?.liveUpdates ?? true;
26
+ const key = multiAgentAggregatesQueryKey(channelName, agentIds);
27
+ const query = (0, react_query_1.useQuery)({
28
+ queryKey: key,
29
+ enabled: agentIds.length > 0,
30
+ staleTime: Infinity,
31
+ queryFn: () => client.agents.getMultiAgentAggregates(channelName, { agent_id: agentIds }),
32
+ });
33
+ const patchAgentAggregate = (0, react_1.useCallback)((agentId, aggregate) => {
34
+ queryClient.setQueryData(key, (current) => {
35
+ if (!current)
36
+ return current;
37
+ const idx = current.results.findIndex((r) => r.agent_id === agentId);
38
+ const next = {
39
+ agent_id: agentId,
40
+ ...aggregate,
41
+ };
42
+ const results = idx === -1
43
+ ? [...current.results, next]
44
+ : current.results.map((r, i) => (i === idx ? next : r));
45
+ return { ...current, results };
46
+ });
47
+ },
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ [queryClient, channelName, agentIds.join(",")]);
50
+ (0, react_1.useEffect)(() => {
51
+ if (!liveUpdates || agentIds.length === 0)
52
+ return;
53
+ const noopMessage = () => { };
54
+ const subscriptions = agentIds.map((agentId) => {
55
+ const identifier = { agentId, channelName };
56
+ const messageCallback = noopMessage;
57
+ const aggregateCallback = (_id, aggregate) => {
58
+ patchAgentAggregate(agentId, aggregate);
59
+ };
60
+ void client.viewer.subscribeToChannel(identifier, messageCallback, aggregateCallback);
61
+ return { identifier, messageCallback };
62
+ });
63
+ return () => {
64
+ for (const { identifier, messageCallback } of subscriptions) {
65
+ client.viewer
66
+ .unsubscribeFromChannel(identifier, messageCallback)
67
+ .catch(() => { });
68
+ }
69
+ };
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, [client, channelName, liveUpdates, agentIds.join(",")]);
72
+ const aggregatesByAgent = (0, react_1.useMemo)(() => {
73
+ const map = {};
74
+ for (const entry of query.data?.results ?? []) {
75
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
76
+ const { agent_id, ...aggregate } = entry;
77
+ map[agent_id] = aggregate;
78
+ }
79
+ return map;
80
+ }, [query.data]);
81
+ return { aggregatesByAgent, query };
82
+ }
@@ -0,0 +1,18 @@
1
+ import { type InfiniteData, type UseInfiniteQueryResult } from "@tanstack/react-query";
2
+ import type { MessageStructure } from "../types/common";
3
+ export declare function multiAgentChannelMessagesQueryKey(channelName: string, agentIds: string[]): readonly ["doover", "channel", string, "messages", string];
4
+ export interface UseMultiAgentChannelMessagesOptions {
5
+ limit?: number;
6
+ /** If false, skip live subscriptions per agent. Defaults true. */
7
+ liveUpdates?: boolean;
8
+ }
9
+ interface Page {
10
+ results: MessageStructure[];
11
+ next?: string;
12
+ }
13
+ export interface UseMultiAgentChannelMessagesResult extends Omit<UseInfiniteQueryResult<InfiniteData<Page>>, "data"> {
14
+ messages: MessageStructure[];
15
+ data: InfiniteData<Page> | undefined;
16
+ }
17
+ export declare function useMultiAgentChannelMessages(channelName: string, agentIds: string[], options?: UseMultiAgentChannelMessagesOptions): UseMultiAgentChannelMessagesResult;
18
+ export {};
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.multiAgentChannelMessagesQueryKey = multiAgentChannelMessagesQueryKey;
4
+ exports.useMultiAgentChannelMessages = useMultiAgentChannelMessages;
5
+ const react_1 = require("react");
6
+ const react_query_1 = require("@tanstack/react-query");
7
+ const context_1 = require("./context");
8
+ function multiAgentChannelMessagesQueryKey(channelName, agentIds) {
9
+ return [
10
+ "doover",
11
+ "channel",
12
+ channelName,
13
+ "messages",
14
+ [...agentIds].sort().join(","),
15
+ ];
16
+ }
17
+ function useMultiAgentChannelMessages(channelName, agentIds, options) {
18
+ const client = (0, context_1.useDooverClient)();
19
+ const queryClient = (0, react_query_1.useQueryClient)();
20
+ const limit = options?.limit;
21
+ const liveUpdates = options?.liveUpdates ?? true;
22
+ const key = multiAgentChannelMessagesQueryKey(channelName, agentIds);
23
+ const prependMessage = (0, react_1.useCallback)((message) => {
24
+ queryClient.setQueryData(key, (current) => {
25
+ if (!current)
26
+ return current;
27
+ const [firstPage, ...rest] = current.pages;
28
+ const firstResults = firstPage?.results ?? [];
29
+ return {
30
+ ...current,
31
+ pages: [
32
+ { ...firstPage, results: [message, ...firstResults] },
33
+ ...rest,
34
+ ],
35
+ };
36
+ });
37
+ },
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ [queryClient, channelName, agentIds.join(",")]);
40
+ (0, react_1.useEffect)(() => {
41
+ if (!liveUpdates || agentIds.length === 0)
42
+ return;
43
+ const subscriptions = agentIds.map((agentId) => {
44
+ const identifier = { agentId, channelName };
45
+ const messageCallback = (_id, message) => {
46
+ prependMessage(message);
47
+ };
48
+ const aggregateCallback = () => { };
49
+ void client.viewer.subscribeToChannel(identifier, messageCallback, aggregateCallback);
50
+ return { identifier, messageCallback };
51
+ });
52
+ return () => {
53
+ for (const { identifier, messageCallback } of subscriptions) {
54
+ client.viewer
55
+ .unsubscribeFromChannel(identifier, messageCallback)
56
+ .catch(() => { });
57
+ }
58
+ };
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [client, channelName, liveUpdates, agentIds.join(",")]);
61
+ const query = (0, react_query_1.useInfiniteQuery)({
62
+ queryKey: key,
63
+ enabled: agentIds.length > 0,
64
+ staleTime: Infinity,
65
+ initialPageParam: undefined,
66
+ getNextPageParam: (lastPage) => lastPage?.next,
67
+ queryFn: async ({ pageParam }) => {
68
+ const page = await client.agents.getMultiAgentMessages(channelName, {
69
+ agent_id: agentIds,
70
+ ...(typeof pageParam === "string" ? { before: pageParam } : {}),
71
+ ...(limit !== undefined ? { limit } : {}),
72
+ });
73
+ return page;
74
+ },
75
+ });
76
+ const messages = (0, react_1.useMemo)(() => (query.data?.pages ?? []).flatMap((p) => p.results), [query.data]);
77
+ return { ...query, messages };
78
+ }
@@ -0,0 +1,9 @@
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { MessageStructure } from "../types/common";
3
+ import type { ChannelIdentifier } from "../types/viewer";
4
+ /**
5
+ * Mutation wrapping `DooverDataProvider.sendMessage`. Posts a JSON message
6
+ * to the channel and resolves with the created `MessageStructure` (including
7
+ * its server-assigned `id` and `timestamp`).
8
+ */
9
+ export declare function useSendMessage(identifier: ChannelIdentifier): UseMutationResult<MessageStructure, Error, object>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSendMessage = useSendMessage;
4
+ const react_query_1 = require("@tanstack/react-query");
5
+ const context_1 = require("./context");
6
+ /**
7
+ * Mutation wrapping `DooverDataProvider.sendMessage`. Posts a JSON message
8
+ * to the channel and resolves with the created `MessageStructure` (including
9
+ * its server-assigned `id` and `timestamp`).
10
+ */
11
+ function useSendMessage(identifier) {
12
+ const client = (0, context_1.useDooverClient)();
13
+ return (0, react_query_1.useMutation)({
14
+ mutationFn: (data) => client.viewer.sendMessage(identifier, data),
15
+ });
16
+ }
@@ -0,0 +1,61 @@
1
+ import { type UseMutateAsyncFunction, type UseMutateFunction, type UseMutationResult } from "@tanstack/react-query";
2
+ import type { RpcStatus } from "../types/common";
3
+ import type { ChannelIdentifier } from "../types/viewer";
4
+ export type RpcCommandId = string;
5
+ export interface RpcStatusEvent<TPending = undefined> {
6
+ commandId: RpcCommandId;
7
+ status: RpcStatus<TPending>;
8
+ timestamp: number;
9
+ }
10
+ export interface SendRpcVariables<TRequest = object> {
11
+ commandId: RpcCommandId;
12
+ request: TRequest;
13
+ /** Optional per-call override of the app_key. */
14
+ app_key?: string;
15
+ }
16
+ interface CommandState<TPending, TResponse> {
17
+ current: RpcStatus<TPending>;
18
+ history: RpcStatusEvent<TPending>[];
19
+ submittedAt: number;
20
+ settledAt?: number;
21
+ data?: TResponse;
22
+ error?: unknown;
23
+ }
24
+ type CommandStatuses<TPending, TResponse> = Record<RpcCommandId, CommandState<TPending, TResponse>>;
25
+ export interface UseSendRpcOptions {
26
+ /** Method name passed on every RPC from this hook. */
27
+ method: string;
28
+ /** Default app_key. Overridable per-call via variables.app_key. */
29
+ app_key?: string;
30
+ /**
31
+ * react-query mutation key. Defaults to
32
+ * `["doover", "agent", agentId, "channel", channelName, "rpc", method]`.
33
+ */
34
+ mutationKey?: readonly unknown[];
35
+ }
36
+ export interface UseSendRpcResult<TRequest, TResponse, TPending> extends Omit<UseMutationResult<TResponse, unknown, SendRpcVariables<TRequest>, void>, "isPending" | "mutate" | "mutateAsync"> {
37
+ /** Most recent status across all commands (by `submittedAt`). */
38
+ currentStatus: RpcStatus<TPending> | undefined;
39
+ commandStatuses: CommandStatuses<TPending, TResponse>;
40
+ getStatus: (commandId: RpcCommandId) => RpcStatus<TPending> | undefined;
41
+ getHistory: (commandId: RpcCommandId) => RpcStatusEvent<TPending>[];
42
+ getCommandState: (commandId: RpcCommandId) => CommandState<TPending, TResponse> | undefined;
43
+ isPending: {
44
+ (): boolean;
45
+ (commandId: RpcCommandId): boolean;
46
+ };
47
+ mutate: UseMutateFunction<TResponse, unknown, SendRpcVariables<TRequest>, void>;
48
+ mutateAsync: UseMutateAsyncFunction<TResponse, unknown, SendRpcVariables<TRequest>, void>;
49
+ }
50
+ /**
51
+ * A react-query-integrated RPC hook. Calls
52
+ * `DooverDataProvider.sendRPC(identifier, { method, request, app_key })`
53
+ * under the hood and tracks a per-`commandId` status history so callers can
54
+ * render multi-step progress UIs and error popovers.
55
+ *
56
+ * For low-latency ephemeral commands (camera PTZ etc.) reach for
57
+ * `client.gateway.sendOneShotMessage` directly instead — those are not
58
+ * persisted and don't flow through this hook.
59
+ */
60
+ export declare function useSendRpc<TRequest = object, TResponse = object, TPending = undefined>(identifier: ChannelIdentifier, options: UseSendRpcOptions): UseSendRpcResult<TRequest, TResponse, TPending>;
61
+ export {};
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSendRpc = useSendRpc;
4
+ const react_1 = require("react");
5
+ const react_query_1 = require("@tanstack/react-query");
6
+ const context_1 = require("./context");
7
+ const SENT_STATUS = { code: "sent" };
8
+ function isTerminal(status) {
9
+ return status?.code === "success" || status?.code === "error";
10
+ }
11
+ function isPendingCode(status) {
12
+ return (status?.code === "sent" ||
13
+ status?.code === "acknowledged" ||
14
+ status?.code === "deferred" ||
15
+ status?.code === "pending");
16
+ }
17
+ function toErrorStatus(error) {
18
+ if (error instanceof Error)
19
+ return { code: "error", message: error.message };
20
+ if (typeof error === "string")
21
+ return { code: "error", message: error };
22
+ return { code: "error", message: "Unknown RPC error" };
23
+ }
24
+ /**
25
+ * A react-query-integrated RPC hook. Calls
26
+ * `DooverDataProvider.sendRPC(identifier, { method, request, app_key })`
27
+ * under the hood and tracks a per-`commandId` status history so callers can
28
+ * render multi-step progress UIs and error popovers.
29
+ *
30
+ * For low-latency ephemeral commands (camera PTZ etc.) reach for
31
+ * `client.gateway.sendOneShotMessage` directly instead — those are not
32
+ * persisted and don't flow through this hook.
33
+ */
34
+ function useSendRpc(identifier, options) {
35
+ const client = (0, context_1.useDooverClient)();
36
+ const [commandStatuses, setCommandStatuses] = (0, react_1.useState)({});
37
+ const mutationKey = options.mutationKey ??
38
+ [
39
+ "doover",
40
+ "agent",
41
+ identifier.agentId,
42
+ "channel",
43
+ identifier.channelName,
44
+ "rpc",
45
+ options.method,
46
+ ];
47
+ const appendStatus = (0, react_1.useCallback)((commandId, status) => {
48
+ const timestamp = Date.now();
49
+ setCommandStatuses((current) => {
50
+ const existing = current[commandId];
51
+ if (!existing || existing.settledAt !== undefined)
52
+ return current;
53
+ return {
54
+ ...current,
55
+ [commandId]: {
56
+ ...existing,
57
+ current: status,
58
+ history: [...existing.history, { commandId, status, timestamp }],
59
+ },
60
+ };
61
+ });
62
+ }, []);
63
+ const settleCommand = (0, react_1.useCallback)((commandId, updates) => {
64
+ setCommandStatuses((current) => {
65
+ const existing = current[commandId];
66
+ if (!existing)
67
+ return current;
68
+ const settledAt = Date.now();
69
+ const next = {
70
+ ...existing,
71
+ settledAt,
72
+ };
73
+ if ("data" in updates)
74
+ next.data = updates.data;
75
+ if ("error" in updates)
76
+ next.error = updates.error;
77
+ if (updates.fallbackStatus && !isTerminal(existing.current)) {
78
+ next.current = updates.fallbackStatus;
79
+ next.history = [
80
+ ...existing.history,
81
+ {
82
+ commandId,
83
+ status: updates.fallbackStatus,
84
+ timestamp: settledAt,
85
+ },
86
+ ];
87
+ }
88
+ return { ...current, [commandId]: next };
89
+ });
90
+ }, []);
91
+ const mutation = (0, react_query_1.useMutation)({
92
+ mutationKey: [...mutationKey],
93
+ mutationFn: async ({ commandId, request, app_key }) => {
94
+ const submittedAt = Date.now();
95
+ setCommandStatuses((current) => ({
96
+ ...current,
97
+ [commandId]: {
98
+ current: SENT_STATUS,
99
+ history: [
100
+ {
101
+ commandId,
102
+ status: SENT_STATUS,
103
+ timestamp: submittedAt,
104
+ },
105
+ ],
106
+ submittedAt,
107
+ },
108
+ }));
109
+ const rpcRequest = {
110
+ method: options.method,
111
+ request,
112
+ ...(app_key !== undefined
113
+ ? { app_key }
114
+ : options.app_key !== undefined
115
+ ? { app_key: options.app_key }
116
+ : {}),
117
+ };
118
+ return client.viewer.sendRPC(identifier, rpcRequest, {
119
+ onStatus: (status) => appendStatus(commandId, status),
120
+ });
121
+ },
122
+ onSuccess: (data, variables) => settleCommand(variables.commandId, {
123
+ data,
124
+ fallbackStatus: { code: "success" },
125
+ }),
126
+ onError: (error, variables) => settleCommand(variables.commandId, {
127
+ error,
128
+ fallbackStatus: toErrorStatus(error),
129
+ }),
130
+ });
131
+ const getCommandState = (0, react_1.useCallback)((commandId) => commandStatuses[commandId], [commandStatuses]);
132
+ const getStatus = (0, react_1.useCallback)((commandId) => getCommandState(commandId)?.current, [getCommandState]);
133
+ const getHistory = (0, react_1.useCallback)((commandId) => getCommandState(commandId)?.history ?? [], [getCommandState]);
134
+ const currentStatus = (0, react_1.useMemo)(() => {
135
+ const latest = Object.values(commandStatuses).reduce((acc, state) => {
136
+ if (!acc || state.submittedAt > acc.submittedAt)
137
+ return state;
138
+ return acc;
139
+ }, undefined);
140
+ return latest?.current;
141
+ }, [commandStatuses]);
142
+ const isPending = ((commandId) => {
143
+ if (commandId === undefined)
144
+ return mutation.isPending;
145
+ return isPendingCode(getStatus(commandId));
146
+ });
147
+ return {
148
+ ...mutation,
149
+ mutate: mutation.mutate,
150
+ mutateAsync: mutation.mutateAsync,
151
+ currentStatus,
152
+ commandStatuses,
153
+ getStatus,
154
+ getHistory,
155
+ getCommandState,
156
+ isPending,
157
+ };
158
+ }
@@ -0,0 +1,12 @@
1
+ import { type UseQueryResult } from "@tanstack/react-query";
2
+ import type { TurnCredential } from "../types/common";
3
+ import type { TurnTokenRequest } from "../types/openapi";
4
+ /**
5
+ * Fetch TURN (relay) credentials for a camera stream. Cached for 10 minutes
6
+ * by default since the token has a server-controlled TTL; consumers that
7
+ * need refresh-on-expiry should do so at the call site.
8
+ */
9
+ export declare function useTurnCredentials(request: TurnTokenRequest, options?: {
10
+ enabled?: boolean;
11
+ staleTime?: number;
12
+ }): UseQueryResult<TurnCredential>;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useTurnCredentials = useTurnCredentials;
4
+ const react_query_1 = require("@tanstack/react-query");
5
+ const context_1 = require("./context");
6
+ /**
7
+ * Fetch TURN (relay) credentials for a camera stream. Cached for 10 minutes
8
+ * by default since the token has a server-controlled TTL; consumers that
9
+ * need refresh-on-expiry should do so at the call site.
10
+ */
11
+ function useTurnCredentials(request, options) {
12
+ const client = (0, context_1.useDooverClient)();
13
+ return (0, react_query_1.useQuery)({
14
+ queryKey: [
15
+ "doover",
16
+ "turn",
17
+ request.role,
18
+ request.device_id ?? null,
19
+ request.camera_name,
20
+ ],
21
+ enabled: options?.enabled ?? true,
22
+ staleTime: options?.staleTime ?? 10 * 60 * 1000,
23
+ queryFn: () => client.turn.createTurnToken(request),
24
+ });
25
+ }
@@ -0,0 +1,14 @@
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { Aggregate } from "../types/common";
3
+ import type { ChannelIdentifier } from "../types/viewer";
4
+ export interface UseUpdateAggregateOptions {
5
+ /** If true, use PUT semantics (replace) instead of PATCH (merge). */
6
+ replace?: boolean;
7
+ }
8
+ /**
9
+ * Mutation wrapping `DooverDataProvider.updateAggregate` (PATCH) or
10
+ * `putAggregate` (PUT when `replace: true`). Also writes the returned
11
+ * aggregate into the react-query cache for its channel so consumers of
12
+ * `useChannelAggregate` see the change immediately.
13
+ */
14
+ export declare function useUpdateAggregate(identifier: ChannelIdentifier, options?: UseUpdateAggregateOptions): UseMutationResult<Aggregate, Error, object>;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useUpdateAggregate = useUpdateAggregate;
4
+ const react_query_1 = require("@tanstack/react-query");
5
+ const context_1 = require("./context");
6
+ const useChannelAggregate_1 = require("./useChannelAggregate");
7
+ /**
8
+ * Mutation wrapping `DooverDataProvider.updateAggregate` (PATCH) or
9
+ * `putAggregate` (PUT when `replace: true`). Also writes the returned
10
+ * aggregate into the react-query cache for its channel so consumers of
11
+ * `useChannelAggregate` see the change immediately.
12
+ */
13
+ function useUpdateAggregate(identifier, options) {
14
+ const client = (0, context_1.useDooverClient)();
15
+ const queryClient = (0, react_query_1.useQueryClient)();
16
+ const replace = options?.replace ?? false;
17
+ return (0, react_query_1.useMutation)({
18
+ mutationFn: (data) => replace
19
+ ? client.viewer.putAggregate(identifier, data)
20
+ : client.viewer.updateAggregate(identifier, data),
21
+ onSuccess: (aggregate) => {
22
+ queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(identifier.agentId, identifier.channelName), aggregate);
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,17 @@
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { MessageBody, MessageMutationParams } from "../apis/messages-api";
3
+ import type { ChannelIdentifier } from "../types/viewer";
4
+ export interface UseUpdateMessageOptions {
5
+ /** If true, use PUT (replace) instead of PATCH (merge). Default PATCH. */
6
+ replace?: boolean;
7
+ params?: MessageMutationParams;
8
+ }
9
+ export interface UpdateMessageVariables {
10
+ messageId: string;
11
+ body: MessageBody;
12
+ }
13
+ /**
14
+ * Mutation wrapping `MessagesApi.patchMessage` or `putMessage`. Bound to a
15
+ * single channel; the message id and body are passed per-call.
16
+ */
17
+ export declare function useUpdateMessage(identifier: ChannelIdentifier, options?: UseUpdateMessageOptions): UseMutationResult<unknown, Error, UpdateMessageVariables>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useUpdateMessage = useUpdateMessage;
4
+ const react_query_1 = require("@tanstack/react-query");
5
+ const context_1 = require("./context");
6
+ /**
7
+ * Mutation wrapping `MessagesApi.patchMessage` or `putMessage`. Bound to a
8
+ * single channel; the message id and body are passed per-call.
9
+ */
10
+ function useUpdateMessage(identifier, options) {
11
+ const client = (0, context_1.useDooverClient)();
12
+ const replace = options?.replace ?? false;
13
+ const params = options?.params;
14
+ return (0, react_query_1.useMutation)({
15
+ mutationFn: ({ messageId, body }) => {
16
+ if (!identifier.agentId || !identifier.channelName) {
17
+ throw new Error("useUpdateMessage requires both agentId and channelName on the identifier.");
18
+ }
19
+ return replace
20
+ ? client.messages.putMessage(identifier.agentId, identifier.channelName, messageId, body, params)
21
+ : client.messages.patchMessage(identifier.agentId, identifier.channelName, messageId, body, params);
22
+ },
23
+ });
24
+ }
@@ -177,6 +177,12 @@ function setupRest(responseFactory) {
177
177
  await connections.getAgentSubscriptionHistory("a1", { channel: "{\"agent_id\":\"a1\",\"name\":\"c1\"}" });
178
178
  await connections.getConnection("conn");
179
179
  await connections.getChannelSubscriptions("a1", "c1");
180
+ await connections.syncConnection("a1");
181
+ const syncCall = fetchMock
182
+ .getCalls()
183
+ .find((call) => call.args[0].endsWith("/agents/a1/connection_sync"));
184
+ (0, chai_1.expect)(syncCall).to.not.equal(undefined);
185
+ (0, chai_1.expect)(syncCall.args[1].method).to.equal("POST");
180
186
  await notifications.getAgentNotifications("a1");
181
187
  await notifications.getAgentNotificationEndpoints("a1", "browser");
182
188
  await notifications.createNotificationEndpoint("a1", {
@@ -207,7 +213,7 @@ function setupRest(responseFactory) {
207
213
  expires_at: 1,
208
214
  });
209
215
  await notifications.getWebPushPublicKey();
210
- (0, chai_1.expect)(fetchMock.callCount).to.equal(20);
216
+ (0, chai_1.expect)(fetchMock.callCount).to.equal(21);
211
217
  });
212
218
  (0, mocha_1.it)("covers permissions, processors, and turn methods", async () => {
213
219
  const { rest, fetchMock } = setupRest(() => (0, helpers_1.createJsonResponse)({ db: { agent_id: "a1", is_superuser: false, resources: [] } }));
@@ -26,4 +26,17 @@ const helpers_1 = require("./helpers");
26
26
  (0, chai_1.expect)(client.agents).to.exist;
27
27
  (0, chai_1.expect)(client.gateway).to.exist;
28
28
  });
29
+ (0, mocha_1.it)("shares a single GatewayClient between client.gateway and client.viewer.gateway", () => {
30
+ const client = new doover_client_1.DooverClient({
31
+ dataRestUrl: "https://api.example.com",
32
+ controlApiUrl: "https://control.example.com",
33
+ dataWssUrl: "wss://ws.example.com",
34
+ fetchImpl: (0, helpers_1.createFetchMock)(),
35
+ webSocketImpl: helpers_1.MockWebSocket,
36
+ disableBrowserLifecycleHooks: true,
37
+ });
38
+ // Regression: these used to be separate GatewayClient instances,
39
+ // each opening its own WebSocket on connect().
40
+ (0, chai_1.expect)(client.gateway).to.equal(client.viewer.gateway);
41
+ });
29
42
  });