doover-js 0.12.1 → 0.13.1

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 (43) hide show
  1. package/README.md +55 -0
  2. package/dist/client/data-client.d.ts +6 -0
  3. package/dist/client/doover-client.d.ts +4 -0
  4. package/dist/client/doover-client.js +6 -1
  5. package/dist/client/index.d.ts +3 -0
  6. package/dist/client/index.js +4 -1
  7. package/dist/client/network-status.d.ts +17 -0
  8. package/dist/client/network-status.js +70 -0
  9. package/dist/client/offline-cache.d.ts +9 -2
  10. package/dist/client/offline-cache.js +19 -6
  11. package/dist/client/service-reachability.d.ts +36 -0
  12. package/dist/client/service-reachability.js +107 -0
  13. package/dist/esm/client/data-client.d.ts +6 -0
  14. package/dist/esm/client/doover-client.d.ts +4 -0
  15. package/dist/esm/client/doover-client.js +6 -1
  16. package/dist/esm/client/index.d.ts +3 -0
  17. package/dist/esm/client/index.js +1 -0
  18. package/dist/esm/client/network-status.d.ts +17 -0
  19. package/dist/esm/client/network-status.js +66 -0
  20. package/dist/esm/client/offline-cache.d.ts +9 -2
  21. package/dist/esm/client/offline-cache.js +19 -6
  22. package/dist/esm/client/service-reachability.d.ts +36 -0
  23. package/dist/esm/client/service-reachability.js +103 -0
  24. package/dist/esm/http/rest-client.d.ts +7 -1
  25. package/dist/esm/http/rest-client.js +15 -7
  26. package/dist/esm/index.d.ts +3 -0
  27. package/dist/esm/index.js +1 -0
  28. package/dist/esm/package.json +1 -1
  29. package/dist/esm/react/index.d.ts +1 -0
  30. package/dist/esm/react/index.js +1 -0
  31. package/dist/esm/react/useOfflineStatus.js +30 -16
  32. package/dist/esm/react/useServiceReachability.d.ts +5 -0
  33. package/dist/esm/react/useServiceReachability.js +13 -0
  34. package/dist/http/rest-client.d.ts +7 -1
  35. package/dist/http/rest-client.js +15 -7
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +4 -1
  38. package/dist/react/index.d.ts +1 -0
  39. package/dist/react/index.js +3 -1
  40. package/dist/react/useOfflineStatus.js +29 -15
  41. package/dist/react/useServiceReachability.d.ts +5 -0
  42. package/dist/react/useServiceReachability.js +16 -0
  43. package/package.json +17 -2
@@ -1,3 +1,5 @@
1
+ import type { ServiceReachabilitySource } from "./service-reachability.js";
2
+ import { type NetworkStatusSource } from "./network-status.js";
1
3
  import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
2
4
  import type { Capability } from "./capabilities.js";
3
5
  export declare const DEFAULT_OFFLINE_RETENTION_MS: number;
@@ -83,18 +85,23 @@ export declare class MemoryOfflineStorageAdapter implements OfflineStorageAdapte
83
85
  deleteBlob(key: string): Promise<void>;
84
86
  }
85
87
  export interface OfflineDataClientOptions {
88
+ /** Reactive platform signal; defaults to the wrapped client's source. */
89
+ networkStatus?: NetworkStatusSource;
86
90
  client: DataClient;
87
91
  storage: OfflineStorageAdapter;
88
92
  scope: OfflineCacheScope;
89
93
  policies?: OfflineChannelPolicy[];
90
94
  defaultRetentionMs?: number;
91
95
  /**
92
- * Apps may provide their own reachability signal. When omitted, the wrapper
93
- * uses `navigator.onLine` when present and otherwise assumes online.
96
+ * Legacy pull-only override. Prefer networkStatus for reactive updates
97
+ * between requests. When omitted, the wrapper reads its network source.
94
98
  */
95
99
  isOnline?: () => boolean;
96
100
  }
97
101
  export declare class OfflineDataClient implements DataClient {
102
+ readonly networkStatus: NetworkStatusSource;
103
+ readonly reachability: ServiceReachabilitySource | undefined;
104
+ private unsubscribeNetwork?;
98
105
  readonly agents: AgentsApiLike;
99
106
  readonly channels: ChannelsApiLike;
100
107
  readonly messages: MessagesApiLike;
@@ -1,3 +1,4 @@
1
+ import { browserNetworkStatus } from "./network-status.js";
1
2
  import { DooverOfflineError } from "./errors.js";
2
3
  import { delegateRequestOptions, splitRequestOptions, } from "./request-options.js";
3
4
  export const DEFAULT_OFFLINE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -53,6 +54,8 @@ export class OfflineDataClient {
53
54
  this.offlineStatus = INITIAL_OFFLINE_STATUS;
54
55
  this.persistQueue = Promise.resolve();
55
56
  this.client = options.client;
57
+ this.reachability = options.client.reachability;
58
+ this.networkStatus = options.networkStatus ?? options.client.networkStatus ?? browserNetworkStatus;
56
59
  this.storage = options.storage;
57
60
  this.scope = options.scope;
58
61
  this.defaultRetentionMs = options.defaultRetentionMs ?? DEFAULT_OFFLINE_RETENTION_MS;
@@ -86,19 +89,30 @@ export class OfflineDataClient {
86
89
  return this.policies;
87
90
  }
88
91
  getOfflineStatus() {
89
- return {
90
- ...this.offlineStatus,
91
- online: this.isOnline(),
92
- };
92
+ const online = this.isOnline();
93
+ if (online !== this.offlineStatus.online) {
94
+ this.offlineStatus = { ...this.offlineStatus, online, at: Date.now() };
95
+ }
96
+ return this.offlineStatus;
93
97
  }
94
98
  onOfflineStatusChange(listener) {
95
99
  this.offlineStatusListeners.add(listener);
100
+ if (!this.unsubscribeNetwork) {
101
+ this.unsubscribeNetwork = this.networkStatus.subscribe(() => {
102
+ const status = this.getOfflineStatus();
103
+ this.offlineStatusListeners.forEach((callback) => callback(status));
104
+ });
105
+ }
96
106
  let active = true;
97
107
  return () => {
98
108
  if (!active)
99
109
  return;
100
110
  active = false;
101
111
  this.offlineStatusListeners.delete(listener);
112
+ if (!this.offlineStatusListeners.size) {
113
+ this.unsubscribeNetwork?.();
114
+ this.unsubscribeNetwork = undefined;
115
+ }
102
116
  };
103
117
  }
104
118
  async clearScope(scope = this.scope) {
@@ -678,8 +692,7 @@ export class OfflineDataClient {
678
692
  isOnline() {
679
693
  if (this.isOnlineFn)
680
694
  return this.isOnlineFn();
681
- const nav = globalThis.navigator;
682
- return nav?.onLine ?? true;
695
+ return this.networkStatus.getSnapshot().online;
683
696
  }
684
697
  setOfflineStatus(status) {
685
698
  this.offlineStatus = {
@@ -0,0 +1,36 @@
1
+ import type { NetworkStatusSource } from "./network-status.js";
2
+ /** Reachability of the data API, independent of browser and gateway status. */
3
+ export type ServiceReachability = "unknown" | "reachable" | "unreachable";
4
+ export interface ServiceReachabilitySource {
5
+ getSnapshot(): ServiceReachability;
6
+ subscribe(listener: () => void): () => void;
7
+ }
8
+ export interface ReachabilityOptions {
9
+ /** A CORS-enabled, read-only URL on the data service. Defaults to dataRestUrl.
10
+ * Any HTTP response proves reachability, including authentication/server errors.
11
+ * HEAD requests omit credentials and bypass the browser cache. */
12
+ probeUrl?: string;
13
+ }
14
+ /** One polling loop per observed client. No requests or timers until subscribed. */
15
+ export declare class ServiceReachabilityMonitor implements ServiceReachabilitySource {
16
+ private readonly url;
17
+ private readonly network;
18
+ private readonly fetchImpl;
19
+ private status;
20
+ private listeners;
21
+ private timer;
22
+ private controller;
23
+ private unsubscribeNetwork;
24
+ private lastCheck;
25
+ constructor(url: string, network: NetworkStatusSource, fetchImpl: typeof fetch);
26
+ getSnapshot(): ServiceReachability;
27
+ subscribe(listener: () => void): () => void;
28
+ /** Transport failures trigger a probe; they never retry the failed request.
29
+ * Coalesce failures and cap probes at one per second during request bursts. */
30
+ requestFailed(): void;
31
+ private setStatus;
32
+ private cancel;
33
+ private networkChanged;
34
+ private schedule;
35
+ private check;
36
+ }
@@ -0,0 +1,103 @@
1
+ /** One polling loop per observed client. No requests or timers until subscribed. */
2
+ export class ServiceReachabilityMonitor {
3
+ constructor(url, network, fetchImpl) {
4
+ this.url = url;
5
+ this.network = network;
6
+ this.fetchImpl = fetchImpl;
7
+ this.status = "unknown";
8
+ this.listeners = new Set();
9
+ this.lastCheck = -Infinity;
10
+ }
11
+ getSnapshot() { return this.status; }
12
+ subscribe(listener) {
13
+ const notify = () => listener();
14
+ this.listeners.add(notify);
15
+ if (this.listeners.size === 1) {
16
+ this.unsubscribeNetwork = this.network.subscribe(() => this.networkChanged());
17
+ this.networkChanged();
18
+ }
19
+ return () => {
20
+ this.listeners.delete(notify);
21
+ if (!this.listeners.size) {
22
+ this.unsubscribeNetwork?.();
23
+ this.unsubscribeNetwork = undefined;
24
+ this.cancel();
25
+ }
26
+ };
27
+ }
28
+ /** Transport failures trigger a probe; they never retry the failed request.
29
+ * Coalesce failures and cap probes at one per second during request bursts. */
30
+ requestFailed() {
31
+ if (!this.listeners.size || this.controller || !this.network.getSnapshot().online)
32
+ return;
33
+ this.schedule(Math.max(0, 1000 - (Date.now() - this.lastCheck)));
34
+ }
35
+ setStatus(status) {
36
+ if (status === this.status)
37
+ return;
38
+ this.status = status;
39
+ this.listeners.forEach((listener) => listener());
40
+ }
41
+ cancel() {
42
+ clearTimeout(this.timer);
43
+ this.timer = undefined;
44
+ const previous = this.controller;
45
+ this.controller = undefined;
46
+ previous?.abort();
47
+ }
48
+ networkChanged() {
49
+ if (!this.network.getSnapshot().online) {
50
+ this.cancel();
51
+ this.setStatus("unreachable");
52
+ }
53
+ else if (!this.controller) {
54
+ void this.check();
55
+ }
56
+ }
57
+ schedule(delay) {
58
+ clearTimeout(this.timer);
59
+ this.timer = setTimeout(() => { void this.check(); }, delay);
60
+ }
61
+ async check() {
62
+ if (!this.listeners.size || this.controller || !this.network.getSnapshot().online)
63
+ return;
64
+ clearTimeout(this.timer);
65
+ this.timer = undefined;
66
+ const controller = new AbortController();
67
+ this.controller = controller;
68
+ this.lastCheck = Date.now();
69
+ // The explicit race also bounds native/custom fetch implementations that
70
+ // ignore AbortSignal. Late responses cannot overwrite a newer observation.
71
+ let rejectAbort = () => { };
72
+ const aborted = new Promise((_resolve, reject) => {
73
+ rejectAbort = () => reject(new Error("Reachability check cancelled or timed out"));
74
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
75
+ });
76
+ const timeout = setTimeout(() => controller.abort(), 5000);
77
+ try {
78
+ const response = await Promise.race([
79
+ Promise.resolve().then(() => this.fetchImpl(this.url, {
80
+ method: "HEAD", cache: "no-store", credentials: "omit",
81
+ signal: controller.signal,
82
+ })),
83
+ aborted,
84
+ ]);
85
+ if (this.controller === controller) {
86
+ this.setStatus(response.status > 0 ? "reachable" : "unreachable");
87
+ }
88
+ }
89
+ catch {
90
+ if (this.controller === controller)
91
+ this.setStatus("unreachable");
92
+ }
93
+ finally {
94
+ clearTimeout(timeout);
95
+ controller.signal.removeEventListener("abort", rejectAbort);
96
+ if (this.controller === controller) {
97
+ this.controller = undefined;
98
+ if (this.listeners.size)
99
+ this.schedule(this.status === "reachable" ? 30000 : 5000);
100
+ }
101
+ }
102
+ }
103
+ }
@@ -1,7 +1,12 @@
1
+ import type { ReachabilityOptions } from "../client/service-reachability.js";
2
+ import type { NetworkStatusSource } from "../client/network-status.js";
1
3
  import type { AuthConfig } from "../auth/build-auth.js";
2
4
  import type { DooverAuth } from "../auth/doover-auth.js";
3
5
  import type { DooverStatsCollector } from "../client/stats.js";
4
6
  export interface DooverClientConfig extends AuthConfig {
7
+ networkStatus?: NetworkStatusSource;
8
+ /** Data API reachability checks while reachability is observed. Set false to disable. */
9
+ reachability?: ReachabilityOptions | false;
5
10
  dataRestUrl: string;
6
11
  controlApiUrl: string;
7
12
  dataWssUrl: string;
@@ -38,10 +43,11 @@ export interface RequestOptions {
38
43
  type QueryValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
39
44
  type QueryParams = Record<string, QueryValue> | object;
40
45
  export declare class RestClient {
46
+ private readonly onTransportFailure?;
41
47
  readonly config: Required<Pick<DooverClientConfig, "dataRestUrl" | "controlApiUrl" | "dataWssUrl" | "sharing" | "impersonateUserStorageKey">> & Pick<DooverClientConfig, "organisationId" | "fetchImpl" | "webSocketImpl" | "webSocketFactory">;
42
48
  readonly auth: DooverAuth | null;
43
49
  private stats;
44
- constructor(config: DooverClientConfig, auth?: DooverAuth);
50
+ constructor(config: DooverClientConfig, auth?: DooverAuth, onTransportFailure?: (() => void) | undefined);
45
51
  /** Attach a stats collector. Call with `null` to detach. */
46
52
  setStats(stats: DooverStatsCollector | null): void;
47
53
  request<T>(options: RequestOptions): Promise<T>;
@@ -1,6 +1,7 @@
1
1
  import { DooverApiError } from "./errors.js";
2
2
  export class RestClient {
3
- constructor(config, auth) {
3
+ constructor(config, auth, onTransportFailure) {
4
+ this.onTransportFailure = onTransportFailure;
4
5
  this.stats = null;
5
6
  this.config = {
6
7
  ...config,
@@ -91,12 +92,19 @@ export class RestClient {
91
92
  }
92
93
  }
93
94
  const fetchImpl = this.config.fetchImpl ?? fetch;
94
- return fetchImpl(request.url, {
95
- method: request.method,
96
- headers: request.headers,
97
- body: request.body,
98
- credentials: request.credentials,
99
- });
95
+ try {
96
+ return await fetchImpl(request.url, {
97
+ method: request.method,
98
+ headers: request.headers,
99
+ body: request.body,
100
+ credentials: request.credentials,
101
+ });
102
+ }
103
+ catch (error) {
104
+ // Auth preparation, HTTP errors and JSON parsing are outside this boundary.
105
+ this.onTransportFailure?.();
106
+ throw error;
107
+ }
100
108
  }
101
109
  get(path, query, baseUrl) {
102
110
  return this.request({ path, query, baseUrl });
@@ -60,3 +60,6 @@ export type * from "./types/viewer.js";
60
60
  export type * from "./types/audit.js";
61
61
  export { DV_AUDIT_CHANNEL } from "./types/audit.js";
62
62
  export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake.js";
63
+ export { browserNetworkStatus, createNetworkStatusStore } from "./client/network-status.js";
64
+ export type { NetworkStatusSource, NetworkStatusSnapshot } from "./client/network-status.js";
65
+ export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./client/service-reachability.js";
package/dist/esm/index.js CHANGED
@@ -35,3 +35,4 @@ export { DooverDataProvider } from "./viewer/doover-data-provider.js";
35
35
  export { getIdentifierFromPath } from "./viewer/path-parsing.js";
36
36
  export { DV_AUDIT_CHANNEL } from "./types/audit.js";
37
37
  export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake.js";
38
+ export { browserNetworkStatus, createNetworkStatusStore } from "./client/network-status.js";
@@ -1 +1 @@
1
- {"type":"module","sideEffects":false}
1
+ {"name":"doover-js","version":"0.13.1","type":"module","sideEffects":false}
@@ -36,3 +36,4 @@ export { useTurnCredentials } from "./useTurnCredentials.js";
36
36
  export { useAgentTokenState, useSetAgentTokenPolicy, useSetAgentAuthLock, useRevokeAgentTokens, useSetRevocationFloor, useCreateAdhocToken, useCreateDeviceToken, agentTokenStateQueryKey, } from "./useAgentTokenState.js";
37
37
  export type { UseAgentTokenStateOptions } from "./useAgentTokenState.js";
38
38
  export { getSharedQueryClient, resetSharedQueryClient, } from "./sharedQueryClient.js";
39
+ export { useServiceReachability } from "./useServiceReachability.js";
@@ -19,3 +19,4 @@ export { useDeviceMap } from "./useDeviceMap.js";
19
19
  export { useTurnCredentials } from "./useTurnCredentials.js";
20
20
  export { useAgentTokenState, useSetAgentTokenPolicy, useSetAgentAuthLock, useRevokeAgentTokens, useSetRevocationFloor, useCreateAdhocToken, useCreateDeviceToken, agentTokenStateQueryKey, } from "./useAgentTokenState.js";
21
21
  export { getSharedQueryClient, resetSharedQueryClient, } from "./sharedQueryClient.js";
22
+ export { useServiceReachability } from "./useServiceReachability.js";
@@ -1,23 +1,37 @@
1
- import { useSyncExternalStore } from "react";
1
+ import { useMemo, useSyncExternalStore } from "react";
2
+ import { browserNetworkStatus } from "../client/network-status.js";
2
3
  import { useDooverClient } from "./context.js";
3
4
  export function hasOfflineStatus(client) {
4
- return (!!client &&
5
- typeof client === "object" &&
6
- typeof client.getOfflineStatus === "function" &&
7
- typeof client.onOfflineStatusChange === "function");
5
+ return !!client && typeof client === "object" &&
6
+ "getOfflineStatus" in client && typeof client.getOfflineStatus === "function" &&
7
+ "onOfflineStatusChange" in client && typeof client.onOfflineStatusChange === "function";
8
8
  }
9
- const UNAVAILABLE_OFFLINE_STATUS = {
10
- online: true,
11
- state: "online",
12
- isOfflineFallback: false,
13
- isExpired: false,
14
- at: 0,
9
+ const SERVER_OFFLINE_STATUS = {
10
+ online: true, state: "online", isOfflineFallback: false, isExpired: false, at: 0,
15
11
  };
16
12
  export function useOfflineStatus() {
17
13
  const client = useDooverClient();
18
- return useSyncExternalStore((listener) => hasOfflineStatus(client)
19
- ? client.onOfflineStatusChange(listener)
20
- : () => undefined, () => hasOfflineStatus(client)
21
- ? client.getOfflineStatus()
22
- : UNAVAILABLE_OFFLINE_STATUS, () => UNAVAILABLE_OFFLINE_STATUS);
14
+ const store = useMemo(() => {
15
+ if (hasOfflineStatus(client)) {
16
+ return {
17
+ subscribe: (listener) => client.onOfflineStatusChange(listener),
18
+ getSnapshot: () => client.getOfflineStatus(),
19
+ };
20
+ }
21
+ const network = client.networkStatus ?? browserNetworkStatus;
22
+ let previous = network.getSnapshot();
23
+ let snapshot = { ...SERVER_OFFLINE_STATUS, ...previous };
24
+ return {
25
+ subscribe: (listener) => network.subscribe(listener),
26
+ getSnapshot: () => {
27
+ const next = network.getSnapshot();
28
+ if (next !== previous) {
29
+ previous = next;
30
+ snapshot = { ...SERVER_OFFLINE_STATUS, ...next };
31
+ }
32
+ return snapshot;
33
+ },
34
+ };
35
+ }, [client]);
36
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, () => SERVER_OFFLINE_STATUS);
23
37
  }
@@ -0,0 +1,5 @@
1
+ import type { ServiceReachability } from "../client/service-reachability.js";
2
+ /** Observe data API reachability even when the browser incorrectly reports online.
3
+ * Shares one polling loop per client; the final unmount stops checks.
4
+ * Clients without a reachability source return unknown and make no requests. */
5
+ export declare function useServiceReachability(): ServiceReachability;
@@ -0,0 +1,13 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { useDooverClient } from "./context.js";
3
+ const unknownSnapshot = () => "unknown";
4
+ const noop = () => { };
5
+ /** Observe data API reachability even when the browser incorrectly reports online.
6
+ * Shares one polling loop per client; the final unmount stops checks.
7
+ * Clients without a reachability source return unknown and make no requests. */
8
+ export function useServiceReachability() {
9
+ const source = useDooverClient().reachability;
10
+ const subscribe = useCallback((listener) => source?.subscribe(listener) ?? noop, [source]);
11
+ const getSnapshot = useCallback(() => source?.getSnapshot() ?? "unknown", [source]);
12
+ return useSyncExternalStore(subscribe, getSnapshot, unknownSnapshot);
13
+ }
@@ -1,7 +1,12 @@
1
+ import type { ReachabilityOptions } from "../client/service-reachability.js";
2
+ import type { NetworkStatusSource } from "../client/network-status.js";
1
3
  import type { AuthConfig } from "../auth/build-auth.js";
2
4
  import type { DooverAuth } from "../auth/doover-auth.js";
3
5
  import type { DooverStatsCollector } from "../client/stats.js";
4
6
  export interface DooverClientConfig extends AuthConfig {
7
+ networkStatus?: NetworkStatusSource;
8
+ /** Data API reachability checks while reachability is observed. Set false to disable. */
9
+ reachability?: ReachabilityOptions | false;
5
10
  dataRestUrl: string;
6
11
  controlApiUrl: string;
7
12
  dataWssUrl: string;
@@ -38,10 +43,11 @@ export interface RequestOptions {
38
43
  type QueryValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
39
44
  type QueryParams = Record<string, QueryValue> | object;
40
45
  export declare class RestClient {
46
+ private readonly onTransportFailure?;
41
47
  readonly config: Required<Pick<DooverClientConfig, "dataRestUrl" | "controlApiUrl" | "dataWssUrl" | "sharing" | "impersonateUserStorageKey">> & Pick<DooverClientConfig, "organisationId" | "fetchImpl" | "webSocketImpl" | "webSocketFactory">;
42
48
  readonly auth: DooverAuth | null;
43
49
  private stats;
44
- constructor(config: DooverClientConfig, auth?: DooverAuth);
50
+ constructor(config: DooverClientConfig, auth?: DooverAuth, onTransportFailure?: (() => void) | undefined);
45
51
  /** Attach a stats collector. Call with `null` to detach. */
46
52
  setStats(stats: DooverStatsCollector | null): void;
47
53
  request<T>(options: RequestOptions): Promise<T>;
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RestClient = void 0;
4
4
  const errors_js_1 = require("./errors.js");
5
5
  class RestClient {
6
- constructor(config, auth) {
6
+ constructor(config, auth, onTransportFailure) {
7
+ this.onTransportFailure = onTransportFailure;
7
8
  this.stats = null;
8
9
  this.config = {
9
10
  ...config,
@@ -94,12 +95,19 @@ class RestClient {
94
95
  }
95
96
  }
96
97
  const fetchImpl = this.config.fetchImpl ?? fetch;
97
- return fetchImpl(request.url, {
98
- method: request.method,
99
- headers: request.headers,
100
- body: request.body,
101
- credentials: request.credentials,
102
- });
98
+ try {
99
+ return await fetchImpl(request.url, {
100
+ method: request.method,
101
+ headers: request.headers,
102
+ body: request.body,
103
+ credentials: request.credentials,
104
+ });
105
+ }
106
+ catch (error) {
107
+ // Auth preparation, HTTP errors and JSON parsing are outside this boundary.
108
+ this.onTransportFailure?.();
109
+ throw error;
110
+ }
103
111
  }
104
112
  get(path, query, baseUrl) {
105
113
  return this.request({ path, query, baseUrl });
package/dist/index.d.ts CHANGED
@@ -60,3 +60,6 @@ export type * from "./types/viewer.js";
60
60
  export type * from "./types/audit.js";
61
61
  export { DV_AUDIT_CHANNEL } from "./types/audit.js";
62
62
  export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake.js";
63
+ export { browserNetworkStatus, createNetworkStatusStore } from "./client/network-status.js";
64
+ export type { NetworkStatusSource, NetworkStatusSnapshot } from "./client/network-status.js";
65
+ export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./client/service-reachability.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractSnowflakeId = exports.addTimestampToMessage = exports.DV_AUDIT_CHANNEL = exports.getIdentifierFromPath = exports.DooverDataProvider = exports.DooverValidationError = exports.DooverGatewayError = exports.DooverApiError = exports.RestClient = exports.GatewayClient = exports.buildAuth = exports.DooverAuthError = exports.AuthProfile = exports.DooverTokenAuth = exports.CookieAuth = exports.DooverAuth = exports.mergeBatchResponses = exports.chunkBatchItems = exports.MAX_BATCH_ITEMS = exports.splitRequestOptions = exports.isDooverRequestOptions = exports.requestOptions = exports.DEFAULT_OFFLINE_RETENTION_MS = exports.MemoryOfflineStorageAdapter = exports.OfflineDataClient = exports.DooverOfflineError = exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = exports.ALL_CAPABILITIES = exports.DooverRpcError = exports.RpcDispatcher = exports.UsersApi = exports.TurnApi = exports.ProcessorsApi = exports.PermissionsApi = exports.OrganisationsApi = exports.NotificationsApi = exports.MessagesApi = exports.ConnectionsApi = exports.ChannelsApi = exports.AlarmsApi = exports.AggregatesApi = exports.AgentsApi = exports.DooverStatsCollector = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.MultiplexClient = exports.LocalAgentClient = exports.DooverClient = void 0;
4
- exports.generateSnowflakeIdAtTime = void 0;
4
+ exports.createNetworkStatusStore = exports.browserNetworkStatus = exports.generateSnowflakeIdAtTime = void 0;
5
5
  var doover_client_js_1 = require("./client/doover-client.js");
6
6
  Object.defineProperty(exports, "DooverClient", { enumerable: true, get: function () { return doover_client_js_1.DooverClient; } });
7
7
  var local_agent_client_js_1 = require("./client/local-agent-client.js");
@@ -90,3 +90,6 @@ var snowflake_js_1 = require("./utils/snowflake.js");
90
90
  Object.defineProperty(exports, "addTimestampToMessage", { enumerable: true, get: function () { return snowflake_js_1.addTimestampToMessage; } });
91
91
  Object.defineProperty(exports, "extractSnowflakeId", { enumerable: true, get: function () { return snowflake_js_1.extractSnowflakeId; } });
92
92
  Object.defineProperty(exports, "generateSnowflakeIdAtTime", { enumerable: true, get: function () { return snowflake_js_1.generateSnowflakeIdAtTime; } });
93
+ var network_status_js_1 = require("./client/network-status.js");
94
+ Object.defineProperty(exports, "browserNetworkStatus", { enumerable: true, get: function () { return network_status_js_1.browserNetworkStatus; } });
95
+ Object.defineProperty(exports, "createNetworkStatusStore", { enumerable: true, get: function () { return network_status_js_1.createNetworkStatusStore; } });
@@ -36,3 +36,4 @@ export { useTurnCredentials } from "./useTurnCredentials.js";
36
36
  export { useAgentTokenState, useSetAgentTokenPolicy, useSetAgentAuthLock, useRevokeAgentTokens, useSetRevocationFloor, useCreateAdhocToken, useCreateDeviceToken, agentTokenStateQueryKey, } from "./useAgentTokenState.js";
37
37
  export type { UseAgentTokenStateOptions } from "./useAgentTokenState.js";
38
38
  export { getSharedQueryClient, resetSharedQueryClient, } from "./sharedQueryClient.js";
39
+ export { useServiceReachability } from "./useServiceReachability.js";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resetSharedQueryClient = exports.getSharedQueryClient = exports.agentTokenStateQueryKey = exports.useCreateDeviceToken = exports.useCreateAdhocToken = exports.useSetRevocationFloor = exports.useRevokeAgentTokens = exports.useSetAgentAuthLock = exports.useSetAgentTokenPolicy = exports.useAgentTokenState = exports.useTurnCredentials = exports.useDeviceMap = exports.multiAgentChannelMessagesQueryKey = exports.useMultiAgentChannelMessages = exports.multiAgentAggregatesQueryKey = exports.useMultiAgentAggregates = exports.useSendRpc = exports.invocationLogsQueryKey = exports.useInvocationLogs = exports.channelMessageQueryKey = exports.useChannelMessage = exports.channelMessagesQueryKey = exports.useChannelMessages = exports.useUpdateMessage = exports.useUpdateAggregate = exports.useSendMessage = exports.useAgentChannel = exports.channelAggregateQueryKey = exports.useChannelAggregate = exports.useChannelSubscription = exports.agentConnectionsQueryKey = exports.useAgentConnections = exports.useConnectionState = exports.hasOfflineStatus = exports.useOfflineStatus = exports.useClientStatus = exports.useDooverClient = exports.DooverProvider = void 0;
3
+ exports.useServiceReachability = exports.resetSharedQueryClient = exports.getSharedQueryClient = exports.agentTokenStateQueryKey = exports.useCreateDeviceToken = exports.useCreateAdhocToken = exports.useSetRevocationFloor = exports.useRevokeAgentTokens = exports.useSetAgentAuthLock = exports.useSetAgentTokenPolicy = exports.useAgentTokenState = exports.useTurnCredentials = exports.useDeviceMap = exports.multiAgentChannelMessagesQueryKey = exports.useMultiAgentChannelMessages = exports.multiAgentAggregatesQueryKey = exports.useMultiAgentAggregates = exports.useSendRpc = exports.invocationLogsQueryKey = exports.useInvocationLogs = exports.channelMessageQueryKey = exports.useChannelMessage = exports.channelMessagesQueryKey = exports.useChannelMessages = exports.useUpdateMessage = exports.useUpdateAggregate = exports.useSendMessage = exports.useAgentChannel = exports.channelAggregateQueryKey = exports.useChannelAggregate = exports.useChannelSubscription = exports.agentConnectionsQueryKey = exports.useAgentConnections = exports.useConnectionState = exports.hasOfflineStatus = exports.useOfflineStatus = exports.useClientStatus = exports.useDooverClient = exports.DooverProvider = void 0;
4
4
  var context_js_1 = require("./context.js");
5
5
  Object.defineProperty(exports, "DooverProvider", { enumerable: true, get: function () { return context_js_1.DooverProvider; } });
6
6
  Object.defineProperty(exports, "useDooverClient", { enumerable: true, get: function () { return context_js_1.useDooverClient; } });
@@ -60,3 +60,5 @@ Object.defineProperty(exports, "agentTokenStateQueryKey", { enumerable: true, ge
60
60
  var sharedQueryClient_js_1 = require("./sharedQueryClient.js");
61
61
  Object.defineProperty(exports, "getSharedQueryClient", { enumerable: true, get: function () { return sharedQueryClient_js_1.getSharedQueryClient; } });
62
62
  Object.defineProperty(exports, "resetSharedQueryClient", { enumerable: true, get: function () { return sharedQueryClient_js_1.resetSharedQueryClient; } });
63
+ var useServiceReachability_js_1 = require("./useServiceReachability.js");
64
+ Object.defineProperty(exports, "useServiceReachability", { enumerable: true, get: function () { return useServiceReachability_js_1.useServiceReachability; } });
@@ -3,25 +3,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hasOfflineStatus = hasOfflineStatus;
4
4
  exports.useOfflineStatus = useOfflineStatus;
5
5
  const react_1 = require("react");
6
+ const network_status_js_1 = require("../client/network-status.js");
6
7
  const context_js_1 = require("./context.js");
7
8
  function hasOfflineStatus(client) {
8
- return (!!client &&
9
- typeof client === "object" &&
10
- typeof client.getOfflineStatus === "function" &&
11
- typeof client.onOfflineStatusChange === "function");
9
+ return !!client && typeof client === "object" &&
10
+ "getOfflineStatus" in client && typeof client.getOfflineStatus === "function" &&
11
+ "onOfflineStatusChange" in client && typeof client.onOfflineStatusChange === "function";
12
12
  }
13
- const UNAVAILABLE_OFFLINE_STATUS = {
14
- online: true,
15
- state: "online",
16
- isOfflineFallback: false,
17
- isExpired: false,
18
- at: 0,
13
+ const SERVER_OFFLINE_STATUS = {
14
+ online: true, state: "online", isOfflineFallback: false, isExpired: false, at: 0,
19
15
  };
20
16
  function useOfflineStatus() {
21
17
  const client = (0, context_js_1.useDooverClient)();
22
- return (0, react_1.useSyncExternalStore)((listener) => hasOfflineStatus(client)
23
- ? client.onOfflineStatusChange(listener)
24
- : () => undefined, () => hasOfflineStatus(client)
25
- ? client.getOfflineStatus()
26
- : UNAVAILABLE_OFFLINE_STATUS, () => UNAVAILABLE_OFFLINE_STATUS);
18
+ const store = (0, react_1.useMemo)(() => {
19
+ if (hasOfflineStatus(client)) {
20
+ return {
21
+ subscribe: (listener) => client.onOfflineStatusChange(listener),
22
+ getSnapshot: () => client.getOfflineStatus(),
23
+ };
24
+ }
25
+ const network = client.networkStatus ?? network_status_js_1.browserNetworkStatus;
26
+ let previous = network.getSnapshot();
27
+ let snapshot = { ...SERVER_OFFLINE_STATUS, ...previous };
28
+ return {
29
+ subscribe: (listener) => network.subscribe(listener),
30
+ getSnapshot: () => {
31
+ const next = network.getSnapshot();
32
+ if (next !== previous) {
33
+ previous = next;
34
+ snapshot = { ...SERVER_OFFLINE_STATUS, ...next };
35
+ }
36
+ return snapshot;
37
+ },
38
+ };
39
+ }, [client]);
40
+ return (0, react_1.useSyncExternalStore)(store.subscribe, store.getSnapshot, () => SERVER_OFFLINE_STATUS);
27
41
  }
@@ -0,0 +1,5 @@
1
+ import type { ServiceReachability } from "../client/service-reachability.js";
2
+ /** Observe data API reachability even when the browser incorrectly reports online.
3
+ * Shares one polling loop per client; the final unmount stops checks.
4
+ * Clients without a reachability source return unknown and make no requests. */
5
+ export declare function useServiceReachability(): ServiceReachability;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useServiceReachability = useServiceReachability;
4
+ const react_1 = require("react");
5
+ const context_js_1 = require("./context.js");
6
+ const unknownSnapshot = () => "unknown";
7
+ const noop = () => { };
8
+ /** Observe data API reachability even when the browser incorrectly reports online.
9
+ * Shares one polling loop per client; the final unmount stops checks.
10
+ * Clients without a reachability source return unknown and make no requests. */
11
+ function useServiceReachability() {
12
+ const source = (0, context_js_1.useDooverClient)().reachability;
13
+ const subscribe = (0, react_1.useCallback)((listener) => source?.subscribe(listener) ?? noop, [source]);
14
+ const getSnapshot = (0, react_1.useCallback)(() => source?.getSnapshot() ?? "unknown", [source]);
15
+ return (0, react_1.useSyncExternalStore)(subscribe, getSnapshot, unknownSnapshot);
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -641,7 +641,19 @@
641
641
  },
642
642
  "default": "./dist/react/useUpdateMessage.js"
643
643
  },
644
- "./package.json": "./package.json"
644
+ "./package.json": "./package.json",
645
+ "./react/useServiceReachability": {
646
+ "module": "./dist/esm/react/useServiceReachability.js",
647
+ "import": {
648
+ "types": "./dist/esm/react/useServiceReachability.d.ts",
649
+ "default": "./dist/esm/react/useServiceReachability.js"
650
+ },
651
+ "require": {
652
+ "types": "./dist/react/useServiceReachability.d.ts",
653
+ "default": "./dist/react/useServiceReachability.js"
654
+ },
655
+ "default": "./dist/react/useServiceReachability.js"
656
+ }
645
657
  },
646
658
  "repository": {
647
659
  "type": "git",
@@ -866,6 +878,9 @@
866
878
  ],
867
879
  "react/useUpdateMessage": [
868
880
  "dist/react/useUpdateMessage.d.ts"
881
+ ],
882
+ "react/useServiceReachability": [
883
+ "dist/react/useServiceReachability.d.ts"
869
884
  ]
870
885
  }
871
886
  }