doover-js 0.13.0 → 0.13.2
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 +30 -0
- package/dist/client/data-client.d.ts +3 -0
- package/dist/client/doover-client.d.ts +2 -0
- package/dist/client/doover-client.js +7 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/offline-cache.d.ts +2 -0
- package/dist/client/offline-cache.js +1 -0
- package/dist/client/service-reachability.d.ts +36 -0
- package/dist/client/service-reachability.js +107 -0
- package/dist/esm/client/data-client.d.ts +3 -0
- package/dist/esm/client/doover-client.d.ts +2 -0
- package/dist/esm/client/doover-client.js +7 -1
- package/dist/esm/client/index.d.ts +1 -0
- package/dist/esm/client/offline-cache.d.ts +2 -0
- package/dist/esm/client/offline-cache.js +1 -0
- package/dist/esm/client/service-reachability.d.ts +36 -0
- package/dist/esm/client/service-reachability.js +103 -0
- package/dist/esm/http/rest-client.d.ts +5 -1
- package/dist/esm/http/rest-client.js +15 -7
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/react/index.d.ts +1 -0
- package/dist/esm/react/index.js +1 -0
- package/dist/esm/react/useServiceReachability.d.ts +5 -0
- package/dist/esm/react/useServiceReachability.js +13 -0
- package/dist/http/rest-client.d.ts +5 -1
- package/dist/http/rest-client.js +15 -7
- package/dist/index.d.ts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -1
- package/dist/react/useServiceReachability.d.ts +5 -0
- package/dist/react/useServiceReachability.js +16 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -416,3 +416,33 @@ its `setOnline(online)` method from platform network callbacks. Gateway failures
|
|
|
416
416
|
must not update this source. The legacy `OfflineDataClient.isOnline` callback
|
|
417
417
|
remains supported for request-time checks; a reactive source also updates the UI
|
|
418
418
|
between requests.
|
|
419
|
+
|
|
420
|
+
### Data service reachability
|
|
421
|
+
|
|
422
|
+
`useServiceReachability()` returns `"unknown"`, `"reachable"`, or `"unreachable"`.
|
|
423
|
+
It observes `client.reachability`, independently of `navigator.onLine` and the
|
|
424
|
+
WebSocket's open state. `OfflineDataClient` forwards the wrapped client's source.
|
|
425
|
+
Clients without this source return `"unknown"`; multiplex consumers can observe
|
|
426
|
+
the relevant member client rather than assuming all services share connectivity.
|
|
427
|
+
|
|
428
|
+
Checks run only while this hook or a direct `client.reachability.subscribe(...)`
|
|
429
|
+
subscription is mounted. Multiple observers share one loop. Existing
|
|
430
|
+
`onStatusChange` subscriptions do not start checks. The final unsubscribe cancels
|
|
431
|
+
the timer and probe. Each probe has a five-second deadline. Healthy clients check
|
|
432
|
+
every 30 seconds; unreachable clients retry every five seconds. Browser offline
|
|
433
|
+
events suspend probes; online events trigger a fresh check.
|
|
434
|
+
|
|
435
|
+
A REST transport failure also schedules a check, coalescing simultaneous failures
|
|
436
|
+
and limiting probes to one per second. It does not retry or queue the failed
|
|
437
|
+
request, including commands. HTTP error responses and response parsing failures
|
|
438
|
+
do not trigger these checks.
|
|
439
|
+
|
|
440
|
+
The probe sends a credential-free `HEAD` request to `dataRestUrl` with a trailing slash and
|
|
441
|
+
`cache: "no-store"`. For example, a base URL ending in `/api` is probed at `/api/`,
|
|
442
|
+
inside the API routing prefix rather than a possible static CDN fallback. An
|
|
443
|
+
explicit `probeUrl` is used unchanged. Any HTTP response, including 401, 404, or 500, proves network
|
|
444
|
+
reachability; this is not an application health check. DNS, CORS, transport errors,
|
|
445
|
+
and timeouts report unreachable. A deployment must allow CORS on its probe URL.
|
|
446
|
+
Set `reachability: { probeUrl: "https://data.example.com/health" }` to use a different
|
|
447
|
+
read-only endpoint, or `reachability: false` to disable the source. The configured
|
|
448
|
+
`fetchImpl` also handles probes, including in native applications.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import type { NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import type { AgentsApi } from "../apis/agents-api.js";
|
|
3
4
|
import type { AggregatesApi } from "../apis/aggregates-api.js";
|
|
@@ -79,6 +80,8 @@ export interface DataClientStatus {
|
|
|
79
80
|
export interface DataClient {
|
|
80
81
|
/** Optional platform source; React hooks otherwise use browser network status. */
|
|
81
82
|
readonly networkStatus?: NetworkStatusSource;
|
|
83
|
+
/** Optional data API reachability. Polls only while explicitly subscribed. */
|
|
84
|
+
readonly reachability?: ServiceReachabilitySource;
|
|
82
85
|
readonly agents: AgentsApiLike;
|
|
83
86
|
readonly channels: ChannelsApiLike;
|
|
84
87
|
readonly messages: MessagesApiLike;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import { type NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import { OrganisationsApi } from "../apis/organisations-api.js";
|
|
3
4
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
@@ -8,6 +9,7 @@ import type { AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike,
|
|
|
8
9
|
import { DooverStatsCollector, type DooverStatsSnapshot } from "./stats.js";
|
|
9
10
|
export declare class DooverClient implements DataClient {
|
|
10
11
|
readonly networkStatus: NetworkStatusSource;
|
|
12
|
+
readonly reachability: ServiceReachabilitySource | undefined;
|
|
11
13
|
readonly auth: DooverAuth;
|
|
12
14
|
readonly rest: RestClient;
|
|
13
15
|
readonly viewer: DooverDataProvider;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DooverClient = void 0;
|
|
4
|
+
const service_reachability_js_1 = require("./service-reachability.js");
|
|
4
5
|
const network_status_js_1 = require("./network-status.js");
|
|
5
6
|
const agents_api_js_1 = require("../apis/agents-api.js");
|
|
6
7
|
const aggregates_api_js_1 = require("../apis/aggregates-api.js");
|
|
@@ -50,7 +51,12 @@ class DooverClient {
|
|
|
50
51
|
},
|
|
51
52
|
};
|
|
52
53
|
const stamper = new provenance_js_1.ProvenanceStamper(this.identity);
|
|
53
|
-
|
|
54
|
+
const reachability = config.reachability === false ? undefined : new service_reachability_js_1.ServiceReachabilityMonitor(
|
|
55
|
+
// Probe inside the API prefix: the bare prefix may route to a static CDN
|
|
56
|
+
// fallback without CORS, even though every real API request succeeds.
|
|
57
|
+
config.reachability?.probeUrl ?? `${config.dataRestUrl.replace(/\/+$/, "")}/`, this.networkStatus, config.fetchImpl ?? ((...args) => fetch(...args)));
|
|
58
|
+
this.reachability = reachability;
|
|
59
|
+
this.rest = new rest_client_js_1.RestClient(config, this.auth, () => reachability?.requestFailed());
|
|
54
60
|
this.gatewayImpl = new gateway_client_js_1.GatewayClient(config, this.auth);
|
|
55
61
|
this.gatewayImpl.setProvenanceHook((value, ctx) => stamper.stampGatewayEvent(value, ctx));
|
|
56
62
|
this.gateway = this.gatewayImpl;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export type { DooverClientConfig } from "../http/rest-client.js";
|
|
|
4
4
|
export type * from "./data-client.js";
|
|
5
5
|
export { browserNetworkStatus, createNetworkStatusStore } from "./network-status.js";
|
|
6
6
|
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./network-status.js";
|
|
7
|
+
export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./service-reachability.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import { type NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
3
4
|
import type { Capability } from "./capabilities.js";
|
|
@@ -99,6 +100,7 @@ export interface OfflineDataClientOptions {
|
|
|
99
100
|
}
|
|
100
101
|
export declare class OfflineDataClient implements DataClient {
|
|
101
102
|
readonly networkStatus: NetworkStatusSource;
|
|
103
|
+
readonly reachability: ServiceReachabilitySource | undefined;
|
|
102
104
|
private unsubscribeNetwork?;
|
|
103
105
|
readonly agents: AgentsApiLike;
|
|
104
106
|
readonly channels: ChannelsApiLike;
|
|
@@ -58,6 +58,7 @@ class OfflineDataClient {
|
|
|
58
58
|
this.offlineStatus = INITIAL_OFFLINE_STATUS;
|
|
59
59
|
this.persistQueue = Promise.resolve();
|
|
60
60
|
this.client = options.client;
|
|
61
|
+
this.reachability = options.client.reachability;
|
|
61
62
|
this.networkStatus = options.networkStatus ?? options.client.networkStatus ?? network_status_js_1.browserNetworkStatus;
|
|
62
63
|
this.storage = options.storage;
|
|
63
64
|
this.scope = options.scope;
|
|
@@ -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 with a trailing slash.
|
|
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,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceReachabilityMonitor = void 0;
|
|
4
|
+
/** One polling loop per observed client. No requests or timers until subscribed. */
|
|
5
|
+
class ServiceReachabilityMonitor {
|
|
6
|
+
constructor(url, network, fetchImpl) {
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.network = network;
|
|
9
|
+
this.fetchImpl = fetchImpl;
|
|
10
|
+
this.status = "unknown";
|
|
11
|
+
this.listeners = new Set();
|
|
12
|
+
this.lastCheck = -Infinity;
|
|
13
|
+
}
|
|
14
|
+
getSnapshot() { return this.status; }
|
|
15
|
+
subscribe(listener) {
|
|
16
|
+
const notify = () => listener();
|
|
17
|
+
this.listeners.add(notify);
|
|
18
|
+
if (this.listeners.size === 1) {
|
|
19
|
+
this.unsubscribeNetwork = this.network.subscribe(() => this.networkChanged());
|
|
20
|
+
this.networkChanged();
|
|
21
|
+
}
|
|
22
|
+
return () => {
|
|
23
|
+
this.listeners.delete(notify);
|
|
24
|
+
if (!this.listeners.size) {
|
|
25
|
+
this.unsubscribeNetwork?.();
|
|
26
|
+
this.unsubscribeNetwork = undefined;
|
|
27
|
+
this.cancel();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Transport failures trigger a probe; they never retry the failed request.
|
|
32
|
+
* Coalesce failures and cap probes at one per second during request bursts. */
|
|
33
|
+
requestFailed() {
|
|
34
|
+
if (!this.listeners.size || this.controller || !this.network.getSnapshot().online)
|
|
35
|
+
return;
|
|
36
|
+
this.schedule(Math.max(0, 1000 - (Date.now() - this.lastCheck)));
|
|
37
|
+
}
|
|
38
|
+
setStatus(status) {
|
|
39
|
+
if (status === this.status)
|
|
40
|
+
return;
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.listeners.forEach((listener) => listener());
|
|
43
|
+
}
|
|
44
|
+
cancel() {
|
|
45
|
+
clearTimeout(this.timer);
|
|
46
|
+
this.timer = undefined;
|
|
47
|
+
const previous = this.controller;
|
|
48
|
+
this.controller = undefined;
|
|
49
|
+
previous?.abort();
|
|
50
|
+
}
|
|
51
|
+
networkChanged() {
|
|
52
|
+
if (!this.network.getSnapshot().online) {
|
|
53
|
+
this.cancel();
|
|
54
|
+
this.setStatus("unreachable");
|
|
55
|
+
}
|
|
56
|
+
else if (!this.controller) {
|
|
57
|
+
void this.check();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
schedule(delay) {
|
|
61
|
+
clearTimeout(this.timer);
|
|
62
|
+
this.timer = setTimeout(() => { void this.check(); }, delay);
|
|
63
|
+
}
|
|
64
|
+
async check() {
|
|
65
|
+
if (!this.listeners.size || this.controller || !this.network.getSnapshot().online)
|
|
66
|
+
return;
|
|
67
|
+
clearTimeout(this.timer);
|
|
68
|
+
this.timer = undefined;
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
this.controller = controller;
|
|
71
|
+
this.lastCheck = Date.now();
|
|
72
|
+
// The explicit race also bounds native/custom fetch implementations that
|
|
73
|
+
// ignore AbortSignal. Late responses cannot overwrite a newer observation.
|
|
74
|
+
let rejectAbort = () => { };
|
|
75
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
76
|
+
rejectAbort = () => reject(new Error("Reachability check cancelled or timed out"));
|
|
77
|
+
controller.signal.addEventListener("abort", rejectAbort, { once: true });
|
|
78
|
+
});
|
|
79
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
80
|
+
try {
|
|
81
|
+
const response = await Promise.race([
|
|
82
|
+
Promise.resolve().then(() => this.fetchImpl(this.url, {
|
|
83
|
+
method: "HEAD", cache: "no-store", credentials: "omit",
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
})),
|
|
86
|
+
aborted,
|
|
87
|
+
]);
|
|
88
|
+
if (this.controller === controller) {
|
|
89
|
+
this.setStatus(response.status > 0 ? "reachable" : "unreachable");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
if (this.controller === controller)
|
|
94
|
+
this.setStatus("unreachable");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
controller.signal.removeEventListener("abort", rejectAbort);
|
|
99
|
+
if (this.controller === controller) {
|
|
100
|
+
this.controller = undefined;
|
|
101
|
+
if (this.listeners.size)
|
|
102
|
+
this.schedule(this.status === "reachable" ? 30000 : 5000);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.ServiceReachabilityMonitor = ServiceReachabilityMonitor;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import type { NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import type { AgentsApi } from "../apis/agents-api.js";
|
|
3
4
|
import type { AggregatesApi } from "../apis/aggregates-api.js";
|
|
@@ -79,6 +80,8 @@ export interface DataClientStatus {
|
|
|
79
80
|
export interface DataClient {
|
|
80
81
|
/** Optional platform source; React hooks otherwise use browser network status. */
|
|
81
82
|
readonly networkStatus?: NetworkStatusSource;
|
|
83
|
+
/** Optional data API reachability. Polls only while explicitly subscribed. */
|
|
84
|
+
readonly reachability?: ServiceReachabilitySource;
|
|
82
85
|
readonly agents: AgentsApiLike;
|
|
83
86
|
readonly channels: ChannelsApiLike;
|
|
84
87
|
readonly messages: MessagesApiLike;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import { type NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import { OrganisationsApi } from "../apis/organisations-api.js";
|
|
3
4
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
@@ -8,6 +9,7 @@ import type { AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike,
|
|
|
8
9
|
import { DooverStatsCollector, type DooverStatsSnapshot } from "./stats.js";
|
|
9
10
|
export declare class DooverClient implements DataClient {
|
|
10
11
|
readonly networkStatus: NetworkStatusSource;
|
|
12
|
+
readonly reachability: ServiceReachabilitySource | undefined;
|
|
11
13
|
readonly auth: DooverAuth;
|
|
12
14
|
readonly rest: RestClient;
|
|
13
15
|
readonly viewer: DooverDataProvider;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ServiceReachabilityMonitor } from "./service-reachability.js";
|
|
1
2
|
import { browserNetworkStatus } from "./network-status.js";
|
|
2
3
|
import { AgentsApi } from "../apis/agents-api.js";
|
|
3
4
|
import { AggregatesApi } from "../apis/aggregates-api.js";
|
|
@@ -47,7 +48,12 @@ export class DooverClient {
|
|
|
47
48
|
},
|
|
48
49
|
};
|
|
49
50
|
const stamper = new ProvenanceStamper(this.identity);
|
|
50
|
-
|
|
51
|
+
const reachability = config.reachability === false ? undefined : new ServiceReachabilityMonitor(
|
|
52
|
+
// Probe inside the API prefix: the bare prefix may route to a static CDN
|
|
53
|
+
// fallback without CORS, even though every real API request succeeds.
|
|
54
|
+
config.reachability?.probeUrl ?? `${config.dataRestUrl.replace(/\/+$/, "")}/`, this.networkStatus, config.fetchImpl ?? ((...args) => fetch(...args)));
|
|
55
|
+
this.reachability = reachability;
|
|
56
|
+
this.rest = new RestClient(config, this.auth, () => reachability?.requestFailed());
|
|
51
57
|
this.gatewayImpl = new GatewayClient(config, this.auth);
|
|
52
58
|
this.gatewayImpl.setProvenanceHook((value, ctx) => stamper.stampGatewayEvent(value, ctx));
|
|
53
59
|
this.gateway = this.gatewayImpl;
|
|
@@ -4,3 +4,4 @@ export type { DooverClientConfig } from "../http/rest-client.js";
|
|
|
4
4
|
export type * from "./data-client.js";
|
|
5
5
|
export { browserNetworkStatus, createNetworkStatusStore } from "./network-status.js";
|
|
6
6
|
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./network-status.js";
|
|
7
|
+
export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./service-reachability.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceReachabilitySource } from "./service-reachability.js";
|
|
1
2
|
import { type NetworkStatusSource } from "./network-status.js";
|
|
2
3
|
import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
3
4
|
import type { Capability } from "./capabilities.js";
|
|
@@ -99,6 +100,7 @@ export interface OfflineDataClientOptions {
|
|
|
99
100
|
}
|
|
100
101
|
export declare class OfflineDataClient implements DataClient {
|
|
101
102
|
readonly networkStatus: NetworkStatusSource;
|
|
103
|
+
readonly reachability: ServiceReachabilitySource | undefined;
|
|
102
104
|
private unsubscribeNetwork?;
|
|
103
105
|
readonly agents: AgentsApiLike;
|
|
104
106
|
readonly channels: ChannelsApiLike;
|
|
@@ -54,6 +54,7 @@ export class OfflineDataClient {
|
|
|
54
54
|
this.offlineStatus = INITIAL_OFFLINE_STATUS;
|
|
55
55
|
this.persistQueue = Promise.resolve();
|
|
56
56
|
this.client = options.client;
|
|
57
|
+
this.reachability = options.client.reachability;
|
|
57
58
|
this.networkStatus = options.networkStatus ?? options.client.networkStatus ?? browserNetworkStatus;
|
|
58
59
|
this.storage = options.storage;
|
|
59
60
|
this.scope = options.scope;
|
|
@@ -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 with a trailing slash.
|
|
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,9 +1,12 @@
|
|
|
1
|
+
import type { ReachabilityOptions } from "../client/service-reachability.js";
|
|
1
2
|
import type { NetworkStatusSource } from "../client/network-status.js";
|
|
2
3
|
import type { AuthConfig } from "../auth/build-auth.js";
|
|
3
4
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
4
5
|
import type { DooverStatsCollector } from "../client/stats.js";
|
|
5
6
|
export interface DooverClientConfig extends AuthConfig {
|
|
6
7
|
networkStatus?: NetworkStatusSource;
|
|
8
|
+
/** Data API reachability checks while reachability is observed. Set false to disable. */
|
|
9
|
+
reachability?: ReachabilityOptions | false;
|
|
7
10
|
dataRestUrl: string;
|
|
8
11
|
controlApiUrl: string;
|
|
9
12
|
dataWssUrl: string;
|
|
@@ -40,10 +43,11 @@ export interface RequestOptions {
|
|
|
40
43
|
type QueryValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
|
|
41
44
|
type QueryParams = Record<string, QueryValue> | object;
|
|
42
45
|
export declare class RestClient {
|
|
46
|
+
private readonly onTransportFailure?;
|
|
43
47
|
readonly config: Required<Pick<DooverClientConfig, "dataRestUrl" | "controlApiUrl" | "dataWssUrl" | "sharing" | "impersonateUserStorageKey">> & Pick<DooverClientConfig, "organisationId" | "fetchImpl" | "webSocketImpl" | "webSocketFactory">;
|
|
44
48
|
readonly auth: DooverAuth | null;
|
|
45
49
|
private stats;
|
|
46
|
-
constructor(config: DooverClientConfig, auth?: DooverAuth);
|
|
50
|
+
constructor(config: DooverClientConfig, auth?: DooverAuth, onTransportFailure?: (() => void) | undefined);
|
|
47
51
|
/** Attach a stats collector. Call with `null` to detach. */
|
|
48
52
|
setStats(stats: DooverStatsCollector | null): void;
|
|
49
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 });
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -62,3 +62,4 @@ export { DV_AUDIT_CHANNEL } from "./types/audit.js";
|
|
|
62
62
|
export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake.js";
|
|
63
63
|
export { browserNetworkStatus, createNetworkStatusStore } from "./client/network-status.js";
|
|
64
64
|
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./client/network-status.js";
|
|
65
|
+
export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./client/service-reachability.js";
|
package/dist/esm/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"doover-js","version":"0.13.
|
|
1
|
+
{"name":"doover-js","version":"0.13.2","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";
|
package/dist/esm/react/index.js
CHANGED
|
@@ -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";
|
|
@@ -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,9 +1,12 @@
|
|
|
1
|
+
import type { ReachabilityOptions } from "../client/service-reachability.js";
|
|
1
2
|
import type { NetworkStatusSource } from "../client/network-status.js";
|
|
2
3
|
import type { AuthConfig } from "../auth/build-auth.js";
|
|
3
4
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
4
5
|
import type { DooverStatsCollector } from "../client/stats.js";
|
|
5
6
|
export interface DooverClientConfig extends AuthConfig {
|
|
6
7
|
networkStatus?: NetworkStatusSource;
|
|
8
|
+
/** Data API reachability checks while reachability is observed. Set false to disable. */
|
|
9
|
+
reachability?: ReachabilityOptions | false;
|
|
7
10
|
dataRestUrl: string;
|
|
8
11
|
controlApiUrl: string;
|
|
9
12
|
dataWssUrl: string;
|
|
@@ -40,10 +43,11 @@ export interface RequestOptions {
|
|
|
40
43
|
type QueryValue = string | number | boolean | null | undefined | Array<string | number | boolean>;
|
|
41
44
|
type QueryParams = Record<string, QueryValue> | object;
|
|
42
45
|
export declare class RestClient {
|
|
46
|
+
private readonly onTransportFailure?;
|
|
43
47
|
readonly config: Required<Pick<DooverClientConfig, "dataRestUrl" | "controlApiUrl" | "dataWssUrl" | "sharing" | "impersonateUserStorageKey">> & Pick<DooverClientConfig, "organisationId" | "fetchImpl" | "webSocketImpl" | "webSocketFactory">;
|
|
44
48
|
readonly auth: DooverAuth | null;
|
|
45
49
|
private stats;
|
|
46
|
-
constructor(config: DooverClientConfig, auth?: DooverAuth);
|
|
50
|
+
constructor(config: DooverClientConfig, auth?: DooverAuth, onTransportFailure?: (() => void) | undefined);
|
|
47
51
|
/** Attach a stats collector. Call with `null` to detach. */
|
|
48
52
|
setStats(stats: DooverStatsCollector | null): void;
|
|
49
53
|
request<T>(options: RequestOptions): Promise<T>;
|
package/dist/http/rest-client.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
@@ -62,3 +62,4 @@ export { DV_AUDIT_CHANNEL } from "./types/audit.js";
|
|
|
62
62
|
export { addTimestampToMessage, extractSnowflakeId, generateSnowflakeIdAtTime, } from "./utils/snowflake.js";
|
|
63
63
|
export { browserNetworkStatus, createNetworkStatusStore } from "./client/network-status.js";
|
|
64
64
|
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./client/network-status.js";
|
|
65
|
+
export type { ServiceReachability, ServiceReachabilitySource, ReachabilityOptions } from "./client/service-reachability.js";
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/react/index.js
CHANGED
|
@@ -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; } });
|
|
@@ -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.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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
|
}
|