doover-js 0.4.7 → 0.5.0-alpha.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 (63) hide show
  1. package/README.md +31 -0
  2. package/dist/apis/_args.d.ts +16 -0
  3. package/dist/apis/_args.js +26 -0
  4. package/dist/apis/agents-api.d.ts +4 -1
  5. package/dist/apis/agents-api.js +139 -1
  6. package/dist/apis/aggregates-api.d.ts +23 -3
  7. package/dist/apis/aggregates-api.js +33 -4
  8. package/dist/apis/alarms-api.d.ts +30 -0
  9. package/dist/apis/alarms-api.js +51 -6
  10. package/dist/apis/channels-api.d.ts +40 -3
  11. package/dist/apis/channels-api.js +50 -5
  12. package/dist/apis/connections-api.d.ts +21 -5
  13. package/dist/apis/connections-api.js +26 -10
  14. package/dist/apis/messages-api.d.ts +55 -9
  15. package/dist/apis/messages-api.js +96 -11
  16. package/dist/apis/notifications-api.d.ts +52 -0
  17. package/dist/apis/notifications-api.js +99 -13
  18. package/dist/apis/permissions-api.d.ts +8 -0
  19. package/dist/apis/permissions-api.js +11 -2
  20. package/dist/apis/processors-api.d.ts +35 -0
  21. package/dist/apis/processors-api.js +64 -8
  22. package/dist/apis/users-api.d.ts +8 -0
  23. package/dist/apis/users-api.js +13 -0
  24. package/dist/client/doover-client.d.ts +4 -8
  25. package/dist/client/doover-client.js +16 -28
  26. package/dist/client/stats.d.ts +24 -0
  27. package/dist/client/stats.js +62 -0
  28. package/dist/gateway/gateway-client.d.ts +13 -1
  29. package/dist/gateway/gateway-client.js +54 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +13 -7
  32. package/dist/react/index.d.ts +2 -0
  33. package/dist/react/index.js +4 -1
  34. package/dist/react/useChannelAggregate.js +8 -1
  35. package/dist/react/useChannelMessages.js +5 -2
  36. package/dist/react/useChannelSubscription.js +8 -18
  37. package/dist/react/useInvocationLogs.d.ts +23 -0
  38. package/dist/react/useInvocationLogs.js +41 -0
  39. package/dist/react/useMultiAgentAggregates.js +8 -14
  40. package/dist/react/useMultiAgentChannelMessages.js +8 -13
  41. package/dist/react/useSendMessage.js +6 -1
  42. package/dist/react/useSendRpc.js +4 -1
  43. package/dist/react/useUpdateAggregate.js +9 -3
  44. package/dist/rpc/errors.d.ts +10 -0
  45. package/dist/rpc/errors.js +23 -0
  46. package/dist/rpc/rpc-dispatcher.d.ts +28 -0
  47. package/dist/rpc/rpc-dispatcher.js +129 -0
  48. package/dist/test/api-args.test.d.ts +1 -0
  49. package/dist/test/api-args.test.js +48 -0
  50. package/dist/test/api-overloads.test.d.ts +9 -0
  51. package/dist/test/api-overloads.test.js +276 -0
  52. package/dist/test/gateway-subscriptions.test.d.ts +1 -0
  53. package/dist/test/gateway-subscriptions.test.js +95 -0
  54. package/dist/test/react.test.js +36 -0
  55. package/dist/test/rpc-dispatcher.test.d.ts +1 -0
  56. package/dist/test/rpc-dispatcher.test.js +190 -0
  57. package/dist/test/stats-rpc.test.d.ts +1 -0
  58. package/dist/test/stats-rpc.test.js +69 -0
  59. package/dist/test/users-api.test.d.ts +1 -0
  60. package/dist/test/users-api.test.js +19 -0
  61. package/dist/viewer/doover-data-provider.d.ts +30 -34
  62. package/dist/viewer/doover-data-provider.js +82 -316
  63. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import { type UseQueryResult } from "@tanstack/react-query";
2
+ import type { ChannelIdentifier } from "../types/viewer";
3
+ export declare function invocationLogsQueryKey(agentId: string | undefined, channelName: string | undefined, messageId: string | undefined): readonly ["doover", "agent", string | undefined, "channel", string | undefined, "message", string | undefined, "logs"];
4
+ export interface UseInvocationLogsOptions {
5
+ /** Defaults to true. Set false to skip the query (e.g. dialog closed). */
6
+ enabled?: boolean;
7
+ /**
8
+ * react-query retry behaviour. Defaults to false — the logs endpoint
9
+ * commonly 404s while logs are still materialising server-side, and a
10
+ * manual refetch button is friendlier than silent retry.
11
+ */
12
+ retry?: boolean | number;
13
+ }
14
+ /**
15
+ * Fetch the log entries for a single invocation message. The `/logs`
16
+ * endpoint is only meaningful for invocation messages (channel name like
17
+ * `dv-proc-inv-…`); other message types return 404.
18
+ *
19
+ * Generic on `TLog` — the shape of a single log entry. Defaults to `unknown`
20
+ * because log payloads are domain-specific (lambda telemetry, app-level
21
+ * structured logs, etc.) and doover-js does not own the canonical shape.
22
+ */
23
+ export declare function useInvocationLogs<TLog = unknown>(identifier: ChannelIdentifier, messageId: string | undefined, options?: UseInvocationLogsOptions): UseQueryResult<TLog[]>;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.invocationLogsQueryKey = invocationLogsQueryKey;
4
+ exports.useInvocationLogs = useInvocationLogs;
5
+ const react_query_1 = require("@tanstack/react-query");
6
+ const context_1 = require("./context");
7
+ function invocationLogsQueryKey(agentId, channelName, messageId) {
8
+ return [
9
+ "doover",
10
+ "agent",
11
+ agentId,
12
+ "channel",
13
+ channelName,
14
+ "message",
15
+ messageId,
16
+ "logs",
17
+ ];
18
+ }
19
+ /**
20
+ * Fetch the log entries for a single invocation message. The `/logs`
21
+ * endpoint is only meaningful for invocation messages (channel name like
22
+ * `dv-proc-inv-…`); other message types return 404.
23
+ *
24
+ * Generic on `TLog` — the shape of a single log entry. Defaults to `unknown`
25
+ * because log payloads are domain-specific (lambda telemetry, app-level
26
+ * structured logs, etc.) and doover-js does not own the canonical shape.
27
+ */
28
+ function useInvocationLogs(identifier, messageId, options) {
29
+ const client = (0, context_1.useDooverClient)();
30
+ const { agentId, channelName } = identifier;
31
+ return (0, react_query_1.useQuery)({
32
+ queryKey: invocationLogsQueryKey(agentId, channelName, messageId),
33
+ queryFn: () => client.messages.getInvocationLogs(agentId, channelName, messageId),
34
+ enabled: !!agentId &&
35
+ !!channelName &&
36
+ !!messageId &&
37
+ (options?.enabled ?? true),
38
+ retry: options?.retry ?? false,
39
+ staleTime: Infinity,
40
+ });
41
+ }
@@ -65,22 +65,16 @@ function useMultiAgentAggregates(channelName, agentIds, options) {
65
65
  (0, react_1.useEffect)(() => {
66
66
  if (!liveUpdates || agentIds.length === 0)
67
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 };
68
+ const offs = agentIds.map((agentId) => {
69
+ const channel = { agent_id: agentId, name: channelName };
70
+ return client.gateway.subscribeToChannel(channel, {
71
+ onAggregate: (agg) => patchAgentAggregate(agentId, agg),
72
+ });
77
73
  });
74
+ void client.gateway.connect();
78
75
  return () => {
79
- for (const { identifier, messageCallback } of subscriptions) {
80
- client.viewer
81
- .unsubscribeFromChannel(identifier, messageCallback)
82
- .catch(() => { });
83
- }
76
+ for (const off of offs)
77
+ off();
84
78
  };
85
79
  // eslint-disable-next-line react-hooks/exhaustive-deps
86
80
  }, [client, channelName, liveUpdates, agentIds.join(",")]);
@@ -43,21 +43,16 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
43
43
  (0, react_1.useEffect)(() => {
44
44
  if (!liveUpdates || agentIds.length === 0)
45
45
  return;
46
- const subscriptions = agentIds.map((agentId) => {
47
- const identifier = { agentId, channelName };
48
- const messageCallback = (_id, message) => {
49
- prependMessage(message);
50
- };
51
- const aggregateCallback = () => { };
52
- void client.viewer.subscribeToChannel(identifier, messageCallback, aggregateCallback);
53
- return { identifier, messageCallback };
46
+ const offs = agentIds.map((agentId) => {
47
+ const channel = { agent_id: agentId, name: channelName };
48
+ return client.gateway.subscribeToChannel(channel, {
49
+ onMessage: (msg) => prependMessage(msg),
50
+ });
54
51
  });
52
+ void client.gateway.connect();
55
53
  return () => {
56
- for (const { identifier, messageCallback } of subscriptions) {
57
- client.viewer
58
- .unsubscribeFromChannel(identifier, messageCallback)
59
- .catch(() => { });
60
- }
54
+ for (const off of offs)
55
+ off();
61
56
  };
62
57
  // eslint-disable-next-line react-hooks/exhaustive-deps
63
58
  }, [client, channelName, liveUpdates, agentIds.join(",")]);
@@ -11,6 +11,11 @@ const context_1 = require("./context");
11
11
  function useSendMessage(identifier) {
12
12
  const client = (0, context_1.useDooverClient)();
13
13
  return (0, react_query_1.useMutation)({
14
- mutationFn: (data) => client.viewer.sendMessage(identifier, data),
14
+ mutationFn: (data) => {
15
+ if (!identifier.agentId || !identifier.channelName) {
16
+ return Promise.reject(new Error("Identifier must include agentId and channelName"));
17
+ }
18
+ return client.messages.postMessage({ agentId: identifier.agentId, channelName: identifier.channelName }, { data: data });
19
+ },
15
20
  });
16
21
  }
@@ -115,7 +115,10 @@ function useSendRpc(identifier, options) {
115
115
  ? { app_key: options.app_key }
116
116
  : {}),
117
117
  };
118
- return client.viewer.sendRPC(identifier, rpcRequest, {
118
+ if (!identifier.agentId || !identifier.channelName) {
119
+ throw new Error("Identifier must include agentId and channelName");
120
+ }
121
+ return client.rpc.send({ agentId: identifier.agentId, channelName: identifier.channelName }, rpcRequest, {
119
122
  onStatus: (status) => appendStatus(commandId, status),
120
123
  });
121
124
  },
@@ -16,9 +16,15 @@ function useUpdateAggregate(identifier, options) {
16
16
  const replace = options?.replace ?? false;
17
17
  const params = options?.params;
18
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)),
19
+ mutationFn: (data) => {
20
+ if (!identifier.agentId || !identifier.channelName) {
21
+ return Promise.reject(new Error("Identifier must include agentId and channelName"));
22
+ }
23
+ const id = { agentId: identifier.agentId, channelName: identifier.channelName };
24
+ return (replace
25
+ ? client.aggregates.putAggregate(id, data, params)
26
+ : client.aggregates.patchAggregate(id, data, params));
27
+ },
22
28
  onSuccess: (aggregate) => {
23
29
  queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(identifier.agentId, identifier.channelName), aggregate);
24
30
  },
@@ -0,0 +1,10 @@
1
+ import type { RpcRequest, RpcStatus } from "../types/common";
2
+ export declare class DooverRpcError extends Error {
3
+ readonly status: RpcStatus<unknown> & {
4
+ code: "error";
5
+ };
6
+ readonly request: RpcRequest;
7
+ constructor(status: RpcStatus<unknown> & {
8
+ code: "error";
9
+ }, request: RpcRequest);
10
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DooverRpcError = void 0;
4
+ class DooverRpcError extends Error {
5
+ constructor(status, request) {
6
+ const text = typeof status.message === "string"
7
+ ? status.message
8
+ : (() => {
9
+ try {
10
+ return JSON.stringify(status.message);
11
+ }
12
+ catch {
13
+ return "Unknown RPC error";
14
+ }
15
+ })();
16
+ super(text);
17
+ this.name = "DooverRpcError";
18
+ this.status = status;
19
+ this.request = request;
20
+ Object.setPrototypeOf(this, DooverRpcError.prototype);
21
+ }
22
+ }
23
+ exports.DooverRpcError = DooverRpcError;
@@ -0,0 +1,28 @@
1
+ import type { MessagesApi } from "../apis/messages-api";
2
+ import type { DooverStatsCollector } from "../client/stats";
3
+ import type { GatewayClient } from "../gateway/gateway-client";
4
+ import type { RpcRequest, RpcStatus } from "../types/common";
5
+ export interface SendRpcOptions<TPending = undefined> {
6
+ onStatus?: (status: RpcStatus<TPending>) => void;
7
+ signal?: AbortSignal;
8
+ timeoutMs?: number;
9
+ }
10
+ interface ChannelIdentifierLike {
11
+ agentId: string;
12
+ channelName: string;
13
+ }
14
+ export declare class RpcDispatcher {
15
+ private readonly gateway;
16
+ private readonly messages;
17
+ private pending;
18
+ private channelRefs;
19
+ private stats;
20
+ constructor(gateway: GatewayClient, messages: MessagesApi);
21
+ setStats(collector: DooverStatsCollector | null): void;
22
+ send<TReq = object, TRes = object, TPending = undefined>(channel: ChannelIdentifierLike, request: RpcRequest<TReq>, options?: SendRpcOptions<TPending>): Promise<TRes>;
23
+ private acquireChannel;
24
+ private releaseChannel;
25
+ private route;
26
+ private settle;
27
+ }
28
+ export {};
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RpcDispatcher = void 0;
4
+ const errors_1 = require("./errors");
5
+ function isRpcMessageData(data) {
6
+ return (typeof data === "object" &&
7
+ data !== null &&
8
+ "status" in data &&
9
+ "method" in data &&
10
+ "request" in data);
11
+ }
12
+ class RpcDispatcher {
13
+ constructor(gateway, messages) {
14
+ this.gateway = gateway;
15
+ this.messages = messages;
16
+ this.pending = new Map();
17
+ this.channelRefs = new Map();
18
+ this.stats = null;
19
+ }
20
+ setStats(collector) {
21
+ this.stats = collector;
22
+ }
23
+ send(channel, request, options) {
24
+ return new Promise((resolve, reject) => {
25
+ const channelRef = {
26
+ agent_id: channel.agentId,
27
+ name: channel.channelName,
28
+ };
29
+ const channelKey = `${channelRef.agent_id}/${channelRef.name}`;
30
+ this.acquireChannel(channelKey, channelRef);
31
+ const startedAt = this.stats?.recordRpcStart() ?? null;
32
+ this.messages
33
+ .postMessage(channel.agentId, channel.channelName, {
34
+ type: "rpc",
35
+ ...request,
36
+ })
37
+ .then((message) => {
38
+ const pending = {
39
+ resolve: resolve,
40
+ reject,
41
+ onStatus: options?.onStatus,
42
+ request: request,
43
+ channelKey,
44
+ channel: channelRef,
45
+ startedAt,
46
+ };
47
+ if (options?.signal) {
48
+ const onAbort = () => this.settle(message.id, "abort", undefined, options.signal?.reason);
49
+ if (options.signal.aborted) {
50
+ this.releaseChannel(channelKey);
51
+ this.stats?.recordRpcEnd(startedAt, "abort");
52
+ reject(options.signal.reason ?? new Error("Aborted"));
53
+ return;
54
+ }
55
+ options.signal.addEventListener("abort", onAbort);
56
+ pending.signal = options.signal;
57
+ pending.abortListener = onAbort;
58
+ }
59
+ if (options?.timeoutMs !== undefined) {
60
+ pending.timer = setTimeout(() => {
61
+ this.settle(message.id, "timeout", undefined, new Error("RPC timed out"));
62
+ }, options.timeoutMs);
63
+ }
64
+ this.pending.set(message.id, pending);
65
+ })
66
+ .catch((err) => {
67
+ // postMessage failed before we registered pending — release and reject.
68
+ this.releaseChannel(channelKey);
69
+ this.stats?.recordRpcEnd(startedAt, "error");
70
+ reject(err);
71
+ });
72
+ });
73
+ }
74
+ acquireChannel(channelKey, channelRef) {
75
+ const existing = this.channelRefs.get(channelKey);
76
+ if (existing) {
77
+ existing.refCount += 1;
78
+ return;
79
+ }
80
+ const handlers = {
81
+ onMessageUpdate: (msg, requestData) => this.route(msg, requestData),
82
+ };
83
+ const unsubscribe = this.gateway.subscribeToChannel(channelRef, handlers);
84
+ this.channelRefs.set(channelKey, { unsubscribe, refCount: 1 });
85
+ }
86
+ releaseChannel(channelKey) {
87
+ const entry = this.channelRefs.get(channelKey);
88
+ if (!entry)
89
+ return;
90
+ entry.refCount -= 1;
91
+ if (entry.refCount <= 0) {
92
+ entry.unsubscribe();
93
+ this.channelRefs.delete(channelKey);
94
+ }
95
+ }
96
+ route(msg, _requestData) {
97
+ const pending = this.pending.get(msg.id);
98
+ if (!pending)
99
+ return;
100
+ if (!isRpcMessageData(msg.data))
101
+ return;
102
+ const status = msg.data.status;
103
+ pending.onStatus?.(status);
104
+ if (status.code === "success") {
105
+ this.settle(msg.id, "success", msg.data.response);
106
+ }
107
+ else if (status.code === "error") {
108
+ this.settle(msg.id, "error", undefined, new errors_1.DooverRpcError(status, pending.request));
109
+ }
110
+ }
111
+ settle(messageId, outcome, response, rejection) {
112
+ const pending = this.pending.get(messageId);
113
+ if (!pending)
114
+ return;
115
+ this.pending.delete(messageId);
116
+ if (pending.timer)
117
+ clearTimeout(pending.timer);
118
+ if (pending.signal && pending.abortListener) {
119
+ pending.signal.removeEventListener("abort", pending.abortListener);
120
+ }
121
+ this.releaseChannel(pending.channelKey);
122
+ this.stats?.recordRpcEnd(pending.startedAt, outcome);
123
+ if (outcome === "success")
124
+ pending.resolve(response);
125
+ else
126
+ pending.reject(rejection);
127
+ }
128
+ }
129
+ exports.RpcDispatcher = RpcDispatcher;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const _args_1 = require("../apis/_args");
5
+ describe("resolveChannelArgs", () => {
6
+ it("parses positional (agentId, channelName)", () => {
7
+ const result = (0, _args_1.resolveChannelArgs)(["a1", "c1"]);
8
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", channelName: "c1", options: undefined });
9
+ });
10
+ it("parses positional with options", () => {
11
+ const result = (0, _args_1.resolveChannelArgs)(["a1", "c1", { x: 7 }]);
12
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", channelName: "c1", options: { x: 7 } });
13
+ });
14
+ it("parses identifier object", () => {
15
+ const result = (0, _args_1.resolveChannelArgs)([
16
+ { agentId: "a1", channelName: "c1" },
17
+ ]);
18
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", channelName: "c1", options: undefined });
19
+ });
20
+ it("parses identifier object with options", () => {
21
+ const result = (0, _args_1.resolveChannelArgs)([
22
+ { agentId: "a1", channelName: "c1" },
23
+ { x: 7 },
24
+ ]);
25
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", channelName: "c1", options: { x: 7 } });
26
+ });
27
+ });
28
+ describe("resolveAgentArgs", () => {
29
+ it("parses positional agentId", () => {
30
+ const result = (0, _args_1.resolveAgentArgs)(["a1"]);
31
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", options: undefined });
32
+ });
33
+ it("parses positional with options", () => {
34
+ const result = (0, _args_1.resolveAgentArgs)(["a1", { archived: true }]);
35
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", options: { archived: true } });
36
+ });
37
+ it("parses identifier object", () => {
38
+ const result = (0, _args_1.resolveAgentArgs)([{ agentId: "a1" }]);
39
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", options: undefined });
40
+ });
41
+ it("parses identifier object with options", () => {
42
+ const result = (0, _args_1.resolveAgentArgs)([
43
+ { agentId: "a1" },
44
+ { archived: true },
45
+ ]);
46
+ (0, chai_1.expect)(result).to.deep.equal({ agentId: "a1", options: { archived: true } });
47
+ });
48
+ });
@@ -0,0 +1,9 @@
1
+ import type { RestClient } from "../http/rest-client";
2
+ interface RecordedCall {
3
+ method: "get" | "post" | "put" | "patch" | "delete" | "request";
4
+ args: unknown[];
5
+ }
6
+ export declare function makeRestStub(): RestClient & {
7
+ calls: RecordedCall[];
8
+ };
9
+ export {};