doover-js 0.2.0-alpha.0 → 0.3.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 +36 -0
  29. package/dist/react/useChannelMessages.js +59 -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 +97 -0
  36. package/dist/react/useMultiAgentChannelMessages.d.ts +25 -0
  37. package/dist/react/useMultiAgentChannelMessages.js +81 -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 +21 -0
  45. package/dist/react/useUpdateAggregate.js +26 -0
  46. package/dist/react/useUpdateMessage.d.ts +33 -0
  47. package/dist/react/useUpdateMessage.js +30 -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 +35 -7
  58. package/dist/viewer/doover-data-provider.js +96 -9
  59. package/package.json +28 -2
@@ -0,0 +1,97 @@
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
+ const useChannelAggregate_1 = require("./useChannelAggregate");
9
+ function multiAgentAggregatesQueryKey(channelName, agentIds) {
10
+ return [
11
+ "doover",
12
+ "channel",
13
+ channelName,
14
+ "aggregates",
15
+ [...agentIds].sort().join(","),
16
+ ];
17
+ }
18
+ /**
19
+ * Batch-fetch aggregates for a channel across many agents, then keep each
20
+ * agent's slot live via its own gateway subscription. Reconciles per-agent
21
+ * `channelSync`/`aggregateUpdate` events into the batched query cache.
22
+ */
23
+ function useMultiAgentAggregates(channelName, agentIds, options) {
24
+ const client = (0, context_1.useDooverClient)();
25
+ const queryClient = (0, react_query_1.useQueryClient)();
26
+ const liveUpdates = options?.liveUpdates ?? true;
27
+ const key = multiAgentAggregatesQueryKey(channelName, agentIds);
28
+ const query = (0, react_query_1.useQuery)({
29
+ queryKey: key,
30
+ enabled: agentIds.length > 0,
31
+ staleTime: Infinity,
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
+ },
44
+ });
45
+ const patchAgentAggregate = (0, react_1.useCallback)((agentId, aggregate) => {
46
+ queryClient.setQueryData(key, (current) => {
47
+ if (!current)
48
+ return current;
49
+ const idx = current.results.findIndex((r) => r.agent_id === agentId);
50
+ const next = {
51
+ agent_id: agentId,
52
+ ...aggregate,
53
+ };
54
+ const results = idx === -1
55
+ ? [...current.results, next]
56
+ : current.results.map((r, i) => (i === idx ? next : r));
57
+ return { ...current, results };
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);
62
+ },
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ [queryClient, channelName, agentIds.join(",")]);
65
+ (0, react_1.useEffect)(() => {
66
+ if (!liveUpdates || agentIds.length === 0)
67
+ return;
68
+ const noopMessage = () => { };
69
+ const subscriptions = agentIds.map((agentId) => {
70
+ const identifier = { agentId, channelName };
71
+ const messageCallback = noopMessage;
72
+ const aggregateCallback = (_id, aggregate) => {
73
+ patchAgentAggregate(agentId, aggregate);
74
+ };
75
+ void client.viewer.subscribeToChannel(identifier, messageCallback, aggregateCallback);
76
+ return { identifier, messageCallback };
77
+ });
78
+ return () => {
79
+ for (const { identifier, messageCallback } of subscriptions) {
80
+ client.viewer
81
+ .unsubscribeFromChannel(identifier, messageCallback)
82
+ .catch(() => { });
83
+ }
84
+ };
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ }, [client, channelName, liveUpdates, agentIds.join(",")]);
87
+ const aggregatesByAgent = (0, react_1.useMemo)(() => {
88
+ const map = {};
89
+ for (const entry of query.data?.results ?? []) {
90
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
91
+ const { agent_id, ...aggregate } = entry;
92
+ map[agent_id] = aggregate;
93
+ }
94
+ return map;
95
+ }, [query.data]);
96
+ return { aggregatesByAgent, query };
97
+ }
@@ -0,0 +1,25 @@
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
+ * 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;
15
+ }
16
+ interface Page {
17
+ results: MessageStructure[];
18
+ next?: string;
19
+ }
20
+ export interface UseMultiAgentChannelMessagesResult extends Omit<UseInfiniteQueryResult<InfiniteData<Page>>, "data"> {
21
+ messages: MessageStructure[];
22
+ data: InfiniteData<Page> | undefined;
23
+ }
24
+ export declare function useMultiAgentChannelMessages(channelName: string, agentIds: string[], options?: UseMultiAgentChannelMessagesOptions): UseMultiAgentChannelMessagesResult;
25
+ export {};
@@ -0,0 +1,81 @@
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 fields = options?.fields;
23
+ const initialBefore = options?.initialBefore;
24
+ const key = multiAgentChannelMessagesQueryKey(channelName, agentIds);
25
+ const prependMessage = (0, react_1.useCallback)((message) => {
26
+ queryClient.setQueryData(key, (current) => {
27
+ if (!current)
28
+ return current;
29
+ const [firstPage, ...rest] = current.pages;
30
+ const firstResults = firstPage?.results ?? [];
31
+ return {
32
+ ...current,
33
+ pages: [
34
+ { ...firstPage, results: [message, ...firstResults] },
35
+ ...rest,
36
+ ],
37
+ };
38
+ });
39
+ },
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ [queryClient, channelName, agentIds.join(",")]);
42
+ (0, react_1.useEffect)(() => {
43
+ if (!liveUpdates || agentIds.length === 0)
44
+ return;
45
+ const subscriptions = agentIds.map((agentId) => {
46
+ const identifier = { agentId, channelName };
47
+ const messageCallback = (_id, message) => {
48
+ prependMessage(message);
49
+ };
50
+ const aggregateCallback = () => { };
51
+ void client.viewer.subscribeToChannel(identifier, messageCallback, aggregateCallback);
52
+ return { identifier, messageCallback };
53
+ });
54
+ return () => {
55
+ for (const { identifier, messageCallback } of subscriptions) {
56
+ client.viewer
57
+ .unsubscribeFromChannel(identifier, messageCallback)
58
+ .catch(() => { });
59
+ }
60
+ };
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ }, [client, channelName, liveUpdates, agentIds.join(",")]);
63
+ const query = (0, react_query_1.useInfiniteQuery)({
64
+ queryKey: key,
65
+ enabled: agentIds.length > 0,
66
+ staleTime: Infinity,
67
+ initialPageParam: initialBefore,
68
+ getNextPageParam: (lastPage) => lastPage?.next,
69
+ queryFn: async ({ pageParam }) => {
70
+ const page = await client.agents.getMultiAgentMessages(channelName, {
71
+ agent_id: agentIds,
72
+ ...(typeof pageParam === "string" ? { before: pageParam } : {}),
73
+ ...(limit !== undefined ? { limit } : {}),
74
+ ...(fields && fields.length > 0 ? { field_name: fields } : {}),
75
+ });
76
+ return page;
77
+ },
78
+ });
79
+ const messages = (0, react_1.useMemo)(() => (query.data?.pages ?? []).flatMap((p) => p.results), [query.data]);
80
+ return { ...query, messages };
81
+ }
@@ -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,21 @@
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { AggregateMutationParams } from "../apis/aggregates-api";
3
+ import type { Aggregate } from "../types/common";
4
+ import type { ChannelIdentifier } from "../types/viewer";
5
+ export interface UseUpdateAggregateOptions {
6
+ /** If true, use PUT semantics (replace) instead of PATCH (merge). */
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;
14
+ }
15
+ /**
16
+ * Mutation wrapping `DooverDataProvider.updateAggregate` (PATCH) or
17
+ * `putAggregate` (PUT when `replace: true`). Also writes the returned
18
+ * aggregate into the react-query cache for its channel so consumers of
19
+ * `useChannelAggregate` see the change immediately.
20
+ */
21
+ export declare function useUpdateAggregate(identifier: ChannelIdentifier, options?: UseUpdateAggregateOptions): UseMutationResult<Aggregate, Error, object>;
@@ -0,0 +1,26 @@
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
+ const params = options?.params;
18
+ return (0, react_query_1.useMutation)({
19
+ mutationFn: (data) => replace
20
+ ? client.viewer.putAggregate(identifier, data, params)
21
+ : client.viewer.updateAggregate(identifier, data, params),
22
+ onSuccess: (aggregate) => {
23
+ queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(identifier.agentId, identifier.channelName), aggregate);
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,33 @@
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 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
+ */
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
+ */
14
+ params?: MessageMutationParams;
15
+ }
16
+ export interface UseUpdateMessageOptions extends UpdateMessageCallOptions {
17
+ }
18
+ export interface UpdateMessageVariables {
19
+ messageId: string;
20
+ body: MessageBody;
21
+ /**
22
+ * Per-call overrides. Extensible — add new knobs here without breaking
23
+ * the variables shape.
24
+ */
25
+ options?: UpdateMessageCallOptions;
26
+ }
27
+ /**
28
+ * Mutation wrapping `MessagesApi.patchMessage` or `putMessage`. Bound to a
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.
32
+ */
33
+ export declare function useUpdateMessage(identifier: ChannelIdentifier, options?: UseUpdateMessageOptions): UseMutationResult<unknown, Error, UpdateMessageVariables>;
@@ -0,0 +1,30 @@
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. Each
9
+ * invocation can override `replace` / `params` via `variables.options` for
10
+ * mutation sites that sometimes PUT and sometimes PATCH the same channel.
11
+ */
12
+ function useUpdateMessage(identifier, options) {
13
+ const client = (0, context_1.useDooverClient)();
14
+ const defaultReplace = options?.replace ?? false;
15
+ const defaultParams = options?.params;
16
+ return (0, react_query_1.useMutation)({
17
+ mutationFn: ({ messageId, body, options: call }) => {
18
+ if (!identifier.agentId || !identifier.channelName) {
19
+ throw new Error("useUpdateMessage requires both agentId and channelName on the identifier.");
20
+ }
21
+ const replace = call?.replace ?? defaultReplace;
22
+ const params = call?.params || defaultParams
23
+ ? { ...defaultParams, ...call?.params }
24
+ : undefined;
25
+ return replace
26
+ ? client.messages.putMessage(identifier.agentId, identifier.channelName, messageId, body, params)
27
+ : client.messages.patchMessage(identifier.agentId, identifier.channelName, messageId, body, params);
28
+ },
29
+ });
30
+ }
@@ -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
  });