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
|
@@ -104,6 +104,23 @@ const helpers_1 = require("./helpers");
|
|
|
104
104
|
(0, chai_1.expect)(messages?.map((message) => message.id)).to.deep.equal([oldId, newId]);
|
|
105
105
|
(0, chai_1.expect)(fetchMock.getCall(0).args[0]).to.equal("https://api.example.com/agents/a1/channels/c1/messages?before=cursor&limit=10");
|
|
106
106
|
});
|
|
107
|
+
(0, mocha_1.it)("forwards listMessages options (limit, after, field_name)", async () => {
|
|
108
|
+
const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)([]));
|
|
109
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
110
|
+
dataRestUrl: "https://api.example.com",
|
|
111
|
+
controlApiUrl: "https://control.example.com",
|
|
112
|
+
dataWssUrl: "wss://ws.example.com",
|
|
113
|
+
fetchImpl: fetchMock,
|
|
114
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
115
|
+
});
|
|
116
|
+
await provider.getMessages({ agentId: "a1", channelName: "c1" }, { before: "b1", after: "a1", limit: 50, field_name: ["x", "y"] });
|
|
117
|
+
const url = fetchMock.getCall(0).args[0];
|
|
118
|
+
(0, chai_1.expect)(url).to.include("before=b1");
|
|
119
|
+
(0, chai_1.expect)(url).to.include("after=a1");
|
|
120
|
+
(0, chai_1.expect)(url).to.include("limit=50");
|
|
121
|
+
(0, chai_1.expect)(url).to.include("field_name=x");
|
|
122
|
+
(0, chai_1.expect)(url).to.include("field_name=y");
|
|
123
|
+
});
|
|
107
124
|
(0, mocha_1.it)("prefers channel aggregate data and falls back to the aggregate endpoint", async () => {
|
|
108
125
|
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
109
126
|
if (url.endsWith("/channels/c1")) {
|
|
@@ -227,4 +244,142 @@ const helpers_1 = require("./helpers");
|
|
|
227
244
|
aggregatePath: "path/to/value",
|
|
228
245
|
});
|
|
229
246
|
});
|
|
247
|
+
(0, mocha_1.it)("sendRPC posts an rpc message, correlates the response by id, and resolves on success", async () => {
|
|
248
|
+
const rpcId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
249
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
250
|
+
if (url.endsWith("/messages")) {
|
|
251
|
+
return (0, helpers_1.createJsonResponse)({
|
|
252
|
+
id: rpcId,
|
|
253
|
+
author_id: "u1",
|
|
254
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
255
|
+
data: {
|
|
256
|
+
type: "rpc",
|
|
257
|
+
method: "ping",
|
|
258
|
+
request: {},
|
|
259
|
+
status: { code: "sent" },
|
|
260
|
+
response: {},
|
|
261
|
+
},
|
|
262
|
+
attachments: [],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
266
|
+
});
|
|
267
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
268
|
+
dataRestUrl: "https://api.example.com",
|
|
269
|
+
controlApiUrl: "https://control.example.com",
|
|
270
|
+
dataWssUrl: "wss://ws.example.com",
|
|
271
|
+
fetchImpl: fetchMock,
|
|
272
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
273
|
+
});
|
|
274
|
+
const statuses = [];
|
|
275
|
+
const promise = provider.sendRPC({ agentId: "a1", channelName: "c1" }, { method: "ping", request: {} }, { onStatus: (status) => statuses.push(status.code) });
|
|
276
|
+
// Let subscribe + POST resolve.
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
278
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
279
|
+
ws.open();
|
|
280
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
281
|
+
ws.receive({
|
|
282
|
+
op: 0,
|
|
283
|
+
t: "Ready",
|
|
284
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
285
|
+
});
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
287
|
+
// Progress updates.
|
|
288
|
+
ws.receive({
|
|
289
|
+
op: 0,
|
|
290
|
+
t: "MessageUpdate",
|
|
291
|
+
d: {
|
|
292
|
+
id: rpcId,
|
|
293
|
+
author_id: "u1",
|
|
294
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
295
|
+
data: {
|
|
296
|
+
type: "rpc",
|
|
297
|
+
method: "ping",
|
|
298
|
+
request: {},
|
|
299
|
+
status: { code: "acknowledged", message: { timestamp: 1 } },
|
|
300
|
+
response: {},
|
|
301
|
+
},
|
|
302
|
+
attachments: [],
|
|
303
|
+
request_data: {},
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
ws.receive({
|
|
307
|
+
op: 0,
|
|
308
|
+
t: "MessageUpdate",
|
|
309
|
+
d: {
|
|
310
|
+
id: rpcId,
|
|
311
|
+
author_id: "u1",
|
|
312
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
313
|
+
data: {
|
|
314
|
+
type: "rpc",
|
|
315
|
+
method: "ping",
|
|
316
|
+
request: {},
|
|
317
|
+
status: { code: "success" },
|
|
318
|
+
response: { pong: true },
|
|
319
|
+
},
|
|
320
|
+
attachments: [],
|
|
321
|
+
request_data: {},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
await (0, chai_1.expect)(promise).to.eventually.deep.equal({ pong: true });
|
|
325
|
+
(0, chai_1.expect)(statuses).to.deep.equal(["acknowledged", "success"]);
|
|
326
|
+
});
|
|
327
|
+
(0, mocha_1.it)("sendRPC rejects with the status message on error", async () => {
|
|
328
|
+
const rpcId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-01-01T00:00:00.000Z"));
|
|
329
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
330
|
+
if (url.endsWith("/messages")) {
|
|
331
|
+
return (0, helpers_1.createJsonResponse)({
|
|
332
|
+
id: rpcId,
|
|
333
|
+
author_id: "u1",
|
|
334
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
335
|
+
data: {
|
|
336
|
+
type: "rpc",
|
|
337
|
+
method: "ping",
|
|
338
|
+
request: {},
|
|
339
|
+
status: { code: "sent" },
|
|
340
|
+
response: {},
|
|
341
|
+
},
|
|
342
|
+
attachments: [],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return (0, helpers_1.createJsonResponse)({});
|
|
346
|
+
});
|
|
347
|
+
const provider = new doover_data_provider_1.DooverDataProvider({
|
|
348
|
+
dataRestUrl: "https://api.example.com",
|
|
349
|
+
controlApiUrl: "https://control.example.com",
|
|
350
|
+
dataWssUrl: "wss://ws.example.com",
|
|
351
|
+
fetchImpl: fetchMock,
|
|
352
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
353
|
+
});
|
|
354
|
+
const promise = provider.sendRPC({ agentId: "a1", channelName: "c1" }, { method: "ping", request: {} });
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
356
|
+
const ws = helpers_1.MockWebSocket.instances[0];
|
|
357
|
+
ws.open();
|
|
358
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
359
|
+
ws.receive({
|
|
360
|
+
op: 0,
|
|
361
|
+
t: "Ready",
|
|
362
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
363
|
+
});
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
365
|
+
ws.receive({
|
|
366
|
+
op: 0,
|
|
367
|
+
t: "MessageUpdate",
|
|
368
|
+
d: {
|
|
369
|
+
id: rpcId,
|
|
370
|
+
author_id: "u1",
|
|
371
|
+
channel: { agent_id: "a1", name: "c1" },
|
|
372
|
+
data: {
|
|
373
|
+
type: "rpc",
|
|
374
|
+
method: "ping",
|
|
375
|
+
request: {},
|
|
376
|
+
status: { code: "error", message: "boom" },
|
|
377
|
+
response: {},
|
|
378
|
+
},
|
|
379
|
+
attachments: [],
|
|
380
|
+
request_data: {},
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
await (0, chai_1.expect)(promise).to.eventually.be.rejectedWith("boom");
|
|
384
|
+
});
|
|
230
385
|
});
|
|
@@ -203,16 +203,14 @@ const helpers_1 = require("./helpers");
|
|
|
203
203
|
(0, chai_1.expect)(unsubscribed.calledOnce).to.equal(true);
|
|
204
204
|
(0, chai_1.expect)(wssError.calledWith({ message: "boom" })).to.equal(true);
|
|
205
205
|
});
|
|
206
|
-
(0, mocha_1.it)("
|
|
206
|
+
(0, mocha_1.it)("handles session cancellation (op 3)", () => {
|
|
207
207
|
const client = new gateway_client_1.GatewayClient({
|
|
208
208
|
dataRestUrl: "https://api.example.com",
|
|
209
209
|
controlApiUrl: "https://control.example.com",
|
|
210
210
|
dataWssUrl: "wss://ws.example.com",
|
|
211
211
|
webSocketImpl: helpers_1.MockWebSocket,
|
|
212
212
|
});
|
|
213
|
-
const ack = sinon_1.default.spy();
|
|
214
213
|
const cancelled = sinon_1.default.spy();
|
|
215
|
-
client.on("heartbeatAck", ack);
|
|
216
214
|
client.on("sessionCancelled", cancelled);
|
|
217
215
|
client.connect();
|
|
218
216
|
const ws = helpers_1.MockWebSocket.instances[0];
|
|
@@ -223,13 +221,129 @@ const helpers_1 = require("./helpers");
|
|
|
223
221
|
t: "Ready",
|
|
224
222
|
d: { session_id: "s1", session_token: "token", subscriptions: [] },
|
|
225
223
|
});
|
|
226
|
-
sinon_1.default.clock.tick(20000);
|
|
227
|
-
(0, chai_1.expect)(JSON.parse(ws.sent[ws.sent.length - 1])).to.deep.equal({ op: 1, d: {} });
|
|
228
|
-
sinon_1.default.clock.tick(10);
|
|
229
|
-
ws.receive({ op: 2, d: {} });
|
|
230
|
-
(0, chai_1.expect)(ack.calledWith(10)).to.equal(true);
|
|
231
224
|
ws.receive({ op: 3, d: {} });
|
|
232
225
|
(0, chai_1.expect)(cancelled.calledOnce).to.equal(true);
|
|
233
226
|
(0, chai_1.expect)(client.getSession()).to.equal(null);
|
|
234
227
|
});
|
|
228
|
+
(0, mocha_1.it)("reconnects with exponential backoff + jitter on unexpected close", () => {
|
|
229
|
+
// Make jitter deterministic: Math.random() → 1 means "wait the full backoff window".
|
|
230
|
+
const randomStub = sinon_1.default.stub(Math, "random").returns(1);
|
|
231
|
+
const client = new gateway_client_1.GatewayClient({
|
|
232
|
+
dataRestUrl: "https://api.example.com",
|
|
233
|
+
controlApiUrl: "https://control.example.com",
|
|
234
|
+
dataWssUrl: "wss://ws.example.com",
|
|
235
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
236
|
+
});
|
|
237
|
+
client.connect();
|
|
238
|
+
helpers_1.MockWebSocket.instances[0].open();
|
|
239
|
+
helpers_1.MockWebSocket.instances[0].close(1006, "transport closed");
|
|
240
|
+
// 1st attempt: ~1s window
|
|
241
|
+
sinon_1.default.clock.tick(999);
|
|
242
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(1);
|
|
243
|
+
sinon_1.default.clock.tick(1);
|
|
244
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(2);
|
|
245
|
+
helpers_1.MockWebSocket.instances[1].open();
|
|
246
|
+
helpers_1.MockWebSocket.instances[1].close(1006, "transport closed");
|
|
247
|
+
// 2nd attempt: ~2s window
|
|
248
|
+
sinon_1.default.clock.tick(1999);
|
|
249
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(2);
|
|
250
|
+
sinon_1.default.clock.tick(1);
|
|
251
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(3);
|
|
252
|
+
// Successful Ready should reset the attempt counter.
|
|
253
|
+
helpers_1.MockWebSocket.instances[2].open();
|
|
254
|
+
helpers_1.MockWebSocket.instances[2].receive({ op: 0, t: "Hello", d: {} });
|
|
255
|
+
helpers_1.MockWebSocket.instances[2].receive({
|
|
256
|
+
op: 0,
|
|
257
|
+
t: "Ready",
|
|
258
|
+
d: { session_id: "s1", session_token: "token", subscriptions: [] },
|
|
259
|
+
});
|
|
260
|
+
helpers_1.MockWebSocket.instances[2].close(1006, "transport closed");
|
|
261
|
+
sinon_1.default.clock.tick(999);
|
|
262
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(3);
|
|
263
|
+
sinon_1.default.clock.tick(1);
|
|
264
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(4);
|
|
265
|
+
randomStub.restore();
|
|
266
|
+
});
|
|
267
|
+
(0, mocha_1.it)("reconnects immediately on visibilitychange → visible", () => {
|
|
268
|
+
const listeners = new Map();
|
|
269
|
+
const fakeDocument = {
|
|
270
|
+
visibilityState: "hidden",
|
|
271
|
+
addEventListener: (event, handler) => {
|
|
272
|
+
listeners.set(event, handler);
|
|
273
|
+
},
|
|
274
|
+
removeEventListener: (event) => {
|
|
275
|
+
listeners.delete(event);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
Object.defineProperty(globalThis, "document", {
|
|
279
|
+
value: fakeDocument,
|
|
280
|
+
configurable: true,
|
|
281
|
+
writable: true,
|
|
282
|
+
});
|
|
283
|
+
sinon_1.default.stub(Math, "random").returns(1);
|
|
284
|
+
const client = new gateway_client_1.GatewayClient({
|
|
285
|
+
dataRestUrl: "https://api.example.com",
|
|
286
|
+
controlApiUrl: "https://control.example.com",
|
|
287
|
+
dataWssUrl: "wss://ws.example.com",
|
|
288
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
289
|
+
});
|
|
290
|
+
client.connect();
|
|
291
|
+
helpers_1.MockWebSocket.instances[0].open();
|
|
292
|
+
helpers_1.MockWebSocket.instances[0].close(1006, "transport closed");
|
|
293
|
+
// Pending backoff would wait up to 1s.
|
|
294
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(1);
|
|
295
|
+
// Tab returns to visible — bypass backoff, reconnect now.
|
|
296
|
+
fakeDocument.visibilityState = "visible";
|
|
297
|
+
listeners.get("visibilitychange")?.(new Event("visibilitychange"));
|
|
298
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(2);
|
|
299
|
+
// @ts-expect-error — cleanup test global
|
|
300
|
+
delete globalThis.document;
|
|
301
|
+
});
|
|
302
|
+
(0, mocha_1.it)("skips lifecycle listeners when disableBrowserLifecycleHooks is true", () => {
|
|
303
|
+
const addSpy = sinon_1.default.spy();
|
|
304
|
+
const fakeDocument = {
|
|
305
|
+
visibilityState: "visible",
|
|
306
|
+
addEventListener: addSpy,
|
|
307
|
+
removeEventListener: sinon_1.default.spy(),
|
|
308
|
+
};
|
|
309
|
+
Object.defineProperty(globalThis, "document", {
|
|
310
|
+
value: fakeDocument,
|
|
311
|
+
configurable: true,
|
|
312
|
+
writable: true,
|
|
313
|
+
});
|
|
314
|
+
new gateway_client_1.GatewayClient({
|
|
315
|
+
dataRestUrl: "https://api.example.com",
|
|
316
|
+
controlApiUrl: "https://control.example.com",
|
|
317
|
+
dataWssUrl: "wss://ws.example.com",
|
|
318
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
319
|
+
disableBrowserLifecycleHooks: true,
|
|
320
|
+
});
|
|
321
|
+
(0, chai_1.expect)(addSpy.called).to.equal(false);
|
|
322
|
+
// @ts-expect-error — cleanup test global
|
|
323
|
+
delete globalThis.document;
|
|
324
|
+
});
|
|
325
|
+
(0, mocha_1.it)("caps backoff at 30s and does not reconnect after explicit disconnect", () => {
|
|
326
|
+
const randomStub = sinon_1.default.stub(Math, "random").returns(1);
|
|
327
|
+
const client = new gateway_client_1.GatewayClient({
|
|
328
|
+
dataRestUrl: "https://api.example.com",
|
|
329
|
+
controlApiUrl: "https://control.example.com",
|
|
330
|
+
dataWssUrl: "wss://ws.example.com",
|
|
331
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
332
|
+
});
|
|
333
|
+
client.connect();
|
|
334
|
+
// Force many failed attempts to push backoff past the cap.
|
|
335
|
+
for (let i = 0; i < 10; i += 1) {
|
|
336
|
+
const ws = helpers_1.MockWebSocket.instances[i];
|
|
337
|
+
ws.open();
|
|
338
|
+
ws.close(1006, "transport closed");
|
|
339
|
+
sinon_1.default.clock.tick(30000);
|
|
340
|
+
}
|
|
341
|
+
// After 10 attempts we should have 11 sockets, and the delay never exceeds 30s.
|
|
342
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(11);
|
|
343
|
+
// Explicit disconnect must not schedule a further reconnect.
|
|
344
|
+
client.disconnect();
|
|
345
|
+
sinon_1.default.clock.tick(60000);
|
|
346
|
+
(0, chai_1.expect)(helpers_1.MockWebSocket.instances.length).to.equal(11);
|
|
347
|
+
randomStub.restore();
|
|
348
|
+
});
|
|
235
349
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|