doover-js 0.1.2 → 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.
- package/README.md +104 -10
- package/dist/apis/connections-api.d.ts +6 -0
- package/dist/apis/connections-api.js +8 -0
- package/dist/auth/auth-profile.d.ts +45 -0
- package/dist/auth/auth-profile.js +72 -0
- package/dist/auth/auth-store.d.ts +14 -0
- package/dist/auth/auth-store.js +2 -0
- package/dist/auth/build-auth.d.ts +37 -0
- package/dist/auth/build-auth.js +75 -0
- package/dist/auth/cookie-auth.d.ts +17 -0
- package/dist/auth/cookie-auth.js +29 -0
- package/dist/auth/doover-auth.d.ts +41 -0
- package/dist/auth/doover-auth.js +24 -0
- package/dist/auth/doover-token-auth.d.ts +35 -0
- package/dist/auth/doover-token-auth.js +178 -0
- package/dist/auth/errors.d.ts +3 -0
- package/dist/auth/errors.js +10 -0
- package/dist/auth/jwt.d.ts +6 -0
- package/dist/auth/jwt.js +29 -0
- package/dist/client/doover-client.d.ts +15 -0
- package/dist/client/doover-client.js +45 -4
- 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 +34 -8
- package/dist/gateway/gateway-client.js +147 -52
- package/dist/gateway/types.d.ts +0 -1
- package/dist/http/rest-client.d.ts +21 -3
- package/dist/http/rest-client.js +33 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +23 -5
- package/dist/node/config-manager.d.ts +34 -0
- package/dist/node/config-manager.js +177 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +5 -0
- 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 +23 -0
- package/dist/react/useChannelMessages.js +56 -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 +82 -0
- package/dist/react/useMultiAgentChannelMessages.d.ts +18 -0
- package/dist/react/useMultiAgentChannelMessages.js +78 -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 +14 -0
- package/dist/react/useUpdateAggregate.js +25 -0
- package/dist/react/useUpdateMessage.d.ts +17 -0
- package/dist/react/useUpdateMessage.js +24 -0
- package/dist/test/apis.test.js +7 -1
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +502 -0
- package/dist/test/config-manager.test.d.ts +1 -0
- package/dist/test/config-manager.test.js +160 -0
- 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 +34 -5
- package/dist/viewer/doover-data-provider.js +96 -9
- package/package.json +33 -2
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const chai_1 = require("chai");
|
|
8
|
+
const mocha_1 = require("mocha");
|
|
9
|
+
const react_1 = require("@testing-library/react");
|
|
10
|
+
const react_query_1 = require("@tanstack/react-query");
|
|
11
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
12
|
+
const doover_client_1 = require("../client/doover-client");
|
|
13
|
+
const react_2 = require("../react");
|
|
14
|
+
const helpers_1 = require("./helpers");
|
|
15
|
+
const snowflake_1 = require("../utils/snowflake");
|
|
16
|
+
function makeClient() {
|
|
17
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
18
|
+
if (url.endsWith("/wss_connections")) {
|
|
19
|
+
return (0, helpers_1.createJsonResponse)([{ address: "x", agent_id: "a1" }]);
|
|
20
|
+
}
|
|
21
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
22
|
+
});
|
|
23
|
+
const client = new doover_client_1.DooverClient({
|
|
24
|
+
dataRestUrl: "https://api.example.com",
|
|
25
|
+
controlApiUrl: "https://control.example.com",
|
|
26
|
+
dataWssUrl: "wss://ws.example.com",
|
|
27
|
+
fetchImpl: fetchMock,
|
|
28
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
29
|
+
disableBrowserLifecycleHooks: true,
|
|
30
|
+
});
|
|
31
|
+
return { client, fetchMock };
|
|
32
|
+
}
|
|
33
|
+
function wrapper(client) {
|
|
34
|
+
const queryClient = new react_query_1.QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
35
|
+
return function Wrapper({ children }) {
|
|
36
|
+
return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(react_2.DooverProvider, { client: client, children: children }) }));
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
(0, mocha_1.describe)("react bindings", () => {
|
|
40
|
+
(0, mocha_1.beforeEach)(() => {
|
|
41
|
+
(0, helpers_1.installSessionStorageMock)();
|
|
42
|
+
helpers_1.MockWebSocket.reset();
|
|
43
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
44
|
+
value: helpers_1.MockWebSocket,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
(0, mocha_1.afterEach)(() => {
|
|
50
|
+
sinon_1.default.restore();
|
|
51
|
+
});
|
|
52
|
+
(0, mocha_1.it)("useDooverClient returns the injected client", () => {
|
|
53
|
+
const { client } = makeClient();
|
|
54
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useDooverClient)(), {
|
|
55
|
+
wrapper: wrapper(client),
|
|
56
|
+
});
|
|
57
|
+
(0, chai_1.expect)(result.current).to.equal(client);
|
|
58
|
+
});
|
|
59
|
+
(0, mocha_1.it)("useDooverClient throws outside of a provider", () => {
|
|
60
|
+
// Suppress React's expected error-boundary log for the unmounted throw.
|
|
61
|
+
sinon_1.default.stub(console, "error");
|
|
62
|
+
(0, chai_1.expect)(() => (0, react_1.renderHook)(() => (0, react_2.useDooverClient)())).to.throw(/DooverProvider/);
|
|
63
|
+
});
|
|
64
|
+
(0, mocha_1.it)("useConnectionState reports connecting initially and flips to open on Ready", async () => {
|
|
65
|
+
const { client } = makeClient();
|
|
66
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useConnectionState)(), {
|
|
67
|
+
wrapper: wrapper(client),
|
|
68
|
+
});
|
|
69
|
+
(0, chai_1.expect)(result.current.status).to.equal("connecting");
|
|
70
|
+
(0, chai_1.expect)(result.current.lastOpenedAt).to.equal(null);
|
|
71
|
+
// Drive the gateway through a real connect.
|
|
72
|
+
await (0, react_1.act)(async () => {
|
|
73
|
+
await client.gateway.connect();
|
|
74
|
+
});
|
|
75
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
76
|
+
(0, react_1.act)(() => {
|
|
77
|
+
ws.open();
|
|
78
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
79
|
+
ws.receive({
|
|
80
|
+
op: 0,
|
|
81
|
+
t: "Ready",
|
|
82
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
await (0, react_1.waitFor)(() => {
|
|
86
|
+
(0, chai_1.expect)(result.current.status).to.equal("open");
|
|
87
|
+
(0, chai_1.expect)(result.current.lastOpenedAt).to.be.a("number");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
(0, mocha_1.it)("useAgentConnections fetches via ConnectionsApi", async () => {
|
|
91
|
+
const { client, fetchMock } = makeClient();
|
|
92
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useAgentConnections)("a1"), {
|
|
93
|
+
wrapper: wrapper(client),
|
|
94
|
+
});
|
|
95
|
+
await (0, react_1.waitFor)(() => {
|
|
96
|
+
(0, chai_1.expect)(result.current.isSuccess).to.equal(true);
|
|
97
|
+
});
|
|
98
|
+
(0, chai_1.expect)(result.current.data).to.deep.equal([{ address: "x", agent_id: "a1" }]);
|
|
99
|
+
(0, chai_1.expect)(fetchMock.getCall(0).args[0]).to.equal("https://api.example.com/agents/a1/wss_connections");
|
|
100
|
+
});
|
|
101
|
+
(0, mocha_1.it)("useAgentConnections stays disabled when agentId is undefined", () => {
|
|
102
|
+
const { client, fetchMock } = makeClient();
|
|
103
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useAgentConnections)(undefined), {
|
|
104
|
+
wrapper: wrapper(client),
|
|
105
|
+
});
|
|
106
|
+
(0, chai_1.expect)(result.current.fetchStatus).to.equal("idle");
|
|
107
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(0);
|
|
108
|
+
});
|
|
109
|
+
(0, mocha_1.it)("useAgentChannel fetches the aggregate and patches cache on gateway events", async () => {
|
|
110
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
111
|
+
if (url.endsWith("/channels/ui_state")) {
|
|
112
|
+
return (0, helpers_1.createJsonResponse)({
|
|
113
|
+
name: "ui_state",
|
|
114
|
+
owner_id: "a1",
|
|
115
|
+
is_private: false,
|
|
116
|
+
aggregate: { data: { x: 1 }, attachments: [] },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
120
|
+
});
|
|
121
|
+
const client = new doover_client_1.DooverClient({
|
|
122
|
+
dataRestUrl: "https://api.example.com",
|
|
123
|
+
controlApiUrl: "https://control.example.com",
|
|
124
|
+
dataWssUrl: "wss://ws.example.com",
|
|
125
|
+
fetchImpl: fetchMock,
|
|
126
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
127
|
+
disableBrowserLifecycleHooks: true,
|
|
128
|
+
});
|
|
129
|
+
const Wrapper = wrapper(client);
|
|
130
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useAgentChannel)("a1", "ui_state"), {
|
|
131
|
+
wrapper: Wrapper,
|
|
132
|
+
});
|
|
133
|
+
await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.isSuccess).to.equal(true));
|
|
134
|
+
(0, chai_1.expect)(result.current.data?.data).to.deep.equal({ x: 1 });
|
|
135
|
+
// Drive a live aggregate update.
|
|
136
|
+
await (0, react_1.act)(async () => {
|
|
137
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
138
|
+
ws.open();
|
|
139
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
140
|
+
ws.receive({
|
|
141
|
+
op: 0,
|
|
142
|
+
t: "Ready",
|
|
143
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
144
|
+
});
|
|
145
|
+
ws.receive({
|
|
146
|
+
op: 0,
|
|
147
|
+
t: "AggregateUpdate",
|
|
148
|
+
d: {
|
|
149
|
+
author_id: "u1",
|
|
150
|
+
channel: { agent_id: "a1", name: "ui_state" },
|
|
151
|
+
aggregate: { data: { x: 2 }, attachments: [] },
|
|
152
|
+
request_data: {},
|
|
153
|
+
organisation_id: "org-1",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.data?.data).to.deep.equal({ x: 2 }));
|
|
158
|
+
});
|
|
159
|
+
(0, mocha_1.it)("useSendMessage posts via MessagesApi", async () => {
|
|
160
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
161
|
+
if (url.endsWith("/messages")) {
|
|
162
|
+
return (0, helpers_1.createJsonResponse)({
|
|
163
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z")),
|
|
164
|
+
author_id: "u1",
|
|
165
|
+
channel: { agent_id: "a1", name: "notes" },
|
|
166
|
+
data: { text: "hi" },
|
|
167
|
+
attachments: [],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
171
|
+
});
|
|
172
|
+
const client = new doover_client_1.DooverClient({
|
|
173
|
+
dataRestUrl: "https://api.example.com",
|
|
174
|
+
controlApiUrl: "https://control.example.com",
|
|
175
|
+
dataWssUrl: "wss://ws.example.com",
|
|
176
|
+
fetchImpl: fetchMock,
|
|
177
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
178
|
+
disableBrowserLifecycleHooks: true,
|
|
179
|
+
});
|
|
180
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useSendMessage)({ agentId: "a1", channelName: "notes" }), { wrapper: wrapper(client) });
|
|
181
|
+
let mutationResult;
|
|
182
|
+
await (0, react_1.act)(async () => {
|
|
183
|
+
mutationResult = await result.current.mutateAsync({ text: "hi" });
|
|
184
|
+
});
|
|
185
|
+
(0, chai_1.expect)(mutationResult?.data).to.deep.equal({ text: "hi" });
|
|
186
|
+
});
|
|
187
|
+
(0, mocha_1.it)("useUpdateAggregate patches and writes back to the channel aggregate cache", async () => {
|
|
188
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({ data: { y: 2 }, attachments: [] }));
|
|
189
|
+
const client = new doover_client_1.DooverClient({
|
|
190
|
+
dataRestUrl: "https://api.example.com",
|
|
191
|
+
controlApiUrl: "https://control.example.com",
|
|
192
|
+
dataWssUrl: "wss://ws.example.com",
|
|
193
|
+
fetchImpl: fetchMock,
|
|
194
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
195
|
+
disableBrowserLifecycleHooks: true,
|
|
196
|
+
});
|
|
197
|
+
const queryClient = new react_query_1.QueryClient({
|
|
198
|
+
defaultOptions: { queries: { retry: false } },
|
|
199
|
+
});
|
|
200
|
+
function Wrapper({ children }) {
|
|
201
|
+
return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(react_2.DooverProvider, { client: client, children: children }) }));
|
|
202
|
+
}
|
|
203
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useUpdateAggregate)({ agentId: "a1", channelName: "ui_cmds" }), { wrapper: Wrapper });
|
|
204
|
+
await (0, react_1.act)(async () => {
|
|
205
|
+
await result.current.mutateAsync({ y: 2 });
|
|
206
|
+
});
|
|
207
|
+
const cached = queryClient.getQueryData((0, react_2.channelAggregateQueryKey)("a1", "ui_cmds"));
|
|
208
|
+
(0, chai_1.expect)(cached).to.deep.equal({ data: { y: 2 }, attachments: [] });
|
|
209
|
+
});
|
|
210
|
+
(0, mocha_1.it)("useChannelMessages paginates and prepends on live messageCreate", async () => {
|
|
211
|
+
const oldId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2025-12-31T23:59:00.000Z"));
|
|
212
|
+
const newId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:01.000Z"));
|
|
213
|
+
const liveId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:02.000Z"));
|
|
214
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)([
|
|
215
|
+
{
|
|
216
|
+
id: newId,
|
|
217
|
+
author_id: "u1",
|
|
218
|
+
channel: { agent_id: "a1", name: "notes" },
|
|
219
|
+
data: { v: 2 },
|
|
220
|
+
attachments: [],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: oldId,
|
|
224
|
+
author_id: "u1",
|
|
225
|
+
channel: { agent_id: "a1", name: "notes" },
|
|
226
|
+
data: { v: 1 },
|
|
227
|
+
attachments: [],
|
|
228
|
+
},
|
|
229
|
+
]));
|
|
230
|
+
const client = new doover_client_1.DooverClient({
|
|
231
|
+
dataRestUrl: "https://api.example.com",
|
|
232
|
+
controlApiUrl: "https://control.example.com",
|
|
233
|
+
dataWssUrl: "wss://ws.example.com",
|
|
234
|
+
fetchImpl: fetchMock,
|
|
235
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
236
|
+
disableBrowserLifecycleHooks: true,
|
|
237
|
+
});
|
|
238
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useChannelMessages)({ agentId: "a1", channelName: "notes" }, { limit: 10 }), { wrapper: wrapper(client) });
|
|
239
|
+
await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.isSuccess).to.equal(true));
|
|
240
|
+
(0, chai_1.expect)(result.current.messages.map((m) => m.id)).to.deep.equal([
|
|
241
|
+
oldId,
|
|
242
|
+
newId,
|
|
243
|
+
]);
|
|
244
|
+
// Gateway MessageCreate — should append to the first (newest) page.
|
|
245
|
+
await (0, react_1.act)(async () => {
|
|
246
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
247
|
+
ws.open();
|
|
248
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
249
|
+
ws.receive({
|
|
250
|
+
op: 0,
|
|
251
|
+
t: "Ready",
|
|
252
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
253
|
+
});
|
|
254
|
+
ws.receive({
|
|
255
|
+
op: 0,
|
|
256
|
+
t: "MessageCreate",
|
|
257
|
+
d: {
|
|
258
|
+
id: liveId,
|
|
259
|
+
author_id: "u1",
|
|
260
|
+
channel: { agent_id: "a1", name: "notes" },
|
|
261
|
+
data: { v: 3 },
|
|
262
|
+
attachments: [],
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
await (0, react_1.waitFor)(() => (0, chai_1.expect)(result.current.messages.map((m) => m.id)).to.deep.equal([
|
|
267
|
+
oldId,
|
|
268
|
+
newId,
|
|
269
|
+
liveId,
|
|
270
|
+
]));
|
|
271
|
+
});
|
|
272
|
+
(0, mocha_1.it)("getSharedQueryClient returns the same QueryClient across callers", () => {
|
|
273
|
+
(0, react_2.resetSharedQueryClient)();
|
|
274
|
+
const a = (0, react_2.getSharedQueryClient)();
|
|
275
|
+
const b = (0, react_2.getSharedQueryClient)();
|
|
276
|
+
(0, chai_1.expect)(a).to.equal(b);
|
|
277
|
+
(0, react_2.resetSharedQueryClient)();
|
|
278
|
+
const c = (0, react_2.getSharedQueryClient)();
|
|
279
|
+
(0, chai_1.expect)(c).to.not.equal(a);
|
|
280
|
+
(0, react_2.resetSharedQueryClient)();
|
|
281
|
+
});
|
|
282
|
+
(0, mocha_1.it)("useSendRpc tracks status history and resolves via sendRPC", async () => {
|
|
283
|
+
const rpcId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
284
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
285
|
+
if (url.endsWith("/messages")) {
|
|
286
|
+
return (0, helpers_1.createJsonResponse)({
|
|
287
|
+
id: rpcId,
|
|
288
|
+
author_id: "u1",
|
|
289
|
+
channel: { agent_id: "a1", name: "ui_cmds" },
|
|
290
|
+
data: {
|
|
291
|
+
type: "rpc",
|
|
292
|
+
method: "ping",
|
|
293
|
+
request: {},
|
|
294
|
+
status: { code: "sent" },
|
|
295
|
+
response: {},
|
|
296
|
+
},
|
|
297
|
+
attachments: [],
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
301
|
+
});
|
|
302
|
+
const client = new doover_client_1.DooverClient({
|
|
303
|
+
dataRestUrl: "https://api.example.com",
|
|
304
|
+
controlApiUrl: "https://control.example.com",
|
|
305
|
+
dataWssUrl: "wss://ws.example.com",
|
|
306
|
+
fetchImpl: fetchMock,
|
|
307
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
308
|
+
disableBrowserLifecycleHooks: true,
|
|
309
|
+
});
|
|
310
|
+
const { result } = (0, react_1.renderHook)(() => (0, react_2.useSendRpc)({ agentId: "a1", channelName: "ui_cmds" }, { method: "ping" }), { wrapper: wrapper(client) });
|
|
311
|
+
// Kick off the mutation.
|
|
312
|
+
const pending = (0, react_1.act)(async () => {
|
|
313
|
+
const promise = result.current.mutateAsync({ commandId: "c1", request: {} });
|
|
314
|
+
// Give the mutation a tick to register the command in state before we
|
|
315
|
+
// drive the gateway, so onStatus updates land against a known command.
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
317
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
318
|
+
ws.open();
|
|
319
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
320
|
+
ws.receive({
|
|
321
|
+
op: 0,
|
|
322
|
+
t: "Ready",
|
|
323
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
324
|
+
});
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
326
|
+
ws.receive({
|
|
327
|
+
op: 0,
|
|
328
|
+
t: "MessageUpdate",
|
|
329
|
+
d: {
|
|
330
|
+
id: rpcId,
|
|
331
|
+
author_id: "u1",
|
|
332
|
+
channel: { agent_id: "a1", name: "ui_cmds" },
|
|
333
|
+
data: {
|
|
334
|
+
type: "rpc",
|
|
335
|
+
method: "ping",
|
|
336
|
+
request: {},
|
|
337
|
+
status: { code: "success" },
|
|
338
|
+
response: { pong: true },
|
|
339
|
+
},
|
|
340
|
+
attachments: [],
|
|
341
|
+
request_data: {},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
return promise;
|
|
345
|
+
});
|
|
346
|
+
const response = await pending;
|
|
347
|
+
(0, chai_1.expect)(response).to.deep.equal({ pong: true });
|
|
348
|
+
(0, chai_1.expect)(result.current.getStatus("c1")?.code).to.equal("success");
|
|
349
|
+
(0, chai_1.expect)(result.current.isPending("c1")).to.equal(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
// Keep `render` imported so tsc doesn't drop it; we use renderHook everywhere.
|
|
353
|
+
void react_1.render;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const mocha_1 = require("mocha");
|
|
8
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
9
|
+
const doover_client_1 = require("../client/doover-client");
|
|
10
|
+
const singleton_1 = require("../client/singleton");
|
|
11
|
+
const helpers_1 = require("./helpers");
|
|
12
|
+
(0, mocha_1.describe)("getDooverClient", () => {
|
|
13
|
+
(0, mocha_1.beforeEach)(() => {
|
|
14
|
+
(0, helpers_1.installSessionStorageMock)();
|
|
15
|
+
helpers_1.MockWebSocket.reset();
|
|
16
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
17
|
+
value: helpers_1.MockWebSocket,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
(0, singleton_1.resetDooverClient)();
|
|
22
|
+
});
|
|
23
|
+
(0, mocha_1.afterEach)(() => {
|
|
24
|
+
sinon_1.default.restore();
|
|
25
|
+
(0, singleton_1.resetDooverClient)();
|
|
26
|
+
});
|
|
27
|
+
const config = {
|
|
28
|
+
dataRestUrl: "https://api.example.com",
|
|
29
|
+
controlApiUrl: "https://control.example.com",
|
|
30
|
+
dataWssUrl: "wss://ws.example.com",
|
|
31
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
32
|
+
disableBrowserLifecycleHooks: true,
|
|
33
|
+
};
|
|
34
|
+
(0, mocha_1.it)("creates a client on first call and returns the same instance after", () => {
|
|
35
|
+
const a = (0, singleton_1.getDooverClient)(config);
|
|
36
|
+
const b = (0, singleton_1.getDooverClient)(config);
|
|
37
|
+
(0, chai_1.expect)(a).to.be.instanceOf(doover_client_1.DooverClient);
|
|
38
|
+
(0, chai_1.expect)(a).to.equal(b);
|
|
39
|
+
});
|
|
40
|
+
(0, mocha_1.it)("peekDooverClient returns null until the first call, then the instance", () => {
|
|
41
|
+
(0, chai_1.expect)((0, singleton_1.peekDooverClient)()).to.equal(null);
|
|
42
|
+
const a = (0, singleton_1.getDooverClient)(config);
|
|
43
|
+
(0, chai_1.expect)((0, singleton_1.peekDooverClient)()).to.equal(a);
|
|
44
|
+
});
|
|
45
|
+
(0, mocha_1.it)("resetDooverClient clears the singleton so the next call creates a new one", () => {
|
|
46
|
+
const a = (0, singleton_1.getDooverClient)(config);
|
|
47
|
+
(0, singleton_1.resetDooverClient)();
|
|
48
|
+
const b = (0, singleton_1.getDooverClient)(config);
|
|
49
|
+
(0, chai_1.expect)(a).to.not.equal(b);
|
|
50
|
+
});
|
|
51
|
+
(0, mocha_1.it)("warns and reuses the existing client when a later caller differs in config", () => {
|
|
52
|
+
const warn = sinon_1.default.stub(console, "warn");
|
|
53
|
+
const a = (0, singleton_1.getDooverClient)(config);
|
|
54
|
+
const b = (0, singleton_1.getDooverClient)({
|
|
55
|
+
...config,
|
|
56
|
+
dataRestUrl: "https://other.example.com",
|
|
57
|
+
});
|
|
58
|
+
(0, chai_1.expect)(b).to.equal(a);
|
|
59
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
60
|
+
(0, chai_1.expect)(warn.getCall(0).args[0]).to.match(/differs from the already-initialised singleton/);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/dist/types/common.d.ts
CHANGED
|
@@ -128,3 +128,43 @@ export interface DataSeries {
|
|
|
128
128
|
count: number;
|
|
129
129
|
results: DataSeriesResult[];
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Lifecycle status for an RPC. `pending` carries arbitrary intermediate
|
|
133
|
+
* payloads (progress updates) emitted by the server while the request is
|
|
134
|
+
* in flight.
|
|
135
|
+
*/
|
|
136
|
+
export type RpcStatus<TPending = undefined> = {
|
|
137
|
+
code: "awaiting_confirmation";
|
|
138
|
+
} | {
|
|
139
|
+
code: "sent";
|
|
140
|
+
} | {
|
|
141
|
+
code: "acknowledged";
|
|
142
|
+
message: {
|
|
143
|
+
timestamp: number;
|
|
144
|
+
};
|
|
145
|
+
} | {
|
|
146
|
+
code: "error";
|
|
147
|
+
message: string | object;
|
|
148
|
+
} | {
|
|
149
|
+
code: "deferred";
|
|
150
|
+
message: {
|
|
151
|
+
until: number;
|
|
152
|
+
at: number;
|
|
153
|
+
};
|
|
154
|
+
} | {
|
|
155
|
+
code: "pending";
|
|
156
|
+
message: TPending;
|
|
157
|
+
} | {
|
|
158
|
+
code: "success";
|
|
159
|
+
};
|
|
160
|
+
/** Shape a consumer posts to initiate an RPC. */
|
|
161
|
+
export interface RpcRequest<TRequest = object> {
|
|
162
|
+
app_key?: string;
|
|
163
|
+
method: string;
|
|
164
|
+
request: TRequest;
|
|
165
|
+
}
|
|
166
|
+
/** Shape of the message the server maintains as the RPC progresses. */
|
|
167
|
+
export interface RpcMessageData<TRequest = object, TResponse = object, TPending = undefined> extends RpcRequest<TRequest> {
|
|
168
|
+
status: RpcStatus<TPending>;
|
|
169
|
+
response: TResponse;
|
|
170
|
+
}
|
|
@@ -2,14 +2,16 @@ import { AgentsApi } from "../apis/agents-api";
|
|
|
2
2
|
import { AggregatesApi } from "../apis/aggregates-api";
|
|
3
3
|
import { ChannelsApi } from "../apis/channels-api";
|
|
4
4
|
import { ConnectionsApi } from "../apis/connections-api";
|
|
5
|
-
import { MessagesApi } from "../apis/messages-api";
|
|
5
|
+
import { MessagesApi, type ListMessagesParams } from "../apis/messages-api";
|
|
6
|
+
import type { DooverAuth } from "../auth/doover-auth";
|
|
6
7
|
import { GatewayClient } from "../gateway/gateway-client";
|
|
7
8
|
import { RestClient, type DooverClientConfig } from "../http/rest-client";
|
|
8
|
-
import type { Aggregate, Channel, ConnectionDetails, MessageStructure } from "../types/common";
|
|
9
|
+
import type { Aggregate, Channel, ConnectionDetails, MessageStructure, RpcRequest, RpcStatus } from "../types/common";
|
|
9
10
|
import type { Agent, AgentsResponse, ChannelIdentifier, ChannelsDataProvider, SubscriptionDetails, User } from "../types/viewer";
|
|
10
11
|
interface SubscriptionEntry {
|
|
11
12
|
messageCallback: (identifier: ChannelIdentifier, message: MessageStructure) => void;
|
|
12
13
|
aggregateCallback: (identifier: ChannelIdentifier, aggregate: Aggregate) => void;
|
|
14
|
+
messageUpdateCallback?: (identifier: ChannelIdentifier, message: MessageStructure) => void;
|
|
13
15
|
}
|
|
14
16
|
export declare class DooverDataProvider implements ChannelsDataProvider<ChannelIdentifier> {
|
|
15
17
|
private readonly config;
|
|
@@ -21,7 +23,7 @@ export declare class DooverDataProvider implements ChannelsDataProvider<ChannelI
|
|
|
21
23
|
readonly aggregatesApi: AggregatesApi;
|
|
22
24
|
readonly connectionsApi: ConnectionsApi;
|
|
23
25
|
private readonly subscriptions;
|
|
24
|
-
constructor(config: DooverClientConfig);
|
|
26
|
+
constructor(config: DooverClientConfig, auth?: DooverAuth);
|
|
25
27
|
getMe(): Promise<User>;
|
|
26
28
|
getAgents(): Promise<AgentsResponse>;
|
|
27
29
|
getChannels(identifier: ChannelIdentifier, options?: {
|
|
@@ -33,12 +35,39 @@ export declare class DooverDataProvider implements ChannelsDataProvider<ChannelI
|
|
|
33
35
|
archiveChannel(identifier: ChannelIdentifier): Promise<unknown>;
|
|
34
36
|
unarchiveChannel(identifier: ChannelIdentifier): Promise<unknown>;
|
|
35
37
|
getChannel(identifier: ChannelIdentifier): Promise<Channel | undefined>;
|
|
36
|
-
subscribeToChannel(identifier: ChannelIdentifier, callback: SubscriptionEntry["messageCallback"], aggregateCallback: SubscriptionEntry["aggregateCallback"]): Promise<void>;
|
|
38
|
+
subscribeToChannel(identifier: ChannelIdentifier, callback: SubscriptionEntry["messageCallback"], aggregateCallback: SubscriptionEntry["aggregateCallback"], messageUpdateCallback?: SubscriptionEntry["messageUpdateCallback"]): Promise<void>;
|
|
37
39
|
unsubscribeFromChannel(identifier: ChannelIdentifier, callback: SubscriptionEntry["messageCallback"]): Promise<void>;
|
|
38
40
|
getAggregate(identifier: ChannelIdentifier): Promise<Aggregate | undefined>;
|
|
39
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a page of messages for a channel.
|
|
43
|
+
*
|
|
44
|
+
* Accepts either a cursor string (legacy — shorthand for `{ before }`)
|
|
45
|
+
* or a `ListMessagesParams` options object.
|
|
46
|
+
*
|
|
47
|
+
* Default limit is 10 for backwards compatibility; pass `limit` explicitly
|
|
48
|
+
* for larger pages.
|
|
49
|
+
*/
|
|
50
|
+
getMessages(identifier: ChannelIdentifier, optionsOrBefore?: string | ListMessagesParams): Promise<MessageStructure[] | undefined>;
|
|
40
51
|
deleteMessage(identifier: ChannelIdentifier, messageId: string): Promise<unknown>;
|
|
41
52
|
sendMessage(identifier: ChannelIdentifier, message: object): Promise<MessageStructure>;
|
|
53
|
+
/**
|
|
54
|
+
* Send a message-based RPC to a channel and await a settled response.
|
|
55
|
+
*
|
|
56
|
+
* Wire protocol: POST a message with `{ type: "rpc", ...rpcRequest }`; the
|
|
57
|
+
* server echoes progress via `MessageUpdate` events keyed by the message id,
|
|
58
|
+
* carrying a `status` envelope. Resolves on `success` with the `response`
|
|
59
|
+
* payload, rejects on `error` with the status message.
|
|
60
|
+
*
|
|
61
|
+
* For low-latency ephemeral commands (camera PTZ etc.), use
|
|
62
|
+
* `gateway.sendOneShotMessage` directly instead — that path is not persisted
|
|
63
|
+
* to the database.
|
|
64
|
+
*
|
|
65
|
+
* `onStatus` fires on every intermediate status (`sent`, `acknowledged`,
|
|
66
|
+
* `deferred`, `pending`) and once more on terminal states.
|
|
67
|
+
*/
|
|
68
|
+
sendRPC<TRequest = object, TResponse = object, TPending = undefined>(identifier: ChannelIdentifier, rpcRequest: RpcRequest<TRequest>, options?: {
|
|
69
|
+
onStatus?: (status: RpcStatus<TPending>) => void;
|
|
70
|
+
}): Promise<TResponse>;
|
|
42
71
|
updateAggregate(identifier: ChannelIdentifier, message: object): Promise<Aggregate>;
|
|
43
72
|
putAggregate(identifier: ChannelIdentifier, message: object): Promise<Aggregate>;
|
|
44
73
|
getChannelSubscriptions(identifier: ChannelIdentifier): Promise<SubscriptionDetails[]>;
|