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.
- package/dist/apis/connections-api.d.ts +6 -0
- package/dist/apis/connections-api.js +8 -0
- package/dist/client/doover-client.d.ts +13 -0
- package/dist/client/doover-client.js +30 -2
- package/dist/client/singleton.d.ts +26 -0
- package/dist/client/singleton.js +59 -0
- package/dist/client/stats.d.ts +55 -0
- package/dist/client/stats.js +97 -0
- package/dist/gateway/gateway-client.d.ts +30 -6
- package/dist/gateway/gateway-client.js +125 -46
- package/dist/gateway/types.d.ts +0 -1
- package/dist/http/rest-client.d.ts +11 -0
- package/dist/http/rest-client.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -1
- package/dist/react/context.d.ts +19 -0
- package/dist/react/context.js +28 -0
- package/dist/react/index.d.ts +24 -0
- package/dist/react/index.js +40 -0
- package/dist/react/sharedQueryClient.d.ts +21 -0
- package/dist/react/sharedQueryClient.js +39 -0
- package/dist/react/useAgentChannel.d.ts +7 -0
- package/dist/react/useAgentChannel.js +12 -0
- package/dist/react/useAgentConnections.d.ts +8 -0
- package/dist/react/useAgentConnections.js +24 -0
- package/dist/react/useChannelAggregate.d.ts +13 -0
- package/dist/react/useChannelAggregate.js +38 -0
- package/dist/react/useChannelMessages.d.ts +36 -0
- package/dist/react/useChannelMessages.js +59 -0
- package/dist/react/useChannelSubscription.d.ts +16 -0
- package/dist/react/useChannelSubscription.js +43 -0
- package/dist/react/useConnectionState.d.ts +21 -0
- package/dist/react/useConnectionState.js +43 -0
- package/dist/react/useMultiAgentAggregates.d.ts +22 -0
- package/dist/react/useMultiAgentAggregates.js +97 -0
- package/dist/react/useMultiAgentChannelMessages.d.ts +25 -0
- package/dist/react/useMultiAgentChannelMessages.js +81 -0
- package/dist/react/useSendMessage.d.ts +9 -0
- package/dist/react/useSendMessage.js +16 -0
- package/dist/react/useSendRpc.d.ts +61 -0
- package/dist/react/useSendRpc.js +158 -0
- package/dist/react/useTurnCredentials.d.ts +12 -0
- package/dist/react/useTurnCredentials.js +25 -0
- package/dist/react/useUpdateAggregate.d.ts +21 -0
- package/dist/react/useUpdateAggregate.js +26 -0
- package/dist/react/useUpdateMessage.d.ts +33 -0
- package/dist/react/useUpdateMessage.js +30 -0
- package/dist/test/apis.test.js +7 -1
- package/dist/test/doover-client.test.js +13 -0
- package/dist/test/doover-data-provider.test.js +155 -0
- package/dist/test/gateway-client.test.js +122 -8
- package/dist/test/react.test.d.ts +1 -0
- package/dist/test/react.test.js +353 -0
- package/dist/test/singleton.test.d.ts +1 -0
- package/dist/test/singleton.test.js +62 -0
- package/dist/types/common.d.ts +40 -0
- package/dist/viewer/doover-data-provider.d.ts +35 -7
- package/dist/viewer/doover-data-provider.js +96 -9
- 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
|
+
}
|
package/dist/test/apis.test.js
CHANGED
|
@@ -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(
|
|
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
|
});
|