doover-js 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -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 +2 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +4 -1
- package/dist/client/network-status.d.ts +17 -0
- package/dist/client/network-status.js +70 -0
- package/dist/client/offline-cache.d.ts +7 -2
- package/dist/client/offline-cache.js +18 -6
- 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 +2 -0
- package/dist/esm/client/index.d.ts +2 -0
- package/dist/esm/client/index.js +1 -0
- package/dist/esm/client/network-status.d.ts +17 -0
- package/dist/esm/client/network-status.js +66 -0
- package/dist/esm/client/offline-cache.d.ts +7 -2
- package/dist/esm/client/offline-cache.js +18 -6
- package/dist/esm/http/rest-client.d.ts +2 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/react/useOfflineStatus.js +30 -16
- package/dist/http/rest-client.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/react/useOfflineStatus.js +29 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -389,3 +389,30 @@ const statuses = useClientStatus();
|
|
|
389
389
|
`*Api` resource methods accept either positional arguments or a `ChannelIdentifier` / `{ agentId }` object on every call form.
|
|
390
390
|
|
|
391
391
|
`client.rpc.send` adds optional `signal: AbortSignal` and `timeoutMs: number` and rejects with `DooverRpcError` (with `.status` and `.request`) instead of the bare status string.
|
|
392
|
+
|
|
393
|
+
## Network availability and offline cache status
|
|
394
|
+
|
|
395
|
+
`useOfflineStatus()` reports browser network changes for every client, including
|
|
396
|
+
`DooverClient` without an offline cache. It updates on initial offline load and
|
|
397
|
+
online/offline events even when no requests run. `online` describes the platform's
|
|
398
|
+
network signal; it does not prove that the Doover server is reachable.
|
|
399
|
+
`useClientStatus()` separately reports the gateway connection.
|
|
400
|
+
|
|
401
|
+
`OfflineDataClient` adds cache metadata such as `isOfflineFallback` and `cachedAt`.
|
|
402
|
+
These fields describe the last cache operation, not every item on the page. Its
|
|
403
|
+
snapshots retain object identity between changes for React external-store hooks.
|
|
404
|
+
Network transitions preserve cache metadata until another operation updates it.
|
|
405
|
+
|
|
406
|
+
Both clients accept `networkStatus`, a `NetworkStatusSource` with `getSnapshot()`
|
|
407
|
+
and `subscribe(listener)`. The offline wrapper inherits its underlying client's
|
|
408
|
+
source unless overridden. Sources return a stable `{ online, at }` snapshot and
|
|
409
|
+
notify subscribers when it changes. The exported `browserNetworkStatus` is the
|
|
410
|
+
default; it attaches browser listeners while subscribed and releases them after
|
|
411
|
+
the last unsubscribe. Outside a browser it assumes online when no platform
|
|
412
|
+
signal exists.
|
|
413
|
+
|
|
414
|
+
Native applications can pass `createNetworkStatusStore(initialOnline)` and call
|
|
415
|
+
its `setOnline(online)` method from platform network callbacks. Gateway failures
|
|
416
|
+
must not update this source. The legacy `OfflineDataClient.isOnline` callback
|
|
417
|
+
remains supported for request-time checks; a reactive source also updates the UI
|
|
418
|
+
between requests.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import type { AgentsApi } from "../apis/agents-api.js";
|
|
2
3
|
import type { AggregatesApi } from "../apis/aggregates-api.js";
|
|
3
4
|
import type { AlarmsApi } from "../apis/alarms-api.js";
|
|
@@ -76,6 +77,8 @@ export interface DataClientStatus {
|
|
|
76
77
|
* later; the invariant is that `DooverClient` always satisfies it.
|
|
77
78
|
*/
|
|
78
79
|
export interface DataClient {
|
|
80
|
+
/** Optional platform source; React hooks otherwise use browser network status. */
|
|
81
|
+
readonly networkStatus?: NetworkStatusSource;
|
|
79
82
|
readonly agents: AgentsApiLike;
|
|
80
83
|
readonly channels: ChannelsApiLike;
|
|
81
84
|
readonly messages: MessagesApiLike;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import { OrganisationsApi } from "../apis/organisations-api.js";
|
|
2
3
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
3
4
|
import { RestClient, type DooverClientConfig } from "../http/rest-client.js";
|
|
@@ -6,6 +7,7 @@ import { type Capability } from "./capabilities.js";
|
|
|
6
7
|
import type { AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, AgentScope, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
7
8
|
import { DooverStatsCollector, type DooverStatsSnapshot } from "./stats.js";
|
|
8
9
|
export declare class DooverClient implements DataClient {
|
|
10
|
+
readonly networkStatus: NetworkStatusSource;
|
|
9
11
|
readonly auth: DooverAuth;
|
|
10
12
|
readonly rest: RestClient;
|
|
11
13
|
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 network_status_js_1 = require("./network-status.js");
|
|
4
5
|
const agents_api_js_1 = require("../apis/agents-api.js");
|
|
5
6
|
const aggregates_api_js_1 = require("../apis/aggregates-api.js");
|
|
6
7
|
const alarms_api_js_1 = require("../apis/alarms-api.js");
|
|
@@ -25,6 +26,7 @@ const stats_js_1 = require("./stats.js");
|
|
|
25
26
|
const ALL_CAPS_SET = new Set(capabilities_js_1.ALL_CAPABILITIES);
|
|
26
27
|
class DooverClient {
|
|
27
28
|
constructor(config) {
|
|
29
|
+
this.networkStatus = config.networkStatus ?? network_status_js_1.browserNetworkStatus;
|
|
28
30
|
this.auth = (0, build_auth_js_1.buildAuth)({
|
|
29
31
|
auth: config.auth,
|
|
30
32
|
profile: config.profile,
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { DooverClient } from "./doover-client.js";
|
|
|
2
2
|
export { getDooverClient, peekDooverClient, resetDooverClient } from "./singleton.js";
|
|
3
3
|
export type { DooverClientConfig } from "../http/rest-client.js";
|
|
4
4
|
export type * from "./data-client.js";
|
|
5
|
+
export { browserNetworkStatus, createNetworkStatusStore } from "./network-status.js";
|
|
6
|
+
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./network-status.js";
|
package/dist/client/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.DooverClient = void 0;
|
|
3
|
+
exports.createNetworkStatusStore = exports.browserNetworkStatus = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.DooverClient = void 0;
|
|
4
4
|
var doover_client_js_1 = require("./doover-client.js");
|
|
5
5
|
Object.defineProperty(exports, "DooverClient", { enumerable: true, get: function () { return doover_client_js_1.DooverClient; } });
|
|
6
6
|
var singleton_js_1 = require("./singleton.js");
|
|
7
7
|
Object.defineProperty(exports, "getDooverClient", { enumerable: true, get: function () { return singleton_js_1.getDooverClient; } });
|
|
8
8
|
Object.defineProperty(exports, "peekDooverClient", { enumerable: true, get: function () { return singleton_js_1.peekDooverClient; } });
|
|
9
9
|
Object.defineProperty(exports, "resetDooverClient", { enumerable: true, get: function () { return singleton_js_1.resetDooverClient; } });
|
|
10
|
+
var network_status_js_1 = require("./network-status.js");
|
|
11
|
+
Object.defineProperty(exports, "browserNetworkStatus", { enumerable: true, get: function () { return network_status_js_1.browserNetworkStatus; } });
|
|
12
|
+
Object.defineProperty(exports, "createNetworkStatusStore", { enumerable: true, get: function () { return network_status_js_1.createNetworkStatusStore; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Network availability is independent of the Doover gateway connection. */
|
|
2
|
+
export interface NetworkStatusSnapshot {
|
|
3
|
+
readonly online: boolean;
|
|
4
|
+
readonly at: number;
|
|
5
|
+
}
|
|
6
|
+
/** Snapshots must retain their identity until the status changes. */
|
|
7
|
+
export interface NetworkStatusSource {
|
|
8
|
+
getSnapshot(): NetworkStatusSnapshot;
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
}
|
|
11
|
+
/** A source that native network callbacks can update. */
|
|
12
|
+
export declare function createNetworkStatusStore(initialOnline: boolean): {
|
|
13
|
+
getSnapshot: () => NetworkStatusSnapshot;
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
setOnline(online: boolean): void;
|
|
16
|
+
};
|
|
17
|
+
export declare const browserNetworkStatus: NetworkStatusSource;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.browserNetworkStatus = void 0;
|
|
4
|
+
exports.createNetworkStatusStore = createNetworkStatusStore;
|
|
5
|
+
/** A source that native network callbacks can update. */
|
|
6
|
+
function createNetworkStatusStore(initialOnline) {
|
|
7
|
+
let snapshot = { online: initialOnline, at: Date.now() };
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
return {
|
|
10
|
+
getSnapshot: () => snapshot,
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
listeners.add(listener);
|
|
13
|
+
return () => { listeners.delete(listener); };
|
|
14
|
+
},
|
|
15
|
+
setOnline(online) {
|
|
16
|
+
if (online === snapshot.online)
|
|
17
|
+
return;
|
|
18
|
+
snapshot = { online, at: Date.now() };
|
|
19
|
+
listeners.forEach((listener) => listener());
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function readBrowserOnline() {
|
|
24
|
+
const nav = typeof window !== "undefined" ? window.navigator
|
|
25
|
+
: typeof navigator !== "undefined" ? navigator : undefined;
|
|
26
|
+
return typeof nav?.onLine === "boolean" ? nav.onLine : true;
|
|
27
|
+
}
|
|
28
|
+
// Listen only while observed. getSnapshot also refreshes after periods with
|
|
29
|
+
// no subscribers, including initial offline loads. No browser globals at import.
|
|
30
|
+
function createBrowserNetworkStatus() {
|
|
31
|
+
let snapshot = { online: true, at: 0 };
|
|
32
|
+
const listeners = new Set();
|
|
33
|
+
let detach;
|
|
34
|
+
const getSnapshot = () => {
|
|
35
|
+
const online = readBrowserOnline();
|
|
36
|
+
if (online !== snapshot.online)
|
|
37
|
+
snapshot = { online, at: Date.now() };
|
|
38
|
+
return snapshot;
|
|
39
|
+
};
|
|
40
|
+
const refresh = () => {
|
|
41
|
+
getSnapshot();
|
|
42
|
+
// A request may already have read the new snapshot before this event.
|
|
43
|
+
// Still notify mounted consumers; React ignores an unchanged snapshot.
|
|
44
|
+
listeners.forEach((listener) => listener());
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
getSnapshot,
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
listeners.add(listener);
|
|
50
|
+
if (!detach && typeof window !== "undefined") {
|
|
51
|
+
const target = window;
|
|
52
|
+
target.addEventListener("online", refresh);
|
|
53
|
+
target.addEventListener("offline", refresh);
|
|
54
|
+
detach = () => {
|
|
55
|
+
target.removeEventListener("online", refresh);
|
|
56
|
+
target.removeEventListener("offline", refresh);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
refresh();
|
|
60
|
+
return () => {
|
|
61
|
+
listeners.delete(listener);
|
|
62
|
+
if (!listeners.size) {
|
|
63
|
+
detach?.();
|
|
64
|
+
detach = undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
exports.browserNetworkStatus = createBrowserNetworkStatus();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
2
3
|
import type { Capability } from "./capabilities.js";
|
|
3
4
|
export declare const DEFAULT_OFFLINE_RETENTION_MS: number;
|
|
@@ -83,18 +84,22 @@ export declare class MemoryOfflineStorageAdapter implements OfflineStorageAdapte
|
|
|
83
84
|
deleteBlob(key: string): Promise<void>;
|
|
84
85
|
}
|
|
85
86
|
export interface OfflineDataClientOptions {
|
|
87
|
+
/** Reactive platform signal; defaults to the wrapped client's source. */
|
|
88
|
+
networkStatus?: NetworkStatusSource;
|
|
86
89
|
client: DataClient;
|
|
87
90
|
storage: OfflineStorageAdapter;
|
|
88
91
|
scope: OfflineCacheScope;
|
|
89
92
|
policies?: OfflineChannelPolicy[];
|
|
90
93
|
defaultRetentionMs?: number;
|
|
91
94
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
95
|
+
* Legacy pull-only override. Prefer networkStatus for reactive updates
|
|
96
|
+
* between requests. When omitted, the wrapper reads its network source.
|
|
94
97
|
*/
|
|
95
98
|
isOnline?: () => boolean;
|
|
96
99
|
}
|
|
97
100
|
export declare class OfflineDataClient implements DataClient {
|
|
101
|
+
readonly networkStatus: NetworkStatusSource;
|
|
102
|
+
private unsubscribeNetwork?;
|
|
98
103
|
readonly agents: AgentsApiLike;
|
|
99
104
|
readonly channels: ChannelsApiLike;
|
|
100
105
|
readonly messages: MessagesApiLike;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OfflineDataClient = exports.MemoryOfflineStorageAdapter = exports.DEFAULT_OFFLINE_RETENTION_MS = void 0;
|
|
4
|
+
const network_status_js_1 = require("./network-status.js");
|
|
4
5
|
const errors_js_1 = require("./errors.js");
|
|
5
6
|
const request_options_js_1 = require("./request-options.js");
|
|
6
7
|
exports.DEFAULT_OFFLINE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -57,6 +58,7 @@ class OfflineDataClient {
|
|
|
57
58
|
this.offlineStatus = INITIAL_OFFLINE_STATUS;
|
|
58
59
|
this.persistQueue = Promise.resolve();
|
|
59
60
|
this.client = options.client;
|
|
61
|
+
this.networkStatus = options.networkStatus ?? options.client.networkStatus ?? network_status_js_1.browserNetworkStatus;
|
|
60
62
|
this.storage = options.storage;
|
|
61
63
|
this.scope = options.scope;
|
|
62
64
|
this.defaultRetentionMs = options.defaultRetentionMs ?? exports.DEFAULT_OFFLINE_RETENTION_MS;
|
|
@@ -90,19 +92,30 @@ class OfflineDataClient {
|
|
|
90
92
|
return this.policies;
|
|
91
93
|
}
|
|
92
94
|
getOfflineStatus() {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
online:
|
|
96
|
-
}
|
|
95
|
+
const online = this.isOnline();
|
|
96
|
+
if (online !== this.offlineStatus.online) {
|
|
97
|
+
this.offlineStatus = { ...this.offlineStatus, online, at: Date.now() };
|
|
98
|
+
}
|
|
99
|
+
return this.offlineStatus;
|
|
97
100
|
}
|
|
98
101
|
onOfflineStatusChange(listener) {
|
|
99
102
|
this.offlineStatusListeners.add(listener);
|
|
103
|
+
if (!this.unsubscribeNetwork) {
|
|
104
|
+
this.unsubscribeNetwork = this.networkStatus.subscribe(() => {
|
|
105
|
+
const status = this.getOfflineStatus();
|
|
106
|
+
this.offlineStatusListeners.forEach((callback) => callback(status));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
100
109
|
let active = true;
|
|
101
110
|
return () => {
|
|
102
111
|
if (!active)
|
|
103
112
|
return;
|
|
104
113
|
active = false;
|
|
105
114
|
this.offlineStatusListeners.delete(listener);
|
|
115
|
+
if (!this.offlineStatusListeners.size) {
|
|
116
|
+
this.unsubscribeNetwork?.();
|
|
117
|
+
this.unsubscribeNetwork = undefined;
|
|
118
|
+
}
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
async clearScope(scope = this.scope) {
|
|
@@ -682,8 +695,7 @@ class OfflineDataClient {
|
|
|
682
695
|
isOnline() {
|
|
683
696
|
if (this.isOnlineFn)
|
|
684
697
|
return this.isOnlineFn();
|
|
685
|
-
|
|
686
|
-
return nav?.onLine ?? true;
|
|
698
|
+
return this.networkStatus.getSnapshot().online;
|
|
687
699
|
}
|
|
688
700
|
setOfflineStatus(status) {
|
|
689
701
|
this.offlineStatus = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import type { AgentsApi } from "../apis/agents-api.js";
|
|
2
3
|
import type { AggregatesApi } from "../apis/aggregates-api.js";
|
|
3
4
|
import type { AlarmsApi } from "../apis/alarms-api.js";
|
|
@@ -76,6 +77,8 @@ export interface DataClientStatus {
|
|
|
76
77
|
* later; the invariant is that `DooverClient` always satisfies it.
|
|
77
78
|
*/
|
|
78
79
|
export interface DataClient {
|
|
80
|
+
/** Optional platform source; React hooks otherwise use browser network status. */
|
|
81
|
+
readonly networkStatus?: NetworkStatusSource;
|
|
79
82
|
readonly agents: AgentsApiLike;
|
|
80
83
|
readonly channels: ChannelsApiLike;
|
|
81
84
|
readonly messages: MessagesApiLike;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import { OrganisationsApi } from "../apis/organisations-api.js";
|
|
2
3
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
3
4
|
import { RestClient, type DooverClientConfig } from "../http/rest-client.js";
|
|
@@ -6,6 +7,7 @@ import { type Capability } from "./capabilities.js";
|
|
|
6
7
|
import type { AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, AgentScope, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
7
8
|
import { DooverStatsCollector, type DooverStatsSnapshot } from "./stats.js";
|
|
8
9
|
export declare class DooverClient implements DataClient {
|
|
10
|
+
readonly networkStatus: NetworkStatusSource;
|
|
9
11
|
readonly auth: DooverAuth;
|
|
10
12
|
readonly rest: RestClient;
|
|
11
13
|
readonly viewer: DooverDataProvider;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { browserNetworkStatus } from "./network-status.js";
|
|
1
2
|
import { AgentsApi } from "../apis/agents-api.js";
|
|
2
3
|
import { AggregatesApi } from "../apis/aggregates-api.js";
|
|
3
4
|
import { AlarmsApi } from "../apis/alarms-api.js";
|
|
@@ -22,6 +23,7 @@ import { DooverStatsCollector } from "./stats.js";
|
|
|
22
23
|
const ALL_CAPS_SET = new Set(ALL_CAPABILITIES);
|
|
23
24
|
export class DooverClient {
|
|
24
25
|
constructor(config) {
|
|
26
|
+
this.networkStatus = config.networkStatus ?? browserNetworkStatus;
|
|
25
27
|
this.auth = buildAuth({
|
|
26
28
|
auth: config.auth,
|
|
27
29
|
profile: config.profile,
|
|
@@ -2,3 +2,5 @@ export { DooverClient } from "./doover-client.js";
|
|
|
2
2
|
export { getDooverClient, peekDooverClient, resetDooverClient } from "./singleton.js";
|
|
3
3
|
export type { DooverClientConfig } from "../http/rest-client.js";
|
|
4
4
|
export type * from "./data-client.js";
|
|
5
|
+
export { browserNetworkStatus, createNetworkStatusStore } from "./network-status.js";
|
|
6
|
+
export type { NetworkStatusSource, NetworkStatusSnapshot } from "./network-status.js";
|
package/dist/esm/client/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Network availability is independent of the Doover gateway connection. */
|
|
2
|
+
export interface NetworkStatusSnapshot {
|
|
3
|
+
readonly online: boolean;
|
|
4
|
+
readonly at: number;
|
|
5
|
+
}
|
|
6
|
+
/** Snapshots must retain their identity until the status changes. */
|
|
7
|
+
export interface NetworkStatusSource {
|
|
8
|
+
getSnapshot(): NetworkStatusSnapshot;
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
}
|
|
11
|
+
/** A source that native network callbacks can update. */
|
|
12
|
+
export declare function createNetworkStatusStore(initialOnline: boolean): {
|
|
13
|
+
getSnapshot: () => NetworkStatusSnapshot;
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
setOnline(online: boolean): void;
|
|
16
|
+
};
|
|
17
|
+
export declare const browserNetworkStatus: NetworkStatusSource;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A source that native network callbacks can update. */
|
|
2
|
+
export function createNetworkStatusStore(initialOnline) {
|
|
3
|
+
let snapshot = { online: initialOnline, at: Date.now() };
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
return {
|
|
6
|
+
getSnapshot: () => snapshot,
|
|
7
|
+
subscribe(listener) {
|
|
8
|
+
listeners.add(listener);
|
|
9
|
+
return () => { listeners.delete(listener); };
|
|
10
|
+
},
|
|
11
|
+
setOnline(online) {
|
|
12
|
+
if (online === snapshot.online)
|
|
13
|
+
return;
|
|
14
|
+
snapshot = { online, at: Date.now() };
|
|
15
|
+
listeners.forEach((listener) => listener());
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function readBrowserOnline() {
|
|
20
|
+
const nav = typeof window !== "undefined" ? window.navigator
|
|
21
|
+
: typeof navigator !== "undefined" ? navigator : undefined;
|
|
22
|
+
return typeof nav?.onLine === "boolean" ? nav.onLine : true;
|
|
23
|
+
}
|
|
24
|
+
// Listen only while observed. getSnapshot also refreshes after periods with
|
|
25
|
+
// no subscribers, including initial offline loads. No browser globals at import.
|
|
26
|
+
function createBrowserNetworkStatus() {
|
|
27
|
+
let snapshot = { online: true, at: 0 };
|
|
28
|
+
const listeners = new Set();
|
|
29
|
+
let detach;
|
|
30
|
+
const getSnapshot = () => {
|
|
31
|
+
const online = readBrowserOnline();
|
|
32
|
+
if (online !== snapshot.online)
|
|
33
|
+
snapshot = { online, at: Date.now() };
|
|
34
|
+
return snapshot;
|
|
35
|
+
};
|
|
36
|
+
const refresh = () => {
|
|
37
|
+
getSnapshot();
|
|
38
|
+
// A request may already have read the new snapshot before this event.
|
|
39
|
+
// Still notify mounted consumers; React ignores an unchanged snapshot.
|
|
40
|
+
listeners.forEach((listener) => listener());
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
getSnapshot,
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
if (!detach && typeof window !== "undefined") {
|
|
47
|
+
const target = window;
|
|
48
|
+
target.addEventListener("online", refresh);
|
|
49
|
+
target.addEventListener("offline", refresh);
|
|
50
|
+
detach = () => {
|
|
51
|
+
target.removeEventListener("online", refresh);
|
|
52
|
+
target.removeEventListener("offline", refresh);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
refresh();
|
|
56
|
+
return () => {
|
|
57
|
+
listeners.delete(listener);
|
|
58
|
+
if (!listeners.size) {
|
|
59
|
+
detach?.();
|
|
60
|
+
detach = undefined;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export const browserNetworkStatus = createBrowserNetworkStatus();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NetworkStatusSource } from "./network-status.js";
|
|
1
2
|
import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client.js";
|
|
2
3
|
import type { Capability } from "./capabilities.js";
|
|
3
4
|
export declare const DEFAULT_OFFLINE_RETENTION_MS: number;
|
|
@@ -83,18 +84,22 @@ export declare class MemoryOfflineStorageAdapter implements OfflineStorageAdapte
|
|
|
83
84
|
deleteBlob(key: string): Promise<void>;
|
|
84
85
|
}
|
|
85
86
|
export interface OfflineDataClientOptions {
|
|
87
|
+
/** Reactive platform signal; defaults to the wrapped client's source. */
|
|
88
|
+
networkStatus?: NetworkStatusSource;
|
|
86
89
|
client: DataClient;
|
|
87
90
|
storage: OfflineStorageAdapter;
|
|
88
91
|
scope: OfflineCacheScope;
|
|
89
92
|
policies?: OfflineChannelPolicy[];
|
|
90
93
|
defaultRetentionMs?: number;
|
|
91
94
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
95
|
+
* Legacy pull-only override. Prefer networkStatus for reactive updates
|
|
96
|
+
* between requests. When omitted, the wrapper reads its network source.
|
|
94
97
|
*/
|
|
95
98
|
isOnline?: () => boolean;
|
|
96
99
|
}
|
|
97
100
|
export declare class OfflineDataClient implements DataClient {
|
|
101
|
+
readonly networkStatus: NetworkStatusSource;
|
|
102
|
+
private unsubscribeNetwork?;
|
|
98
103
|
readonly agents: AgentsApiLike;
|
|
99
104
|
readonly channels: ChannelsApiLike;
|
|
100
105
|
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,7 @@ export class OfflineDataClient {
|
|
|
53
54
|
this.offlineStatus = INITIAL_OFFLINE_STATUS;
|
|
54
55
|
this.persistQueue = Promise.resolve();
|
|
55
56
|
this.client = options.client;
|
|
57
|
+
this.networkStatus = options.networkStatus ?? options.client.networkStatus ?? browserNetworkStatus;
|
|
56
58
|
this.storage = options.storage;
|
|
57
59
|
this.scope = options.scope;
|
|
58
60
|
this.defaultRetentionMs = options.defaultRetentionMs ?? DEFAULT_OFFLINE_RETENTION_MS;
|
|
@@ -86,19 +88,30 @@ export class OfflineDataClient {
|
|
|
86
88
|
return this.policies;
|
|
87
89
|
}
|
|
88
90
|
getOfflineStatus() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
online:
|
|
92
|
-
}
|
|
91
|
+
const online = this.isOnline();
|
|
92
|
+
if (online !== this.offlineStatus.online) {
|
|
93
|
+
this.offlineStatus = { ...this.offlineStatus, online, at: Date.now() };
|
|
94
|
+
}
|
|
95
|
+
return this.offlineStatus;
|
|
93
96
|
}
|
|
94
97
|
onOfflineStatusChange(listener) {
|
|
95
98
|
this.offlineStatusListeners.add(listener);
|
|
99
|
+
if (!this.unsubscribeNetwork) {
|
|
100
|
+
this.unsubscribeNetwork = this.networkStatus.subscribe(() => {
|
|
101
|
+
const status = this.getOfflineStatus();
|
|
102
|
+
this.offlineStatusListeners.forEach((callback) => callback(status));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
96
105
|
let active = true;
|
|
97
106
|
return () => {
|
|
98
107
|
if (!active)
|
|
99
108
|
return;
|
|
100
109
|
active = false;
|
|
101
110
|
this.offlineStatusListeners.delete(listener);
|
|
111
|
+
if (!this.offlineStatusListeners.size) {
|
|
112
|
+
this.unsubscribeNetwork?.();
|
|
113
|
+
this.unsubscribeNetwork = undefined;
|
|
114
|
+
}
|
|
102
115
|
};
|
|
103
116
|
}
|
|
104
117
|
async clearScope(scope = this.scope) {
|
|
@@ -678,8 +691,7 @@ export class OfflineDataClient {
|
|
|
678
691
|
isOnline() {
|
|
679
692
|
if (this.isOnlineFn)
|
|
680
693
|
return this.isOnlineFn();
|
|
681
|
-
|
|
682
|
-
return nav?.onLine ?? true;
|
|
694
|
+
return this.networkStatus.getSnapshot().online;
|
|
683
695
|
}
|
|
684
696
|
setOfflineStatus(status) {
|
|
685
697
|
this.offlineStatus = {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { NetworkStatusSource } from "../client/network-status.js";
|
|
1
2
|
import type { AuthConfig } from "../auth/build-auth.js";
|
|
2
3
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
3
4
|
import type { DooverStatsCollector } from "../client/stats.js";
|
|
4
5
|
export interface DooverClientConfig extends AuthConfig {
|
|
6
|
+
networkStatus?: NetworkStatusSource;
|
|
5
7
|
dataRestUrl: string;
|
|
6
8
|
controlApiUrl: string;
|
|
7
9
|
dataWssUrl: string;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -60,3 +60,5 @@ 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";
|
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";
|
package/dist/esm/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","sideEffects":false}
|
|
1
|
+
{"name":"doover-js","version":"0.13.0","type":"module","sideEffects":false}
|
|
@@ -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
|
|
5
|
-
typeof client === "
|
|
6
|
-
typeof client.
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { NetworkStatusSource } from "../client/network-status.js";
|
|
1
2
|
import type { AuthConfig } from "../auth/build-auth.js";
|
|
2
3
|
import type { DooverAuth } from "../auth/doover-auth.js";
|
|
3
4
|
import type { DooverStatsCollector } from "../client/stats.js";
|
|
4
5
|
export interface DooverClientConfig extends AuthConfig {
|
|
6
|
+
networkStatus?: NetworkStatusSource;
|
|
5
7
|
dataRestUrl: string;
|
|
6
8
|
controlApiUrl: string;
|
|
7
9
|
dataWssUrl: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -60,3 +60,5 @@ 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";
|
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; } });
|
|
@@ -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
|
|
9
|
-
typeof client === "
|
|
10
|
-
typeof client.
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
}
|