doover-js 0.5.0-alpha.0 → 0.5.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +115 -0
  2. package/dist/client/capabilities.d.ts +10 -0
  3. package/dist/client/capabilities.js +43 -0
  4. package/dist/client/data-client.d.ts +104 -0
  5. package/dist/client/data-client.js +2 -0
  6. package/dist/client/doover-client.d.ts +31 -27
  7. package/dist/client/doover-client.js +49 -16
  8. package/dist/client/errors.d.ts +23 -0
  9. package/dist/client/errors.js +50 -0
  10. package/dist/client/local-agent-client.d.ts +65 -0
  11. package/dist/client/local-agent-client.js +222 -0
  12. package/dist/client/multiplex-client.d.ts +151 -0
  13. package/dist/client/multiplex-client.js +744 -0
  14. package/dist/client/multiplex-gateway.d.ts +49 -0
  15. package/dist/client/multiplex-gateway.js +114 -0
  16. package/dist/client/multiplex-merge.d.ts +12 -0
  17. package/dist/client/multiplex-merge.js +42 -0
  18. package/dist/client/provenance.d.ts +60 -0
  19. package/dist/client/provenance.js +113 -0
  20. package/dist/client/status-tracker.d.ts +24 -0
  21. package/dist/client/status-tracker.js +90 -0
  22. package/dist/gateway/gateway-client.d.ts +10 -0
  23. package/dist/gateway/gateway-client.js +21 -6
  24. package/dist/http/rest-client.d.ts +5 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.js +16 -7
  27. package/dist/react/context.d.ts +9 -5
  28. package/dist/react/context.js +6 -2
  29. package/dist/react/index.d.ts +3 -0
  30. package/dist/react/index.js +3 -1
  31. package/dist/react/useChannelAggregate.d.ts +8 -1
  32. package/dist/react/useChannelAggregate.js +18 -7
  33. package/dist/react/useChannelMessage.d.ts +8 -1
  34. package/dist/react/useChannelMessage.js +17 -6
  35. package/dist/react/useChannelMessages.d.ts +8 -1
  36. package/dist/react/useChannelMessages.js +15 -6
  37. package/dist/react/useChannelSubscription.d.ts +6 -0
  38. package/dist/react/useClientStatus.d.ts +10 -0
  39. package/dist/react/useClientStatus.js +22 -0
  40. package/dist/react/useConnectionState.d.ts +5 -0
  41. package/dist/react/useConnectionState.js +5 -0
  42. package/dist/react/useMultiAgentAggregates.d.ts +8 -1
  43. package/dist/react/useMultiAgentAggregates.js +15 -6
  44. package/dist/react/useMultiAgentChannelMessages.d.ts +8 -1
  45. package/dist/react/useMultiAgentChannelMessages.js +15 -5
  46. package/dist/react/useSendMessage.d.ts +8 -1
  47. package/dist/react/useSendMessage.js +10 -2
  48. package/dist/react/useUpdateAggregate.d.ts +6 -0
  49. package/dist/react/useUpdateAggregate.js +14 -4
  50. package/dist/react/useUpdateMessage.d.ts +5 -0
  51. package/dist/react/useUpdateMessage.js +11 -2
  52. package/dist/test/capabilities.test.d.ts +1 -0
  53. package/dist/test/capabilities.test.js +27 -0
  54. package/dist/test/data-client-errors.test.d.ts +1 -0
  55. package/dist/test/data-client-errors.test.js +24 -0
  56. package/dist/test/data-client-shape.test.d.ts +1 -0
  57. package/dist/test/data-client-shape.test.js +28 -0
  58. package/dist/test/doover-client-dataclient.test.d.ts +1 -0
  59. package/dist/test/doover-client-dataclient.test.js +58 -0
  60. package/dist/test/doover-client-provenance.test.d.ts +1 -0
  61. package/dist/test/doover-client-provenance.test.js +32 -0
  62. package/dist/test/exports.test.d.ts +1 -0
  63. package/dist/test/exports.test.js +76 -0
  64. package/dist/test/gateway-provenance.test.d.ts +1 -0
  65. package/dist/test/gateway-provenance.test.js +29 -0
  66. package/dist/test/local-agent-client.test.d.ts +1 -0
  67. package/dist/test/local-agent-client.test.js +202 -0
  68. package/dist/test/multiplex-capabilities.test.d.ts +1 -0
  69. package/dist/test/multiplex-capabilities.test.js +37 -0
  70. package/dist/test/multiplex-conflicts.test.d.ts +1 -0
  71. package/dist/test/multiplex-conflicts.test.js +35 -0
  72. package/dist/test/multiplex-gateway.test.d.ts +1 -0
  73. package/dist/test/multiplex-gateway.test.js +103 -0
  74. package/dist/test/multiplex-merge.test.d.ts +1 -0
  75. package/dist/test/multiplex-merge.test.js +19 -0
  76. package/dist/test/multiplex-noncore.test.d.ts +1 -0
  77. package/dist/test/multiplex-noncore.test.js +82 -0
  78. package/dist/test/multiplex-passthrough.test.d.ts +1 -0
  79. package/dist/test/multiplex-passthrough.test.js +35 -0
  80. package/dist/test/multiplex-reads.test.d.ts +1 -0
  81. package/dist/test/multiplex-reads.test.js +87 -0
  82. package/dist/test/multiplex-registry.test.d.ts +1 -0
  83. package/dist/test/multiplex-registry.test.js +81 -0
  84. package/dist/test/multiplex-routing.test.d.ts +1 -0
  85. package/dist/test/multiplex-routing.test.js +45 -0
  86. package/dist/test/multiplex-status.test.d.ts +1 -0
  87. package/dist/test/multiplex-status.test.js +63 -0
  88. package/dist/test/multiplex-writes.test.d.ts +1 -0
  89. package/dist/test/multiplex-writes.test.js +93 -0
  90. package/dist/test/provenance-regression.test.d.ts +1 -0
  91. package/dist/test/provenance-regression.test.js +45 -0
  92. package/dist/test/provenance-stamper.test.d.ts +1 -0
  93. package/dist/test/provenance-stamper.test.js +41 -0
  94. package/dist/test/provenance-type.test.d.ts +1 -0
  95. package/dist/test/provenance-type.test.js +15 -0
  96. package/dist/test/react-client-status.test.d.ts +1 -0
  97. package/dist/test/react-client-status.test.js +35 -0
  98. package/dist/test/react-dataclient.test.d.ts +1 -0
  99. package/dist/test/react-dataclient.test.js +28 -0
  100. package/dist/test/react-exports.test.d.ts +1 -0
  101. package/dist/test/react-exports.test.js +45 -0
  102. package/dist/test/react-sources-option.test.d.ts +1 -0
  103. package/dist/test/react-sources-option.test.js +41 -0
  104. package/dist/test/react.test.js +2 -2
  105. package/dist/test/status-tracker.test.d.ts +1 -0
  106. package/dist/test/status-tracker.test.js +85 -0
  107. package/dist/types/common.d.ts +16 -0
  108. package/dist/types/provenance.d.ts +45 -0
  109. package/dist/types/provenance.js +2 -0
  110. package/dist/types/viewer.d.ts +3 -0
  111. package/package.json +1 -1
package/README.md CHANGED
@@ -122,12 +122,127 @@ const client = new DooverClient({
122
122
 
123
123
  Reconnections automatically use the latest (potentially refreshed) token.
124
124
 
125
+ ## Multi-source data (`DataClient`)
126
+
127
+ ### The `DataClient` contract
128
+
129
+ `DooverClient` now implements the `DataClient` interface — the shared contract for every client type in this library. Any hook or provider that previously accepted a `DooverClient` now accepts any `DataClient`.
130
+
131
+ ```ts
132
+ import type { DataClient } from "doover-js";
133
+
134
+ function useMyData(client: DataClient) {
135
+ return client.channels.listChannels("my-agent");
136
+ }
137
+ ```
138
+
139
+ ### Capability model
140
+
141
+ Each client advertises what it can do via `getCapabilities()` and the `supports(cap)` helper. Calling an unsupported method throws `UnsupportedCapabilityError`.
142
+
143
+ ```ts
144
+ import { UnsupportedCapabilityError } from "doover-js";
145
+
146
+ if (client.supports("aggregates.write")) {
147
+ await client.aggregates.putAggregate(channelId, data);
148
+ } else {
149
+ console.warn("Client does not support aggregate writes");
150
+ }
151
+ ```
152
+
153
+ ### `LocalAgentClient` — LAN / direct access
154
+
155
+ Connect directly to a local Doover agent without cloud auth:
156
+
157
+ ```ts
158
+ import { LocalAgentClient } from "doover-js";
159
+
160
+ const local = new LocalAgentClient({
161
+ baseUrl: "http://192.168.0.7:49100",
162
+ sourceId: "local:192.168.0.7:49100", // stable id for cache keys
163
+ });
164
+ const channels = await local.channels.listChannels("my-agent");
165
+ ```
166
+
167
+ `LocalAgentClient` exposes a narrowed capability set — reads for agents, channels, messages, aggregates, and the gateway are supported; cloud-only operations (alarms, notifications, permissions, etc.) are not.
168
+
169
+ ### `MultiplexClient` — fan-out reads, routed writes
170
+
171
+ `MultiplexClient` manages a registry of named sources (cloud or local) and fans reads out across all active sources, merging the results. Writes are routed to the single source that owns the target agent.
172
+
173
+ ```ts
174
+ import { MultiplexClient, LocalAgentClient, getDooverClient } from "doover-js";
175
+
176
+ const mux = new MultiplexClient({
177
+ factory: (d) =>
178
+ d.kind === "cloud"
179
+ ? getDooverClient({ /* cloud config */ })
180
+ : new LocalAgentClient({ baseUrl: `http://${(d.params as any).host}:${(d.params as any).port}`, sourceId: d.id }),
181
+ register: [{ id: "cloud", kind: "cloud" }],
182
+ enable: ["cloud"],
183
+ });
184
+ mux.setActiveSources(["cloud", { id: "local:192.168.0.7:49100", kind: "local", params: { host: "192.168.0.7", port: 49100 } }]);
185
+ const channels = await mux.channels.listChannels("dev7"); // merged cloud + local
186
+ mux.on("conflict", (c) => console.warn("source disagreement", c));
187
+ ```
188
+
189
+ Use `getLastConflicts()` to inspect the most recent per-key disagreements between sources after a read.
190
+
191
+ ### `__source` provenance
192
+
193
+ Every datum returned by any `DataClient` carries a `__source` field (type `SourceProvenance`) recording which client returned it, when, and via which transport.
194
+
195
+ ```ts
196
+ const ch = await client.channels.getChannel("my-channel");
197
+ console.log(ch.__source?.client.id); // e.g. "cloud" or "local:192.168.0.7:49100"
198
+ ```
199
+
200
+ ### React: hooks, `DooverProvider`, and `useClientStatus`
201
+
202
+ `DooverProvider` accepts any `DataClient`:
203
+
204
+ ```tsx
205
+ import { DooverProvider } from "doover-js/react";
206
+ import { MultiplexClient } from "doover-js";
207
+
208
+ <DooverProvider client={mux}>
209
+ <App />
210
+ </DooverProvider>
211
+ ```
212
+
213
+ All data hooks gained an optional `sources?: string[]` prop that restricts fan-out to specific sources:
214
+
215
+ ```tsx
216
+ const { data } = useChannelAggregate(channelId, { sources: ["local:192.168.0.7:49100"] });
217
+ ```
218
+
219
+ The new `useClientStatus()` hook surfaces the connection status of every registered source:
220
+
221
+ ```tsx
222
+ import { useClientStatus } from "doover-js/react";
223
+
224
+ const statuses = useClientStatus();
225
+ // statuses is DataClientStatus[] — one entry per registered source
226
+ ```
227
+
228
+ `useConnectionState` is soft-deprecated in favour of `useClientStatus`. It still works and remains exported, but new code should prefer `useClientStatus`.
229
+
125
230
  ## Architecture
126
231
 
127
232
  `DooverClient` builds one shared `DooverAuth` instance and injects it into `RestClient`, `DooverDataProvider`, and `GatewayClient`. Token refreshes propagate everywhere automatically.
128
233
 
129
234
  `DooverDataProvider` preserves the older viewer-oriented interface. `DooverClient` exposes the broader API surface through subclients.
130
235
 
236
+ ## 0.5.0-alpha.1
237
+
238
+ - **`DataClient` contract + capability model** — `DooverClient` now implements the `DataClient` interface. Every client type advertises its capability set; unsupported calls throw `UnsupportedCapabilityError`. `AmbiguousWriteError` is raised when a `MultiplexClient` write has more than one candidate source. (Additive, no breaking changes.)
239
+ - **`__source` provenance** — every datum carries a `__source: SourceProvenance` field recording which client returned it, when, and via which transport.
240
+ - **`LocalAgentClient`** — new client for direct LAN connections to a local Doover agent (no cloud auth required).
241
+ - **`MultiplexClient`** — new client that manages a registry of named sources, fans reads out across all active sources, merges results, and routes writes to the owning source.
242
+ - **React `sources` option** — all data hooks accept `sources?: string[]` to restrict fan-out to specific sources; query keys are source-dimensioned.
243
+ - **`useClientStatus()`** — new react hook that surfaces `DataClientStatus` for each registered source.
244
+ - **`useConnectionState` soft-deprecated** — still exported and working; prefer `useClientStatus` for new code.
245
+
131
246
  ## Migrating to 0.6.0
132
247
 
133
248
  `DooverDataProvider` (`client.viewer`) is deprecated in 0.5.0 and removed in 0.6.0. The replacements are subclients on `DooverClient`:
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One capability per distinct endpoint-or-ability across the full `DataClient`
3
+ * surface. A backend advertises exactly what it can do; callers gate calls on
4
+ * these and an unsupported call throws `UnsupportedCapabilityError`.
5
+ *
6
+ * String-literal union (not a TS enum) so values serialise cleanly and can
7
+ * appear in error messages / debug UIs verbatim.
8
+ */
9
+ export type Capability = "agents.list" | "agents.multiAgentMessages" | "agents.multiAgentAggregates" | "channels.list" | "channels.get" | "channels.create" | "channels.archive" | "channels.dataSeries" | "aggregates.get" | "aggregates.put" | "aggregates.patch" | "aggregates.attachment" | "messages.list" | "messages.listHistorical" | "messages.get" | "messages.post" | "messages.put" | "messages.delete" | "messages.attachment" | "messages.timeseries" | "messages.invocationLogs" | "gateway.subscribe" | "gateway.realtime" | "gateway.oneShot" | "rpc.send" | "alarms.read" | "alarms.write" | "connections.read" | "connections.write" | "notifications.read" | "notifications.write" | "permissions.read" | "permissions.write" | "processors.read" | "processors.write" | "turn.credentials" | "users.me";
10
+ export declare const ALL_CAPABILITIES: readonly Capability[];
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_CAPABILITIES = void 0;
4
+ exports.ALL_CAPABILITIES = [
5
+ "agents.list",
6
+ "agents.multiAgentMessages",
7
+ "agents.multiAgentAggregates",
8
+ "channels.list",
9
+ "channels.get",
10
+ "channels.create",
11
+ "channels.archive",
12
+ "channels.dataSeries",
13
+ "aggregates.get",
14
+ "aggregates.put",
15
+ "aggregates.patch",
16
+ "aggregates.attachment",
17
+ "messages.list",
18
+ "messages.listHistorical",
19
+ "messages.get",
20
+ "messages.post",
21
+ "messages.put",
22
+ "messages.delete",
23
+ "messages.attachment",
24
+ "messages.timeseries",
25
+ "messages.invocationLogs",
26
+ "gateway.subscribe",
27
+ "gateway.realtime",
28
+ "gateway.oneShot",
29
+ "rpc.send",
30
+ "alarms.read",
31
+ "alarms.write",
32
+ "connections.read",
33
+ "connections.write",
34
+ "notifications.read",
35
+ "notifications.write",
36
+ "permissions.read",
37
+ "permissions.write",
38
+ "processors.read",
39
+ "processors.write",
40
+ "turn.credentials",
41
+ "users.me",
42
+ ];
43
+ void true;
@@ -0,0 +1,104 @@
1
+ import type { AgentsApi } from "../apis/agents-api";
2
+ import type { AggregatesApi } from "../apis/aggregates-api";
3
+ import type { AlarmsApi } from "../apis/alarms-api";
4
+ import type { ChannelsApi } from "../apis/channels-api";
5
+ import type { ConnectionsApi } from "../apis/connections-api";
6
+ import type { MessagesApi } from "../apis/messages-api";
7
+ import type { NotificationsApi } from "../apis/notifications-api";
8
+ import type { PermissionsApi } from "../apis/permissions-api";
9
+ import type { ProcessorsApi } from "../apis/processors-api";
10
+ import type { TurnApi } from "../apis/turn-api";
11
+ import type { UsersApi } from "../apis/users-api";
12
+ import type { GatewayClient } from "../gateway/gateway-client";
13
+ import type { RpcDispatcher } from "../rpc/rpc-dispatcher";
14
+ import type { Capability } from "./capabilities";
15
+ /**
16
+ * Public structural shape of each concrete subclient — `Pick<Class, keyof Class>`
17
+ * yields just the public members as a plain object type (no nominal/private-member
18
+ * coupling), so a non-`DooverClient` implementation can satisfy it structurally.
19
+ */
20
+ export type AgentsApiLike = Pick<AgentsApi, keyof AgentsApi>;
21
+ export type AggregatesApiLike = Pick<AggregatesApi, keyof AggregatesApi>;
22
+ export type AlarmsApiLike = Pick<AlarmsApi, keyof AlarmsApi>;
23
+ export type ChannelsApiLike = Pick<ChannelsApi, keyof ChannelsApi>;
24
+ export type ConnectionsApiLike = Pick<ConnectionsApi, keyof ConnectionsApi>;
25
+ export type MessagesApiLike = Pick<MessagesApi, keyof MessagesApi>;
26
+ export type NotificationsApiLike = Pick<NotificationsApi, keyof NotificationsApi>;
27
+ export type PermissionsApiLike = Pick<PermissionsApi, keyof PermissionsApi>;
28
+ export type ProcessorsApiLike = Pick<ProcessorsApi, keyof ProcessorsApi>;
29
+ export type TurnApiLike = Pick<TurnApi, keyof TurnApi>;
30
+ export type UsersApiLike = Pick<UsersApi, keyof UsersApi>;
31
+ export type GatewayClientLike = Pick<GatewayClient, keyof GatewayClient>;
32
+ export type RpcDispatcherLike = Pick<RpcDispatcher, keyof RpcDispatcher>;
33
+ /** Which agents a `DataClient` can serve. */
34
+ export type AgentScope =
35
+ /** Serves every agent (the cloud). Routing treats this as a wildcard. */
36
+ {
37
+ mode: "all";
38
+ }
39
+ /** Serves exactly these agent ids (a local device agent → typically one id). */
40
+ | {
41
+ mode: "list";
42
+ agentIds: string[];
43
+ };
44
+ export type DataClientConnectionState = "connected" | "connecting" | "disconnected" | "degraded" | "error";
45
+ export interface DataClientStatus {
46
+ /** This client's id ("cloud", "local:…", "multiplex", …). */
47
+ clientId: string;
48
+ /** True when the realtime link is up. Mirrors `isConnected()`. */
49
+ connected: boolean;
50
+ state: DataClientConnectionState;
51
+ /** Gateway session, when applicable. */
52
+ session?: {
53
+ id: string;
54
+ } | null;
55
+ /** Last lifecycle event seen ("init" | "open" | "ready" | "close" | "error" | …). */
56
+ lastEvent?: string;
57
+ /** Round-trip latency estimate in ms, if measured. */
58
+ latencyMs?: number | null;
59
+ /** Last error message, if the last event was an error. */
60
+ lastError?: string;
61
+ /** Best-effort agent scope at snapshot time ("unknown" before first resolution). */
62
+ agentScope: AgentScope | "unknown";
63
+ /** When this snapshot was taken (epoch ms). */
64
+ at: number;
65
+ /** Per-member statuses — present only for `MultiplexClient`. */
66
+ members?: Array<{
67
+ sourceId: string;
68
+ label?: string;
69
+ status: DataClientStatus;
70
+ }>;
71
+ }
72
+ /**
73
+ * The capability-aware data-access contract. `DooverClient`, `LocalAgentClient`
74
+ * and `MultiplexClient` all implement it. Deliberately excludes `DooverClient`'s
75
+ * `auth`/`rest`/`stats`/`viewer` (construction/internals/legacy). May be widened
76
+ * later; the invariant is that `DooverClient` always satisfies it.
77
+ */
78
+ export interface DataClient {
79
+ readonly agents: AgentsApiLike;
80
+ readonly channels: ChannelsApiLike;
81
+ readonly messages: MessagesApiLike;
82
+ readonly aggregates: AggregatesApiLike;
83
+ readonly alarms: AlarmsApiLike;
84
+ readonly connections: ConnectionsApiLike;
85
+ readonly notifications: NotificationsApiLike;
86
+ readonly permissions: PermissionsApiLike;
87
+ readonly processors: ProcessorsApiLike;
88
+ readonly turn: TurnApiLike;
89
+ readonly users: UsersApiLike;
90
+ readonly gateway: GatewayClientLike;
91
+ readonly rpc: RpcDispatcherLike;
92
+ getCapabilities(): ReadonlySet<Capability>;
93
+ /** Convenience: `getCapabilities().has(cap)`. */
94
+ supports(cap: Capability): boolean;
95
+ /** True when the client's realtime link is up (multiplex: all members with `gateway.subscribe`). */
96
+ isConnected(): boolean;
97
+ getStatus(): DataClientStatus;
98
+ /** Subscribe to status changes; returns an idempotent unsubscribe fn. */
99
+ onStatusChange(listener: (status: DataClientStatus) => void): () => void;
100
+ /** Which agents this client can serve. Cloud → `{ mode: "all" }` with no round-trip. */
101
+ getAgentScope(): Promise<AgentScope>;
102
+ /** Synchronous best-effort snapshot; `"unknown"` until the first resolution. */
103
+ getKnownAgentScope(): AgentScope | "unknown";
104
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,40 +1,44 @@
1
- import { AgentsApi } from "../apis/agents-api";
2
- import { AggregatesApi } from "../apis/aggregates-api";
3
- import { AlarmsApi } from "../apis/alarms-api";
4
- import { ChannelsApi } from "../apis/channels-api";
5
- import { ConnectionsApi } from "../apis/connections-api";
6
- import { MessagesApi } from "../apis/messages-api";
7
- import { NotificationsApi } from "../apis/notifications-api";
8
- import { PermissionsApi } from "../apis/permissions-api";
9
- import { ProcessorsApi } from "../apis/processors-api";
10
- import { TurnApi } from "../apis/turn-api";
11
- import { UsersApi } from "../apis/users-api";
12
1
  import type { DooverAuth } from "../auth/doover-auth";
13
- import { GatewayClient } from "../gateway/gateway-client";
14
2
  import { RestClient, type DooverClientConfig } from "../http/rest-client";
15
- import { RpcDispatcher } from "../rpc/rpc-dispatcher";
16
3
  import { DooverDataProvider } from "../viewer/doover-data-provider";
4
+ import { type Capability } from "./capabilities";
5
+ import type { AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, AgentScope, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client";
17
6
  import { DooverStatsCollector, type DooverStatsSnapshot } from "./stats";
18
- export declare class DooverClient {
7
+ export declare class DooverClient implements DataClient {
19
8
  readonly auth: DooverAuth;
20
9
  readonly rest: RestClient;
21
10
  readonly viewer: DooverDataProvider;
22
- readonly users: UsersApi;
23
- readonly channels: ChannelsApi;
24
- readonly messages: MessagesApi;
25
- readonly aggregates: AggregatesApi;
26
- readonly alarms: AlarmsApi;
27
- readonly connections: ConnectionsApi;
28
- readonly notifications: NotificationsApi;
29
- readonly permissions: PermissionsApi;
30
- readonly processors: ProcessorsApi;
31
- readonly turn: TurnApi;
32
- readonly agents: AgentsApi;
33
- readonly gateway: GatewayClient;
34
- readonly rpc: RpcDispatcher;
11
+ readonly users: UsersApiLike;
12
+ readonly channels: ChannelsApiLike;
13
+ readonly messages: MessagesApiLike;
14
+ readonly aggregates: AggregatesApiLike;
15
+ readonly alarms: AlarmsApiLike;
16
+ readonly connections: ConnectionsApiLike;
17
+ readonly notifications: NotificationsApiLike;
18
+ readonly permissions: PermissionsApiLike;
19
+ readonly processors: ProcessorsApiLike;
20
+ readonly turn: TurnApiLike;
21
+ readonly agents: AgentsApiLike;
22
+ readonly gateway: GatewayClientLike;
23
+ readonly rpc: RpcDispatcherLike;
35
24
  readonly stats: DooverStatsCollector;
25
+ private readonly identity;
26
+ private readonly statusTracker;
27
+ /** Underlying concrete gateway (the public `gateway` is the same object,
28
+ * typed as the structural `GatewayClientLike`). */
29
+ private readonly gatewayImpl;
30
+ /** Underlying concrete RPC dispatcher (the public `rpc` is the same object,
31
+ * typed as the structural `RpcDispatcherLike`). */
32
+ private readonly rpcImpl;
36
33
  constructor(config: DooverClientConfig);
37
34
  enableStats(): void;
38
35
  disableStats(): void;
39
36
  getStats(): DooverStatsSnapshot;
37
+ getCapabilities(): ReadonlySet<Capability>;
38
+ supports(cap: Capability): boolean;
39
+ getAgentScope(): Promise<AgentScope>;
40
+ getKnownAgentScope(): AgentScope | "unknown";
41
+ isConnected(): boolean;
42
+ getStatus(): DataClientStatus;
43
+ onStatusChange(listener: (status: DataClientStatus) => void): () => void;
40
44
  }
@@ -17,7 +17,11 @@ const gateway_client_1 = require("../gateway/gateway-client");
17
17
  const rest_client_1 = require("../http/rest-client");
18
18
  const rpc_dispatcher_1 = require("../rpc/rpc-dispatcher");
19
19
  const doover_data_provider_1 = require("../viewer/doover-data-provider");
20
+ const capabilities_1 = require("./capabilities");
21
+ const provenance_1 = require("./provenance");
22
+ const status_tracker_1 = require("./status-tracker");
20
23
  const stats_1 = require("./stats");
24
+ const ALL_CAPS_SET = new Set(capabilities_1.ALL_CAPABILITIES);
21
25
  class DooverClient {
22
26
  constructor(config) {
23
27
  this.auth = (0, build_auth_1.buildAuth)({
@@ -32,32 +36,61 @@ class DooverClient {
32
36
  authServerClientId: config.authServerClientId,
33
37
  fetchImpl: config.fetchImpl,
34
38
  });
39
+ this.identity = {
40
+ id: config.sourceId ?? "cloud",
41
+ kind: "cloud",
42
+ ...(config.sourceLabel ? { label: config.sourceLabel } : {}),
43
+ meta: {
44
+ dataRestUrl: config.dataRestUrl,
45
+ controlApiUrl: config.controlApiUrl,
46
+ ...(config.organisationId ? { organisationId: config.organisationId } : {}),
47
+ },
48
+ };
49
+ const stamper = new provenance_1.ProvenanceStamper(this.identity);
35
50
  this.rest = new rest_client_1.RestClient(config, this.auth);
36
- this.gateway = new gateway_client_1.GatewayClient(config, this.auth);
51
+ this.gatewayImpl = new gateway_client_1.GatewayClient(config, this.auth);
52
+ this.gatewayImpl.setProvenanceHook((value, ctx) => stamper.stampGatewayEvent(value, ctx));
53
+ this.gateway = this.gatewayImpl;
37
54
  this.viewer = new doover_data_provider_1.DooverDataProvider({
38
55
  rest: this.rest,
39
- gateway: this.gateway,
56
+ gateway: this.gatewayImpl,
40
57
  controlApiUrl: config.controlApiUrl,
41
58
  });
42
- this.users = new users_api_1.UsersApi(this.rest, config.controlApiUrl);
43
- this.channels = new channels_api_1.ChannelsApi(this.rest);
44
- this.messages = new messages_api_1.MessagesApi(this.rest);
45
- this.aggregates = new aggregates_api_1.AggregatesApi(this.rest);
46
- this.alarms = new alarms_api_1.AlarmsApi(this.rest);
47
- this.connections = new connections_api_1.ConnectionsApi(this.rest);
48
- this.notifications = new notifications_api_1.NotificationsApi(this.rest);
49
- this.permissions = new permissions_api_1.PermissionsApi(this.rest);
50
- this.processors = new processors_api_1.ProcessorsApi(this.rest);
51
- this.turn = new turn_api_1.TurnApi(this.rest);
52
- this.agents = new agents_api_1.AgentsApi(this.rest, config.controlApiUrl);
53
- this.rpc = new rpc_dispatcher_1.RpcDispatcher(this.gateway, this.messages);
59
+ this.users = (0, provenance_1.wrapSubclient)(new users_api_1.UsersApi(this.rest, config.controlApiUrl), "users", stamper);
60
+ this.channels = (0, provenance_1.wrapSubclient)(new channels_api_1.ChannelsApi(this.rest), "channels", stamper);
61
+ this.messages = (0, provenance_1.wrapSubclient)(new messages_api_1.MessagesApi(this.rest), "messages", stamper);
62
+ this.aggregates = (0, provenance_1.wrapSubclient)(new aggregates_api_1.AggregatesApi(this.rest), "aggregates", stamper);
63
+ this.alarms = (0, provenance_1.wrapSubclient)(new alarms_api_1.AlarmsApi(this.rest), "alarms", stamper);
64
+ this.connections = (0, provenance_1.wrapSubclient)(new connections_api_1.ConnectionsApi(this.rest), "connections", stamper);
65
+ this.notifications = (0, provenance_1.wrapSubclient)(new notifications_api_1.NotificationsApi(this.rest), "notifications", stamper);
66
+ this.permissions = (0, provenance_1.wrapSubclient)(new permissions_api_1.PermissionsApi(this.rest), "permissions", stamper);
67
+ this.processors = (0, provenance_1.wrapSubclient)(new processors_api_1.ProcessorsApi(this.rest), "processors", stamper);
68
+ this.turn = (0, provenance_1.wrapSubclient)(new turn_api_1.TurnApi(this.rest), "turn", stamper);
69
+ this.agents = (0, provenance_1.wrapSubclient)(new agents_api_1.AgentsApi(this.rest, config.controlApiUrl), "agents", stamper);
70
+ // RpcDispatcher needs the concrete MessagesApi (it calls postMessage internally);
71
+ // give it an *unwrapped* one so stamping happens once at the public boundary.
72
+ this.rpcImpl = new rpc_dispatcher_1.RpcDispatcher(this.gatewayImpl, new messages_api_1.MessagesApi(this.rest));
73
+ this.rpc = this.rpcImpl;
54
74
  this.stats = new stats_1.DooverStatsCollector();
55
75
  this.rest.setStats(this.stats);
56
- this.gateway.setStats(this.stats);
57
- this.rpc.setStats(this.stats);
76
+ this.gatewayImpl.setStats(this.stats);
77
+ this.rpcImpl.setStats(this.stats);
78
+ this.statusTracker = new status_tracker_1.ClientStatusTracker(this.identity.id, this.gateway, () => this.getKnownAgentScope());
58
79
  }
59
80
  enableStats() { this.stats.setEnabled(true); }
60
81
  disableStats() { this.stats.setEnabled(false); }
61
82
  getStats() { return this.stats.snapshot(); }
83
+ // --- DataClient: capabilities ---
84
+ getCapabilities() { return ALL_CAPS_SET; }
85
+ supports(cap) { return ALL_CAPS_SET.has(cap); }
86
+ // --- DataClient: agent scope (cloud serves every agent) ---
87
+ getAgentScope() { return Promise.resolve({ mode: "all" }); }
88
+ getKnownAgentScope() { return { mode: "all" }; }
89
+ // --- DataClient: status ---
90
+ isConnected() { return this.gateway.isConnected(); }
91
+ getStatus() { return this.statusTracker.getStatus(); }
92
+ onStatusChange(listener) {
93
+ return this.statusTracker.onChange(listener);
94
+ }
62
95
  }
63
96
  exports.DooverClient = DooverClient;
@@ -0,0 +1,23 @@
1
+ import type { Capability } from "./capabilities";
2
+ import { DooverApiError } from "../http/errors";
3
+ /**
4
+ * Thrown when a `DataClient` method is called whose backing capability the
5
+ * client does not advertise. Extends `DooverApiError` so existing
6
+ * `instanceof DooverApiError` error handling catches it; the HTTP-ish fields
7
+ * are placeholders since no request was made.
8
+ */
9
+ export declare class UnsupportedCapabilityError extends DooverApiError {
10
+ readonly capability: Capability;
11
+ readonly clientId?: string;
12
+ constructor(capability: Capability, clientId?: string);
13
+ }
14
+ /**
15
+ * Thrown by `MultiplexClient` when a write cannot be routed to a single
16
+ * member — more than one enabled member owns the targeted agent and has the
17
+ * write capability, and the call was not `sources`-scoped to one of them.
18
+ */
19
+ export declare class AmbiguousWriteError extends DooverApiError {
20
+ readonly capability: Capability;
21
+ readonly candidateSourceIds: string[];
22
+ constructor(capability: Capability, candidateSourceIds: string[]);
23
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = void 0;
4
+ const errors_1 = require("../http/errors");
5
+ /**
6
+ * Thrown when a `DataClient` method is called whose backing capability the
7
+ * client does not advertise. Extends `DooverApiError` so existing
8
+ * `instanceof DooverApiError` error handling catches it; the HTTP-ish fields
9
+ * are placeholders since no request was made.
10
+ */
11
+ class UnsupportedCapabilityError extends errors_1.DooverApiError {
12
+ constructor(capability, clientId) {
13
+ super({
14
+ status: 0,
15
+ body: { capability, clientId },
16
+ url: "",
17
+ method: "",
18
+ message: `Capability "${capability}" is not supported` +
19
+ (clientId ? ` by client "${clientId}"` : "") + ".",
20
+ });
21
+ this.name = "UnsupportedCapabilityError";
22
+ this.capability = capability;
23
+ this.clientId = clientId;
24
+ Object.setPrototypeOf(this, UnsupportedCapabilityError.prototype);
25
+ }
26
+ }
27
+ exports.UnsupportedCapabilityError = UnsupportedCapabilityError;
28
+ /**
29
+ * Thrown by `MultiplexClient` when a write cannot be routed to a single
30
+ * member — more than one enabled member owns the targeted agent and has the
31
+ * write capability, and the call was not `sources`-scoped to one of them.
32
+ */
33
+ class AmbiguousWriteError extends errors_1.DooverApiError {
34
+ constructor(capability, candidateSourceIds) {
35
+ super({
36
+ status: 0,
37
+ body: { capability, candidateSourceIds },
38
+ url: "",
39
+ method: "",
40
+ message: `Ambiguous write for "${capability}": ${candidateSourceIds.length} ` +
41
+ `members are eligible (${candidateSourceIds.join(", ")}). ` +
42
+ `Scope the call with { sources: [<one-id>] }.`,
43
+ });
44
+ this.name = "AmbiguousWriteError";
45
+ this.capability = capability;
46
+ this.candidateSourceIds = candidateSourceIds;
47
+ Object.setPrototypeOf(this, AmbiguousWriteError.prototype);
48
+ }
49
+ }
50
+ exports.AmbiguousWriteError = AmbiguousWriteError;
@@ -0,0 +1,65 @@
1
+ import { type DooverClientConfig } from "../http/rest-client";
2
+ import type { Capability } from "./capabilities";
3
+ import type { AgentScope, AgentsApiLike, AggregatesApiLike, AlarmsApiLike, ChannelsApiLike, ConnectionsApiLike, DataClient, DataClientStatus, GatewayClientLike, MessagesApiLike, NotificationsApiLike, PermissionsApiLike, ProcessorsApiLike, RpcDispatcherLike, TurnApiLike, UsersApiLike } from "./data-client";
4
+ export interface LocalAgentClientConfig {
5
+ /** Base URL of the local agent's REST API, e.g. "http://192.168.0.7:49100". */
6
+ baseUrl: string;
7
+ /** Base URL of the local agent's WebSocket gateway. Defaults to `baseUrl`
8
+ * with http(s)→ws(s). */
9
+ wssUrl?: string;
10
+ fetchImpl?: typeof fetch;
11
+ webSocketImpl?: typeof WebSocket;
12
+ webSocketFactory?: DooverClientConfig["webSocketFactory"];
13
+ disableBrowserLifecycleHooks?: boolean;
14
+ /** Stable source id. Defaults to `local:<host>:<port>` derived from baseUrl. */
15
+ sourceId?: string;
16
+ sourceLabel?: string;
17
+ /** Reserved for a future LAN auth blob — ignored in v1. */
18
+ auth?: unknown;
19
+ }
20
+ export declare class LocalAgentClient implements DataClient {
21
+ readonly agents: AgentsApiLike;
22
+ readonly channels: ChannelsApiLike;
23
+ readonly messages: MessagesApiLike;
24
+ readonly aggregates: AggregatesApiLike;
25
+ readonly alarms: AlarmsApiLike;
26
+ readonly connections: ConnectionsApiLike;
27
+ readonly notifications: NotificationsApiLike;
28
+ readonly permissions: PermissionsApiLike;
29
+ readonly processors: ProcessorsApiLike;
30
+ readonly turn: TurnApiLike;
31
+ readonly users: UsersApiLike;
32
+ readonly gateway: GatewayClientLike;
33
+ readonly rpc: RpcDispatcherLike;
34
+ private readonly identity;
35
+ private readonly capSet;
36
+ private readonly rest;
37
+ private readonly gatewayImpl;
38
+ private readonly statusTracker;
39
+ /** Resolved device-agent id list; null until first resolution. */
40
+ private resolvedScope;
41
+ private scopeResolving;
42
+ constructor(config: LocalAgentClientConfig);
43
+ getCapabilities(): ReadonlySet<Capability>;
44
+ supports(cap: Capability): boolean;
45
+ getAgentScope(): Promise<AgentScope>;
46
+ getKnownAgentScope(): AgentScope | "unknown";
47
+ isConnected(): boolean;
48
+ getStatus(): DataClientStatus;
49
+ onStatusChange(listener: (status: DataClientStatus) => void): () => void;
50
+ /**
51
+ * Wraps a real (provenance-wrapped) subclient so only `allowed` method names
52
+ * pass through; any other method call throws `UnsupportedCapabilityError`.
53
+ *
54
+ * Typing `real` as `WrappedSubclient<T>` and `allowed` as
55
+ * `ReadonlyArray<keyof WrappedSubclient<T> & string>` ensures the compiler
56
+ * catches typos in the allowed-method list at the call site.
57
+ */
58
+ private gatedSubclient;
59
+ /**
60
+ * Builds a Proxy that throws `UnsupportedCapabilityError` (mapped via
61
+ * METHOD_TO_CAPABILITY) for every method call. Used for subclients with no
62
+ * advertised methods.
63
+ */
64
+ private unsupportedSubclient;
65
+ }