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.
Files changed (84) hide show
  1. package/README.md +104 -10
  2. package/dist/apis/connections-api.d.ts +6 -0
  3. package/dist/apis/connections-api.js +8 -0
  4. package/dist/auth/auth-profile.d.ts +45 -0
  5. package/dist/auth/auth-profile.js +72 -0
  6. package/dist/auth/auth-store.d.ts +14 -0
  7. package/dist/auth/auth-store.js +2 -0
  8. package/dist/auth/build-auth.d.ts +37 -0
  9. package/dist/auth/build-auth.js +75 -0
  10. package/dist/auth/cookie-auth.d.ts +17 -0
  11. package/dist/auth/cookie-auth.js +29 -0
  12. package/dist/auth/doover-auth.d.ts +41 -0
  13. package/dist/auth/doover-auth.js +24 -0
  14. package/dist/auth/doover-token-auth.d.ts +35 -0
  15. package/dist/auth/doover-token-auth.js +178 -0
  16. package/dist/auth/errors.d.ts +3 -0
  17. package/dist/auth/errors.js +10 -0
  18. package/dist/auth/jwt.d.ts +6 -0
  19. package/dist/auth/jwt.js +29 -0
  20. package/dist/client/doover-client.d.ts +15 -0
  21. package/dist/client/doover-client.js +45 -4
  22. package/dist/client/singleton.d.ts +26 -0
  23. package/dist/client/singleton.js +59 -0
  24. package/dist/client/stats.d.ts +55 -0
  25. package/dist/client/stats.js +97 -0
  26. package/dist/gateway/gateway-client.d.ts +34 -8
  27. package/dist/gateway/gateway-client.js +147 -52
  28. package/dist/gateway/types.d.ts +0 -1
  29. package/dist/http/rest-client.d.ts +21 -3
  30. package/dist/http/rest-client.js +33 -2
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +23 -5
  33. package/dist/node/config-manager.d.ts +34 -0
  34. package/dist/node/config-manager.js +177 -0
  35. package/dist/node/index.d.ts +1 -0
  36. package/dist/node/index.js +5 -0
  37. package/dist/react/context.d.ts +19 -0
  38. package/dist/react/context.js +28 -0
  39. package/dist/react/index.d.ts +24 -0
  40. package/dist/react/index.js +40 -0
  41. package/dist/react/sharedQueryClient.d.ts +21 -0
  42. package/dist/react/sharedQueryClient.js +39 -0
  43. package/dist/react/useAgentChannel.d.ts +7 -0
  44. package/dist/react/useAgentChannel.js +12 -0
  45. package/dist/react/useAgentConnections.d.ts +8 -0
  46. package/dist/react/useAgentConnections.js +24 -0
  47. package/dist/react/useChannelAggregate.d.ts +13 -0
  48. package/dist/react/useChannelAggregate.js +38 -0
  49. package/dist/react/useChannelMessages.d.ts +23 -0
  50. package/dist/react/useChannelMessages.js +56 -0
  51. package/dist/react/useChannelSubscription.d.ts +16 -0
  52. package/dist/react/useChannelSubscription.js +43 -0
  53. package/dist/react/useConnectionState.d.ts +21 -0
  54. package/dist/react/useConnectionState.js +43 -0
  55. package/dist/react/useMultiAgentAggregates.d.ts +22 -0
  56. package/dist/react/useMultiAgentAggregates.js +82 -0
  57. package/dist/react/useMultiAgentChannelMessages.d.ts +18 -0
  58. package/dist/react/useMultiAgentChannelMessages.js +78 -0
  59. package/dist/react/useSendMessage.d.ts +9 -0
  60. package/dist/react/useSendMessage.js +16 -0
  61. package/dist/react/useSendRpc.d.ts +61 -0
  62. package/dist/react/useSendRpc.js +158 -0
  63. package/dist/react/useTurnCredentials.d.ts +12 -0
  64. package/dist/react/useTurnCredentials.js +25 -0
  65. package/dist/react/useUpdateAggregate.d.ts +14 -0
  66. package/dist/react/useUpdateAggregate.js +25 -0
  67. package/dist/react/useUpdateMessage.d.ts +17 -0
  68. package/dist/react/useUpdateMessage.js +24 -0
  69. package/dist/test/apis.test.js +7 -1
  70. package/dist/test/auth.test.d.ts +1 -0
  71. package/dist/test/auth.test.js +502 -0
  72. package/dist/test/config-manager.test.d.ts +1 -0
  73. package/dist/test/config-manager.test.js +160 -0
  74. package/dist/test/doover-client.test.js +13 -0
  75. package/dist/test/doover-data-provider.test.js +155 -0
  76. package/dist/test/gateway-client.test.js +122 -8
  77. package/dist/test/react.test.d.ts +1 -0
  78. package/dist/test/react.test.js +353 -0
  79. package/dist/test/singleton.test.d.ts +1 -0
  80. package/dist/test/singleton.test.js +62 -0
  81. package/dist/types/common.d.ts +40 -0
  82. package/dist/viewer/doover-data-provider.d.ts +34 -5
  83. package/dist/viewer/doover-data-provider.js +96 -9
  84. package/package.json +33 -2
@@ -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
  });
@@ -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)("sends heartbeats, measures latency, and handles session cancellation", () => {
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 {};