doover-js 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apis/_args.js +9 -4
- package/dist/apis/agents-api.d.ts +3 -0
- package/dist/apis/agents-api.js +8 -7
- package/dist/apis/aggregates-api.d.ts +11 -0
- package/dist/apis/channels-api.d.ts +19 -0
- package/dist/apis/messages-api.d.ts +6 -0
- package/dist/client/errors.d.ts +9 -0
- package/dist/client/errors.js +21 -1
- package/dist/client/offline-cache.d.ts +162 -0
- package/dist/client/offline-cache.js +757 -0
- package/dist/client/request-options.d.ts +23 -0
- package/dist/client/request-options.js +30 -0
- package/dist/http/rest-client.js +4 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +10 -1
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +4 -1
- package/dist/react/useChannelAggregate.d.ts +3 -0
- package/dist/react/useChannelAggregate.js +3 -0
- package/dist/react/useChannelMessages.d.ts +3 -0
- package/dist/react/useChannelMessages.js +3 -1
- package/dist/react/useOfflineStatus.d.ts +8 -0
- package/dist/react/useOfflineStatus.js +27 -0
- package/dist/test/exports.test.js +5 -1
- package/dist/test/offline-cache.test.d.ts +1 -0
- package/dist/test/offline-cache.test.js +289 -0
- package/dist/test/react-exports.test.js +1 -0
- package/dist/types/common.d.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OfflineReadCacheOptions } from "./offline-cache";
|
|
2
|
+
export declare const REQUEST_OPTIONS_SYMBOL: unique symbol;
|
|
3
|
+
export interface DooverRequestOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Restrict a call to these source ids on a `MultiplexClient`.
|
|
6
|
+
*/
|
|
7
|
+
sources?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Offline cache behavior for this specific call. `false` opts out even when
|
|
10
|
+
* a stored channel policy would otherwise match.
|
|
11
|
+
*/
|
|
12
|
+
cache?: OfflineReadCacheOptions | false;
|
|
13
|
+
[REQUEST_OPTIONS_SYMBOL]?: true;
|
|
14
|
+
}
|
|
15
|
+
export declare function requestOptions<T extends Omit<DooverRequestOptions, typeof REQUEST_OPTIONS_SYMBOL>>(options: T): T & {
|
|
16
|
+
[REQUEST_OPTIONS_SYMBOL]: true;
|
|
17
|
+
};
|
|
18
|
+
export declare function isDooverRequestOptions(value: unknown): value is DooverRequestOptions;
|
|
19
|
+
export declare function splitRequestOptions(args: unknown[]): {
|
|
20
|
+
args: unknown[];
|
|
21
|
+
request?: DooverRequestOptions;
|
|
22
|
+
};
|
|
23
|
+
export declare function delegateRequestOptions(request?: DooverRequestOptions): DooverRequestOptions | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REQUEST_OPTIONS_SYMBOL = void 0;
|
|
4
|
+
exports.requestOptions = requestOptions;
|
|
5
|
+
exports.isDooverRequestOptions = isDooverRequestOptions;
|
|
6
|
+
exports.splitRequestOptions = splitRequestOptions;
|
|
7
|
+
exports.delegateRequestOptions = delegateRequestOptions;
|
|
8
|
+
exports.REQUEST_OPTIONS_SYMBOL = Symbol.for("doover.requestOptions");
|
|
9
|
+
function requestOptions(options) {
|
|
10
|
+
return { ...options, [exports.REQUEST_OPTIONS_SYMBOL]: true };
|
|
11
|
+
}
|
|
12
|
+
function isDooverRequestOptions(value) {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
14
|
+
return false;
|
|
15
|
+
const candidate = value;
|
|
16
|
+
return (candidate[exports.REQUEST_OPTIONS_SYMBOL] === true ||
|
|
17
|
+
Array.isArray(candidate.sources) ||
|
|
18
|
+
"cache" in candidate);
|
|
19
|
+
}
|
|
20
|
+
function splitRequestOptions(args) {
|
|
21
|
+
const last = args[args.length - 1];
|
|
22
|
+
if (!isDooverRequestOptions(last))
|
|
23
|
+
return { args };
|
|
24
|
+
return { args: args.slice(0, -1), request: last };
|
|
25
|
+
}
|
|
26
|
+
function delegateRequestOptions(request) {
|
|
27
|
+
if (!request?.sources)
|
|
28
|
+
return undefined;
|
|
29
|
+
return requestOptions({ sources: request.sources });
|
|
30
|
+
}
|
package/dist/http/rest-client.js
CHANGED
|
@@ -138,7 +138,10 @@ class RestClient {
|
|
|
138
138
|
return response.json();
|
|
139
139
|
}
|
|
140
140
|
if (contentType.includes("application/octet-stream") ||
|
|
141
|
-
contentType.includes("application/pdf")
|
|
141
|
+
contentType.includes("application/pdf") ||
|
|
142
|
+
contentType.startsWith("image/") ||
|
|
143
|
+
contentType.startsWith("video/") ||
|
|
144
|
+
contentType.startsWith("audio/")) {
|
|
142
145
|
return response.blob();
|
|
143
146
|
}
|
|
144
147
|
const text = await response.text();
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,11 @@ export type { ChannelHandlers } from "./gateway/gateway-client";
|
|
|
26
26
|
export type { RpcStatsSnapshot } from "./client/stats";
|
|
27
27
|
export { ALL_CAPABILITIES } from "./client/capabilities";
|
|
28
28
|
export type { Capability } from "./client/capabilities";
|
|
29
|
-
export { UnsupportedCapabilityError, AmbiguousWriteError } from "./client/errors";
|
|
29
|
+
export { UnsupportedCapabilityError, AmbiguousWriteError, DooverOfflineError } from "./client/errors";
|
|
30
|
+
export { OfflineDataClient, MemoryOfflineStorageAdapter, DEFAULT_OFFLINE_RETENTION_MS, } from "./client/offline-cache";
|
|
31
|
+
export type { OfflineCacheMode, OfflineReadCacheOptions, OfflineChannelPolicy, OfflineCacheScope, OfflineCacheRecord, OfflineStorageAdapter, OfflineDataClientOptions, } from "./client/offline-cache";
|
|
32
|
+
export { requestOptions, isDooverRequestOptions, splitRequestOptions, } from "./client/request-options";
|
|
33
|
+
export type { DooverRequestOptions } from "./client/request-options";
|
|
30
34
|
export type { DataClient, AgentScope, DataClientStatus, DataClientConnectionState, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, TurnApiLike, UsersApiLike, GatewayClientLike, RpcDispatcherLike, } from "./client/data-client";
|
|
31
35
|
export type { SourceProvenance, SourceProvenanceViaRest, SourceProvenanceViaGateway, } from "./types/provenance";
|
|
32
36
|
export { DooverAuth } from "./auth/doover-auth";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateSnowflakeIdAtTime = 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.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;
|
|
3
|
+
exports.generateSnowflakeIdAtTime = 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.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
4
|
var doover_client_1 = require("./client/doover-client");
|
|
5
5
|
Object.defineProperty(exports, "DooverClient", { enumerable: true, get: function () { return doover_client_1.DooverClient; } });
|
|
6
6
|
var local_agent_client_1 = require("./client/local-agent-client");
|
|
@@ -46,6 +46,15 @@ Object.defineProperty(exports, "ALL_CAPABILITIES", { enumerable: true, get: func
|
|
|
46
46
|
var errors_2 = require("./client/errors");
|
|
47
47
|
Object.defineProperty(exports, "UnsupportedCapabilityError", { enumerable: true, get: function () { return errors_2.UnsupportedCapabilityError; } });
|
|
48
48
|
Object.defineProperty(exports, "AmbiguousWriteError", { enumerable: true, get: function () { return errors_2.AmbiguousWriteError; } });
|
|
49
|
+
Object.defineProperty(exports, "DooverOfflineError", { enumerable: true, get: function () { return errors_2.DooverOfflineError; } });
|
|
50
|
+
var offline_cache_1 = require("./client/offline-cache");
|
|
51
|
+
Object.defineProperty(exports, "OfflineDataClient", { enumerable: true, get: function () { return offline_cache_1.OfflineDataClient; } });
|
|
52
|
+
Object.defineProperty(exports, "MemoryOfflineStorageAdapter", { enumerable: true, get: function () { return offline_cache_1.MemoryOfflineStorageAdapter; } });
|
|
53
|
+
Object.defineProperty(exports, "DEFAULT_OFFLINE_RETENTION_MS", { enumerable: true, get: function () { return offline_cache_1.DEFAULT_OFFLINE_RETENTION_MS; } });
|
|
54
|
+
var request_options_1 = require("./client/request-options");
|
|
55
|
+
Object.defineProperty(exports, "requestOptions", { enumerable: true, get: function () { return request_options_1.requestOptions; } });
|
|
56
|
+
Object.defineProperty(exports, "isDooverRequestOptions", { enumerable: true, get: function () { return request_options_1.isDooverRequestOptions; } });
|
|
57
|
+
Object.defineProperty(exports, "splitRequestOptions", { enumerable: true, get: function () { return request_options_1.splitRequestOptions; } });
|
|
49
58
|
var doover_auth_1 = require("./auth/doover-auth");
|
|
50
59
|
Object.defineProperty(exports, "DooverAuth", { enumerable: true, get: function () { return doover_auth_1.DooverAuth; } });
|
|
51
60
|
var cookie_auth_1 = require("./auth/cookie-auth");
|
package/dist/react/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { DooverProvider, useDooverClient } from "./context";
|
|
|
2
2
|
export type { DooverProviderProps } from "./context";
|
|
3
3
|
export type { DataClientStatus, DataClientConnectionState, AgentScope } from "../client/data-client";
|
|
4
4
|
export { useClientStatus } from "./useClientStatus";
|
|
5
|
+
export { useOfflineStatus, hasOfflineStatus } from "./useOfflineStatus";
|
|
6
|
+
export type { OfflineStatusSnapshot, OfflineStatusClient } from "./useOfflineStatus";
|
|
5
7
|
export { useConnectionState } from "./useConnectionState";
|
|
6
8
|
export type { ConnectionState, ConnectionStatus } from "./useConnectionState";
|
|
7
9
|
export { useAgentConnections, agentConnectionsQueryKey, } from "./useAgentConnections";
|
package/dist/react/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resetSharedQueryClient = exports.getSharedQueryClient = 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.useClientStatus = exports.useDooverClient = exports.DooverProvider = void 0;
|
|
3
|
+
exports.resetSharedQueryClient = exports.getSharedQueryClient = 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_1 = require("./context");
|
|
5
5
|
Object.defineProperty(exports, "DooverProvider", { enumerable: true, get: function () { return context_1.DooverProvider; } });
|
|
6
6
|
Object.defineProperty(exports, "useDooverClient", { enumerable: true, get: function () { return context_1.useDooverClient; } });
|
|
7
7
|
var useClientStatus_1 = require("./useClientStatus");
|
|
8
8
|
Object.defineProperty(exports, "useClientStatus", { enumerable: true, get: function () { return useClientStatus_1.useClientStatus; } });
|
|
9
|
+
var useOfflineStatus_1 = require("./useOfflineStatus");
|
|
10
|
+
Object.defineProperty(exports, "useOfflineStatus", { enumerable: true, get: function () { return useOfflineStatus_1.useOfflineStatus; } });
|
|
11
|
+
Object.defineProperty(exports, "hasOfflineStatus", { enumerable: true, get: function () { return useOfflineStatus_1.hasOfflineStatus; } });
|
|
9
12
|
var useConnectionState_1 = require("./useConnectionState");
|
|
10
13
|
Object.defineProperty(exports, "useConnectionState", { enumerable: true, get: function () { return useConnectionState_1.useConnectionState; } });
|
|
11
14
|
var useAgentConnections_1 = require("./useAgentConnections");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type UseQueryResult } from "@tanstack/react-query";
|
|
2
2
|
import type { Aggregate } from "../types/common";
|
|
3
3
|
import type { ChannelIdentifier } from "../types/viewer";
|
|
4
|
+
import { type OfflineStatusSnapshot } from "./useOfflineStatus";
|
|
4
5
|
export declare function channelAggregateQueryKey(agentId: string | undefined, channelName: string | undefined, sources?: string[]): readonly ["doover", "agent", string | undefined, "channel", string | undefined, "src", string];
|
|
5
6
|
/**
|
|
6
7
|
* Shape returned by `useChannelAggregate`. Shadows the react-query
|
|
@@ -23,6 +24,8 @@ export interface UseChannelAggregateResult<TData> extends Omit<UseQueryResult<Ag
|
|
|
23
24
|
* years in the future).
|
|
24
25
|
*/
|
|
25
26
|
last_updated: number | null | undefined;
|
|
27
|
+
/** Offline/cache status when the provided client supports offline caching. */
|
|
28
|
+
offline: OfflineStatusSnapshot;
|
|
26
29
|
}
|
|
27
30
|
export interface UseChannelAggregateOptions {
|
|
28
31
|
/**
|
|
@@ -7,6 +7,7 @@ const react_query_1 = require("@tanstack/react-query");
|
|
|
7
7
|
const errors_1 = require("../http/errors");
|
|
8
8
|
const context_1 = require("./context");
|
|
9
9
|
const useChannelSubscription_1 = require("./useChannelSubscription");
|
|
10
|
+
const useOfflineStatus_1 = require("./useOfflineStatus");
|
|
10
11
|
function channelAggregateQueryKey(agentId, channelName, sources) {
|
|
11
12
|
const sourceDim = sources && sources.length ? [...sources].sort().join(",") : "*";
|
|
12
13
|
return ["doover", "agent", agentId, "channel", channelName, "src", sourceDim];
|
|
@@ -24,6 +25,7 @@ function channelAggregateQueryKey(agentId, channelName, sources) {
|
|
|
24
25
|
*/
|
|
25
26
|
function useChannelAggregate(identifier, options) {
|
|
26
27
|
const client = (0, context_1.useDooverClient)();
|
|
28
|
+
const offline = (0, useOfflineStatus_1.useOfflineStatus)();
|
|
27
29
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
28
30
|
const { agentId, channelName } = identifier;
|
|
29
31
|
const sources = options?.sources;
|
|
@@ -74,5 +76,6 @@ function useChannelAggregate(identifier, options) {
|
|
|
74
76
|
data: aggregate?.data,
|
|
75
77
|
attachments: aggregate?.attachments,
|
|
76
78
|
last_updated: aggregate?.last_updated,
|
|
79
|
+
offline,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type InfiniteData, type UseInfiniteQueryResult } from "@tanstack/react-query";
|
|
2
2
|
import type { MessageStructure } from "../types/common";
|
|
3
3
|
import type { ChannelIdentifier } from "../types/viewer";
|
|
4
|
+
import { type OfflineStatusSnapshot } from "./useOfflineStatus";
|
|
4
5
|
export declare function channelMessagesQueryKey(agentId: string | undefined, channelName: string | undefined, fields?: readonly string[], sources?: string[]): readonly ["doover", "agent", string | undefined, "channel", string | undefined, "messages", "src", string] | readonly ["doover", "agent", string | undefined, "channel", string | undefined, "messages", "src", string, {
|
|
5
6
|
readonly fields: string[];
|
|
6
7
|
}];
|
|
@@ -49,6 +50,8 @@ export interface UseChannelMessagesResult<TData> extends Omit<UseInfiniteQueryRe
|
|
|
49
50
|
messages: MessageStructure<TData>[];
|
|
50
51
|
/** Raw react-query data (pages). */
|
|
51
52
|
data: InfiniteData<Page<TData>> | undefined;
|
|
53
|
+
/** Offline/cache status when the provided client supports offline caching. */
|
|
54
|
+
offline: OfflineStatusSnapshot;
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Paginated infinite query over `DooverDataProvider.getMessages`, with live
|
|
@@ -6,6 +6,7 @@ const react_1 = require("react");
|
|
|
6
6
|
const react_query_1 = require("@tanstack/react-query");
|
|
7
7
|
const context_1 = require("./context");
|
|
8
8
|
const useChannelSubscription_1 = require("./useChannelSubscription");
|
|
9
|
+
const useOfflineStatus_1 = require("./useOfflineStatus");
|
|
9
10
|
function channelMessagesQueryKey(agentId, channelName, fields, sources) {
|
|
10
11
|
// Source-dimension the key (multiplex source subsets cache independently);
|
|
11
12
|
// "*" means "all/unspecified" for a plain DooverClient/LocalAgentClient.
|
|
@@ -28,6 +29,7 @@ function channelMessagesQueryKey(agentId, channelName, fields, sources) {
|
|
|
28
29
|
*/
|
|
29
30
|
function useChannelMessages(identifier, options) {
|
|
30
31
|
const client = (0, context_1.useDooverClient)();
|
|
32
|
+
const offline = (0, useOfflineStatus_1.useOfflineStatus)();
|
|
31
33
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
32
34
|
const { agentId, channelName } = identifier;
|
|
33
35
|
const limit = options?.limit;
|
|
@@ -99,5 +101,5 @@ function useChannelMessages(identifier, options) {
|
|
|
99
101
|
query.fetchNextPage();
|
|
100
102
|
}, [autoPaginate, query.isFetching, query.hasNextPage, query.fetchNextPage]);
|
|
101
103
|
const messages = (0, react_1.useMemo)(() => (query.data?.pages ?? []).flat(), [query.data]);
|
|
102
|
-
return { ...query, messages };
|
|
104
|
+
return { ...query, messages, offline };
|
|
103
105
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OfflineStatusSnapshot } from "../client/offline-cache";
|
|
2
|
+
export type { OfflineStatusSnapshot } from "../client/offline-cache";
|
|
3
|
+
export interface OfflineStatusClient {
|
|
4
|
+
getOfflineStatus(): OfflineStatusSnapshot;
|
|
5
|
+
onOfflineStatusChange(listener: (status: OfflineStatusSnapshot) => void): () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function hasOfflineStatus(client: unknown): client is OfflineStatusClient;
|
|
8
|
+
export declare function useOfflineStatus(): OfflineStatusSnapshot;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasOfflineStatus = hasOfflineStatus;
|
|
4
|
+
exports.useOfflineStatus = useOfflineStatus;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
function hasOfflineStatus(client) {
|
|
8
|
+
return (!!client &&
|
|
9
|
+
typeof client === "object" &&
|
|
10
|
+
typeof client.getOfflineStatus === "function" &&
|
|
11
|
+
typeof client.onOfflineStatusChange === "function");
|
|
12
|
+
}
|
|
13
|
+
const UNAVAILABLE_OFFLINE_STATUS = {
|
|
14
|
+
online: true,
|
|
15
|
+
state: "online",
|
|
16
|
+
isOfflineFallback: false,
|
|
17
|
+
isExpired: false,
|
|
18
|
+
at: 0,
|
|
19
|
+
};
|
|
20
|
+
function useOfflineStatus() {
|
|
21
|
+
const client = (0, context_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);
|
|
27
|
+
}
|
|
@@ -47,6 +47,10 @@ const doover = __importStar(require("../index"));
|
|
|
47
47
|
(0, chai_1.expect)(doover.ALL_CAPABILITIES).to.be.an("array");
|
|
48
48
|
(0, chai_1.expect)(doover.UnsupportedCapabilityError).to.be.a("function");
|
|
49
49
|
(0, chai_1.expect)(doover.AmbiguousWriteError).to.be.a("function");
|
|
50
|
+
(0, chai_1.expect)(doover.DooverOfflineError).to.be.a("function");
|
|
51
|
+
(0, chai_1.expect)(doover.OfflineDataClient).to.be.a("function");
|
|
52
|
+
(0, chai_1.expect)(doover.MemoryOfflineStorageAdapter).to.be.a("function");
|
|
53
|
+
(0, chai_1.expect)(doover.requestOptions).to.be.a("function");
|
|
50
54
|
});
|
|
51
55
|
(0, mocha_1.it)("type-only exports resolve (compile-time)", () => {
|
|
52
56
|
// If any of these types are removed from the public index the import above
|
|
@@ -69,7 +73,7 @@ const doover = __importStar(require("../index"));
|
|
|
69
73
|
(0, chai_1.expect)(true).to.equal(true); // keep mocha happy
|
|
70
74
|
});
|
|
71
75
|
(0, mocha_1.it)("exports the full Phase-1..4 surface", () => {
|
|
72
|
-
for (const name of ["ALL_CAPABILITIES", "UnsupportedCapabilityError", "AmbiguousWriteError", "LocalAgentClient", "MultiplexClient", "DooverClient"]) {
|
|
76
|
+
for (const name of ["ALL_CAPABILITIES", "UnsupportedCapabilityError", "AmbiguousWriteError", "DooverOfflineError", "LocalAgentClient", "MultiplexClient", "DooverClient", "OfflineDataClient", "MemoryOfflineStorageAdapter", "requestOptions"]) {
|
|
73
77
|
(0, chai_1.expect)(doover[name], name).to.exist;
|
|
74
78
|
}
|
|
75
79
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const mocha_1 = require("mocha");
|
|
5
|
+
const doover_client_1 = require("../client/doover-client");
|
|
6
|
+
const offline_cache_1 = require("../client/offline-cache");
|
|
7
|
+
const request_options_1 = require("../client/request-options");
|
|
8
|
+
const errors_1 = require("../client/errors");
|
|
9
|
+
const snowflake_1 = require("../utils/snowflake");
|
|
10
|
+
const helpers_1 = require("./helpers");
|
|
11
|
+
function makeOnlineClient(fetchImpl) {
|
|
12
|
+
return new doover_client_1.DooverClient({
|
|
13
|
+
dataRestUrl: "https://api.example.com",
|
|
14
|
+
controlApiUrl: "https://control.example.com",
|
|
15
|
+
dataWssUrl: "wss://ws.example.com",
|
|
16
|
+
fetchImpl,
|
|
17
|
+
webSocketImpl: helpers_1.MockWebSocket,
|
|
18
|
+
disableBrowserLifecycleHooks: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
(0, mocha_1.describe)("requestOptions", () => {
|
|
22
|
+
(0, mocha_1.it)("marks a request-options bag explicitly", () => {
|
|
23
|
+
const options = (0, request_options_1.requestOptions)({
|
|
24
|
+
sources: ["cloud"],
|
|
25
|
+
cache: { policy: "tag-values-history" },
|
|
26
|
+
});
|
|
27
|
+
(0, chai_1.expect)(options[request_options_1.REQUEST_OPTIONS_SYMBOL]).to.equal(true);
|
|
28
|
+
(0, chai_1.expect)(options.sources).to.deep.equal(["cloud"]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
(0, mocha_1.describe)("OfflineDataClient", () => {
|
|
32
|
+
(0, mocha_1.it)("caches reads covered by a stored channel policy and serves them offline", async () => {
|
|
33
|
+
let online = true;
|
|
34
|
+
const messageId = (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-06-10T00:00:00Z"));
|
|
35
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
36
|
+
if (url.includes("/agents/a1/channels/tag_values/messages")) {
|
|
37
|
+
return (0, helpers_1.createJsonResponse)([
|
|
38
|
+
{
|
|
39
|
+
id: messageId,
|
|
40
|
+
data: { value: 42 },
|
|
41
|
+
attachments: [],
|
|
42
|
+
author_id: "u1",
|
|
43
|
+
channel: { agent_id: "a1", name: "tag_values" },
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
48
|
+
});
|
|
49
|
+
const storage = new offline_cache_1.MemoryOfflineStorageAdapter();
|
|
50
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
51
|
+
client: makeOnlineClient(fetchMock),
|
|
52
|
+
storage,
|
|
53
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
54
|
+
isOnline: () => online,
|
|
55
|
+
});
|
|
56
|
+
client.setChannelPolicy({
|
|
57
|
+
id: "tag-values-history",
|
|
58
|
+
channel: { agentId: "a1", channelName: "tag_values" },
|
|
59
|
+
messages: { mode: "latest", count: 100 },
|
|
60
|
+
});
|
|
61
|
+
const onlineMessages = await client.messages.listMessages({ agentId: "a1", channelName: "tag_values" }, { limit: 100, order: "asc" });
|
|
62
|
+
(0, chai_1.expect)(onlineMessages.map((message) => message.data)).to.deep.equal([{ value: 42 }]);
|
|
63
|
+
online = false;
|
|
64
|
+
const offlineMessages = await client.messages.listMessages({ agentId: "a1", channelName: "tag_values" }, { limit: 100, order: "asc" });
|
|
65
|
+
(0, chai_1.expect)(offlineMessages).to.deep.equal(onlineMessages);
|
|
66
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(1);
|
|
67
|
+
});
|
|
68
|
+
(0, mocha_1.it)("supports per-call cache opt-in for discovery reads", async () => {
|
|
69
|
+
let online = true;
|
|
70
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
71
|
+
if (url.includes("/agents/")) {
|
|
72
|
+
return (0, helpers_1.createJsonResponse)({
|
|
73
|
+
agents: [
|
|
74
|
+
{
|
|
75
|
+
id: "a1",
|
|
76
|
+
organisation: "org1",
|
|
77
|
+
name: "pump",
|
|
78
|
+
display_name: "Pump",
|
|
79
|
+
archived: false,
|
|
80
|
+
group: "",
|
|
81
|
+
type: "device",
|
|
82
|
+
fixed_location: { latitude: 0, longitude: 0 },
|
|
83
|
+
extra_config: {},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
count: 1,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
90
|
+
});
|
|
91
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
92
|
+
client: makeOnlineClient(fetchMock),
|
|
93
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
94
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
95
|
+
isOnline: () => online,
|
|
96
|
+
});
|
|
97
|
+
const onlineAgents = await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through", policy: "agents" } }));
|
|
98
|
+
(0, chai_1.expect)((onlineAgents.agents ?? []).map((agent) => agent.id)).to.deep.equal(["a1"]);
|
|
99
|
+
online = false;
|
|
100
|
+
const offlineAgents = await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through", policy: "agents" } }));
|
|
101
|
+
(0, chai_1.expect)(offlineAgents).to.deep.equal(onlineAgents);
|
|
102
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(1);
|
|
103
|
+
});
|
|
104
|
+
(0, mocha_1.it)("fails writes immediately while offline", async () => {
|
|
105
|
+
const fetchMock = (0, helpers_1.createFetchMock)();
|
|
106
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
107
|
+
client: makeOnlineClient(fetchMock),
|
|
108
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
109
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
110
|
+
isOnline: () => false,
|
|
111
|
+
});
|
|
112
|
+
let error;
|
|
113
|
+
try {
|
|
114
|
+
await client.messages.postMessage({ agentId: "a1", channelName: "commands" }, { data: { run: true } });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
error = err;
|
|
118
|
+
}
|
|
119
|
+
(0, chai_1.expect)(error).to.be.instanceOf(errors_1.DooverOfflineError);
|
|
120
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(0);
|
|
121
|
+
});
|
|
122
|
+
(0, mocha_1.it)("honours cache: false even when a stored policy exists", async () => {
|
|
123
|
+
let online = true;
|
|
124
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
125
|
+
if (url.includes("/agents/a1/channels/tag_values/messages")) {
|
|
126
|
+
return (0, helpers_1.createJsonResponse)([
|
|
127
|
+
{
|
|
128
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date("2026-06-10T00:00:00Z")),
|
|
129
|
+
data: { value: 1 },
|
|
130
|
+
attachments: [],
|
|
131
|
+
author_id: "u1",
|
|
132
|
+
channel: { agent_id: "a1", name: "tag_values" },
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
137
|
+
});
|
|
138
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
139
|
+
client: makeOnlineClient(fetchMock),
|
|
140
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
141
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
142
|
+
isOnline: () => online,
|
|
143
|
+
});
|
|
144
|
+
client.setChannelPolicy({
|
|
145
|
+
id: "tag-values-history",
|
|
146
|
+
channel: { agentId: "a1", channelName: "tag_values" },
|
|
147
|
+
messages: { mode: "latest", count: 100 },
|
|
148
|
+
});
|
|
149
|
+
await client.messages.listMessages({ agentId: "a1", channelName: "tag_values" }, { limit: 100, order: "asc" }, (0, request_options_1.requestOptions)({ cache: false }));
|
|
150
|
+
online = false;
|
|
151
|
+
let error;
|
|
152
|
+
try {
|
|
153
|
+
await client.messages.listMessages({ agentId: "a1", channelName: "tag_values" }, { limit: 100, order: "asc" }, (0, request_options_1.requestOptions)({ cache: false }));
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
error = err;
|
|
157
|
+
}
|
|
158
|
+
(0, chai_1.expect)(error).to.be.instanceOf(errors_1.DooverOfflineError);
|
|
159
|
+
});
|
|
160
|
+
(0, mocha_1.it)("tracks expired cached records and requires allowExpired to read them", async () => {
|
|
161
|
+
let online = true;
|
|
162
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
163
|
+
if (url.includes("/agents/"))
|
|
164
|
+
return (0, helpers_1.createJsonResponse)({ agents: [], count: 0 });
|
|
165
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
166
|
+
});
|
|
167
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
168
|
+
client: makeOnlineClient(fetchMock),
|
|
169
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
170
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
171
|
+
isOnline: () => online,
|
|
172
|
+
});
|
|
173
|
+
await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through", retentionMs: 1 } }));
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
175
|
+
online = false;
|
|
176
|
+
let error;
|
|
177
|
+
try {
|
|
178
|
+
await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through" } }));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
error = err;
|
|
182
|
+
}
|
|
183
|
+
(0, chai_1.expect)(error).to.be.instanceOf(errors_1.DooverOfflineError);
|
|
184
|
+
(0, chai_1.expect)(client.getOfflineStatus().state).to.equal("cache-miss");
|
|
185
|
+
const expired = await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through", allowExpired: true } }));
|
|
186
|
+
(0, chai_1.expect)(expired.agents).to.deep.equal([]);
|
|
187
|
+
(0, chai_1.expect)(client.getOfflineStatus().isExpired).to.equal(true);
|
|
188
|
+
});
|
|
189
|
+
(0, mocha_1.it)("persists gateway messages into latest-N cache according to channel policy", async () => {
|
|
190
|
+
let online = true;
|
|
191
|
+
const fetchMock = (0, helpers_1.createFetchMock)();
|
|
192
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
193
|
+
client: makeOnlineClient(fetchMock),
|
|
194
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
195
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
196
|
+
isOnline: () => online,
|
|
197
|
+
});
|
|
198
|
+
client.setChannelPolicy({
|
|
199
|
+
id: "camera-latest",
|
|
200
|
+
channel: { agentId: "a1", channelName: "camera" },
|
|
201
|
+
messages: { mode: "latest", count: 2 },
|
|
202
|
+
});
|
|
203
|
+
client.gateway.subscribeToChannel({ agent_id: "a1", name: "camera" }, {});
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
205
|
+
const ws = helpers_1.MockWebSocket.instances[helpers_1.MockWebSocket.instances.length - 1];
|
|
206
|
+
ws.open();
|
|
207
|
+
ws.receive({ op: 0, t: "Hello", d: {} });
|
|
208
|
+
ws.receive({
|
|
209
|
+
op: 0,
|
|
210
|
+
t: "Ready",
|
|
211
|
+
d: { session_id: "s1", session_token: "t1", subscriptions: [] },
|
|
212
|
+
});
|
|
213
|
+
for (const [idx, iso] of [
|
|
214
|
+
"2026-06-10T00:00:00Z",
|
|
215
|
+
"2026-06-10T00:01:00Z",
|
|
216
|
+
"2026-06-10T00:02:00Z",
|
|
217
|
+
].entries()) {
|
|
218
|
+
ws.receive({
|
|
219
|
+
op: 0,
|
|
220
|
+
t: "MessageCreate",
|
|
221
|
+
d: {
|
|
222
|
+
id: (0, snowflake_1.generateSnowflakeIdAtTime)(new Date(iso)),
|
|
223
|
+
data: { idx },
|
|
224
|
+
attachments: [],
|
|
225
|
+
author_id: "u1",
|
|
226
|
+
channel: { agent_id: "a1", name: "camera" },
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
231
|
+
online = false;
|
|
232
|
+
const messages = await client.messages.listMessages({ agentId: "a1", channelName: "camera" }, { limit: 10, order: "asc" });
|
|
233
|
+
(0, chai_1.expect)(messages.map((message) => message.data)).to.deep.equal([
|
|
234
|
+
{ idx: 1 },
|
|
235
|
+
{ idx: 2 },
|
|
236
|
+
]);
|
|
237
|
+
(0, chai_1.expect)(client.getOfflineStatus().state).to.equal("cache-fallback");
|
|
238
|
+
});
|
|
239
|
+
(0, mocha_1.it)("notifies offline status listeners when cached data is used", async () => {
|
|
240
|
+
let online = true;
|
|
241
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
242
|
+
if (url.includes("/agents/"))
|
|
243
|
+
return (0, helpers_1.createJsonResponse)({ agents: [], count: 0 });
|
|
244
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
245
|
+
});
|
|
246
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
247
|
+
client: makeOnlineClient(fetchMock),
|
|
248
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
249
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
250
|
+
isOnline: () => online,
|
|
251
|
+
});
|
|
252
|
+
const seen = [];
|
|
253
|
+
const off = client.onOfflineStatusChange((status) => seen.push(status.state));
|
|
254
|
+
await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through" } }));
|
|
255
|
+
online = false;
|
|
256
|
+
await client.agents.listAgents((0, request_options_1.requestOptions)({ cache: { mode: "read-through" } }));
|
|
257
|
+
off();
|
|
258
|
+
(0, chai_1.expect)(seen).to.include("online");
|
|
259
|
+
(0, chai_1.expect)(seen).to.include("cache-fallback");
|
|
260
|
+
});
|
|
261
|
+
(0, mocha_1.it)("caches selected attachment blobs through the blob storage adapter", async () => {
|
|
262
|
+
let online = true;
|
|
263
|
+
const fetchMock = (0, helpers_1.createFetchMock)((url) => {
|
|
264
|
+
if (url.includes("/attachments/thumb1")) {
|
|
265
|
+
return (0, helpers_1.createBlobResponse)("thumbnail-bytes", {
|
|
266
|
+
headers: { "content-type": "image/jpeg" },
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return (0, helpers_1.createJsonResponse)({ ok: true });
|
|
270
|
+
});
|
|
271
|
+
const client = new offline_cache_1.OfflineDataClient({
|
|
272
|
+
client: makeOnlineClient(fetchMock),
|
|
273
|
+
storage: new offline_cache_1.MemoryOfflineStorageAdapter(),
|
|
274
|
+
scope: { userId: "u1", organisationId: "org1", sourceId: "cloud" },
|
|
275
|
+
isOnline: () => online,
|
|
276
|
+
});
|
|
277
|
+
client.setChannelPolicy({
|
|
278
|
+
id: "camera-thumbnails",
|
|
279
|
+
channel: { agentId: "a1", channelName: "camera" },
|
|
280
|
+
messageAttachments: { include: "metadata-and-selected-blobs" },
|
|
281
|
+
});
|
|
282
|
+
const onlineBlob = await client.messages.getMessageAttachment({ agentId: "a1", channelName: "camera" }, "m1", "thumb1");
|
|
283
|
+
(0, chai_1.expect)(await onlineBlob.text()).to.equal("thumbnail-bytes");
|
|
284
|
+
online = false;
|
|
285
|
+
const offlineBlob = await client.messages.getMessageAttachment({ agentId: "a1", channelName: "camera" }, "m1", "thumb1");
|
|
286
|
+
(0, chai_1.expect)(await offlineBlob.text()).to.equal("thumbnail-bytes");
|
|
287
|
+
(0, chai_1.expect)(fetchMock.callCount).to.equal(1);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -39,6 +39,7 @@ const r = __importStar(require("../react"));
|
|
|
39
39
|
(0, mocha_1.describe)("react exports", () => {
|
|
40
40
|
(0, mocha_1.it)("exports useClientStatus and the key helpers", () => {
|
|
41
41
|
(0, chai_1.expect)(r.useClientStatus).to.be.a("function");
|
|
42
|
+
(0, chai_1.expect)(r.useOfflineStatus).to.be.a("function");
|
|
42
43
|
(0, chai_1.expect)(r.channelAggregateQueryKey).to.be.a("function");
|
|
43
44
|
(0, chai_1.expect)(r.useConnectionState).to.be.a("function"); // still exported (soft-deprecated)
|
|
44
45
|
});
|
package/dist/types/common.d.ts
CHANGED
|
@@ -174,11 +174,17 @@ export type RpcStatus<TPending = undefined> = {
|
|
|
174
174
|
} | {
|
|
175
175
|
code: "success";
|
|
176
176
|
};
|
|
177
|
+
/** Who initiated an RPC (e.g. the logged-in user dispatching a ui_cmd). */
|
|
178
|
+
export interface RpcActor {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
}
|
|
177
182
|
/** Shape a consumer posts to initiate an RPC. */
|
|
178
183
|
export interface RpcRequest<TRequest = object> {
|
|
179
184
|
app_key?: string;
|
|
180
185
|
method: string;
|
|
181
186
|
request: TRequest;
|
|
187
|
+
actor?: RpcActor;
|
|
182
188
|
}
|
|
183
189
|
/** Shape of the message the server maintains as the RPC progresses. */
|
|
184
190
|
export interface RpcMessageData<TRequest = object, TResponse = object, TPending = undefined> extends RpcRequest<TRequest> {
|