doover-js 0.5.4 → 0.6.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 (157) hide show
  1. package/README.md +146 -0
  2. package/dist/apis/_args.d.ts +16 -0
  3. package/dist/apis/_args.js +26 -0
  4. package/dist/apis/agents-api.d.ts +4 -1
  5. package/dist/apis/agents-api.js +139 -1
  6. package/dist/apis/aggregates-api.d.ts +23 -3
  7. package/dist/apis/aggregates-api.js +33 -4
  8. package/dist/apis/alarms-api.d.ts +30 -0
  9. package/dist/apis/alarms-api.js +51 -6
  10. package/dist/apis/channels-api.d.ts +40 -3
  11. package/dist/apis/channels-api.js +50 -5
  12. package/dist/apis/connections-api.d.ts +21 -5
  13. package/dist/apis/connections-api.js +26 -10
  14. package/dist/apis/messages-api.d.ts +54 -9
  15. package/dist/apis/messages-api.js +94 -12
  16. package/dist/apis/notifications-api.d.ts +52 -0
  17. package/dist/apis/notifications-api.js +99 -13
  18. package/dist/apis/permissions-api.d.ts +8 -0
  19. package/dist/apis/permissions-api.js +11 -2
  20. package/dist/apis/processors-api.d.ts +35 -0
  21. package/dist/apis/processors-api.js +64 -8
  22. package/dist/apis/users-api.d.ts +8 -0
  23. package/dist/apis/users-api.js +13 -0
  24. package/dist/client/capabilities.d.ts +10 -0
  25. package/dist/client/capabilities.js +43 -0
  26. package/dist/client/data-client.d.ts +104 -0
  27. package/dist/client/data-client.js +2 -0
  28. package/dist/client/doover-client.d.ts +31 -31
  29. package/dist/client/doover-client.js +58 -37
  30. package/dist/client/errors.d.ts +23 -0
  31. package/dist/client/errors.js +50 -0
  32. package/dist/client/local-agent-client.d.ts +65 -0
  33. package/dist/client/local-agent-client.js +222 -0
  34. package/dist/client/multiplex-client.d.ts +151 -0
  35. package/dist/client/multiplex-client.js +744 -0
  36. package/dist/client/multiplex-gateway.d.ts +49 -0
  37. package/dist/client/multiplex-gateway.js +114 -0
  38. package/dist/client/multiplex-merge.d.ts +12 -0
  39. package/dist/client/multiplex-merge.js +42 -0
  40. package/dist/client/provenance.d.ts +60 -0
  41. package/dist/client/provenance.js +113 -0
  42. package/dist/client/stats.d.ts +24 -0
  43. package/dist/client/stats.js +62 -0
  44. package/dist/client/status-tracker.d.ts +24 -0
  45. package/dist/client/status-tracker.js +90 -0
  46. package/dist/gateway/gateway-client.d.ts +23 -1
  47. package/dist/gateway/gateway-client.js +75 -6
  48. package/dist/http/rest-client.d.ts +5 -0
  49. package/dist/index.d.ts +17 -0
  50. package/dist/index.js +24 -7
  51. package/dist/react/context.d.ts +9 -5
  52. package/dist/react/context.js +6 -2
  53. package/dist/react/index.d.ts +3 -0
  54. package/dist/react/index.js +3 -1
  55. package/dist/react/useChannelAggregate.d.ts +8 -1
  56. package/dist/react/useChannelAggregate.js +24 -6
  57. package/dist/react/useChannelMessage.d.ts +8 -1
  58. package/dist/react/useChannelMessage.js +17 -6
  59. package/dist/react/useChannelMessages.d.ts +8 -1
  60. package/dist/react/useChannelMessages.js +21 -7
  61. package/dist/react/useChannelSubscription.d.ts +6 -0
  62. package/dist/react/useChannelSubscription.js +8 -18
  63. package/dist/react/useClientStatus.d.ts +10 -0
  64. package/dist/react/useClientStatus.js +22 -0
  65. package/dist/react/useConnectionState.d.ts +5 -0
  66. package/dist/react/useConnectionState.js +5 -0
  67. package/dist/react/useMultiAgentAggregates.d.ts +8 -1
  68. package/dist/react/useMultiAgentAggregates.js +25 -21
  69. package/dist/react/useMultiAgentChannelMessages.d.ts +19 -1
  70. package/dist/react/useMultiAgentChannelMessages.js +42 -19
  71. package/dist/react/useSendMessage.d.ts +8 -1
  72. package/dist/react/useSendMessage.js +15 -2
  73. package/dist/react/useSendRpc.js +4 -1
  74. package/dist/react/useUpdateAggregate.d.ts +6 -0
  75. package/dist/react/useUpdateAggregate.js +20 -4
  76. package/dist/react/useUpdateMessage.d.ts +5 -0
  77. package/dist/react/useUpdateMessage.js +11 -2
  78. package/dist/rpc/errors.d.ts +10 -0
  79. package/dist/rpc/errors.js +23 -0
  80. package/dist/rpc/rpc-dispatcher.d.ts +28 -0
  81. package/dist/rpc/rpc-dispatcher.js +129 -0
  82. package/dist/test/api-args.test.d.ts +1 -0
  83. package/dist/test/api-args.test.js +48 -0
  84. package/dist/test/api-overloads.test.d.ts +9 -0
  85. package/dist/test/api-overloads.test.js +276 -0
  86. package/dist/test/capabilities.test.d.ts +1 -0
  87. package/dist/test/capabilities.test.js +27 -0
  88. package/dist/test/data-client-errors.test.d.ts +1 -0
  89. package/dist/test/data-client-errors.test.js +24 -0
  90. package/dist/test/data-client-shape.test.d.ts +1 -0
  91. package/dist/test/data-client-shape.test.js +28 -0
  92. package/dist/test/doover-client-dataclient.test.d.ts +1 -0
  93. package/dist/test/doover-client-dataclient.test.js +58 -0
  94. package/dist/test/doover-client-provenance.test.d.ts +1 -0
  95. package/dist/test/doover-client-provenance.test.js +32 -0
  96. package/dist/test/exports.test.d.ts +1 -0
  97. package/dist/test/exports.test.js +76 -0
  98. package/dist/test/gateway-provenance.test.d.ts +1 -0
  99. package/dist/test/gateway-provenance.test.js +29 -0
  100. package/dist/test/gateway-subscriptions.test.d.ts +1 -0
  101. package/dist/test/gateway-subscriptions.test.js +95 -0
  102. package/dist/test/local-agent-client.test.d.ts +1 -0
  103. package/dist/test/local-agent-client.test.js +202 -0
  104. package/dist/test/multiplex-capabilities.test.d.ts +1 -0
  105. package/dist/test/multiplex-capabilities.test.js +37 -0
  106. package/dist/test/multiplex-conflicts.test.d.ts +1 -0
  107. package/dist/test/multiplex-conflicts.test.js +35 -0
  108. package/dist/test/multiplex-gateway.test.d.ts +1 -0
  109. package/dist/test/multiplex-gateway.test.js +103 -0
  110. package/dist/test/multiplex-merge.test.d.ts +1 -0
  111. package/dist/test/multiplex-merge.test.js +19 -0
  112. package/dist/test/multiplex-noncore.test.d.ts +1 -0
  113. package/dist/test/multiplex-noncore.test.js +82 -0
  114. package/dist/test/multiplex-passthrough.test.d.ts +1 -0
  115. package/dist/test/multiplex-passthrough.test.js +35 -0
  116. package/dist/test/multiplex-reads.test.d.ts +1 -0
  117. package/dist/test/multiplex-reads.test.js +87 -0
  118. package/dist/test/multiplex-registry.test.d.ts +1 -0
  119. package/dist/test/multiplex-registry.test.js +81 -0
  120. package/dist/test/multiplex-routing.test.d.ts +1 -0
  121. package/dist/test/multiplex-routing.test.js +45 -0
  122. package/dist/test/multiplex-status.test.d.ts +1 -0
  123. package/dist/test/multiplex-status.test.js +63 -0
  124. package/dist/test/multiplex-writes.test.d.ts +1 -0
  125. package/dist/test/multiplex-writes.test.js +93 -0
  126. package/dist/test/provenance-regression.test.d.ts +1 -0
  127. package/dist/test/provenance-regression.test.js +45 -0
  128. package/dist/test/provenance-stamper.test.d.ts +1 -0
  129. package/dist/test/provenance-stamper.test.js +41 -0
  130. package/dist/test/provenance-type.test.d.ts +1 -0
  131. package/dist/test/provenance-type.test.js +15 -0
  132. package/dist/test/react-client-status.test.d.ts +1 -0
  133. package/dist/test/react-client-status.test.js +35 -0
  134. package/dist/test/react-dataclient.test.d.ts +1 -0
  135. package/dist/test/react-dataclient.test.js +28 -0
  136. package/dist/test/react-exports.test.d.ts +1 -0
  137. package/dist/test/react-exports.test.js +45 -0
  138. package/dist/test/react-sources-option.test.d.ts +1 -0
  139. package/dist/test/react-sources-option.test.js +41 -0
  140. package/dist/test/react.test.js +2 -2
  141. package/dist/test/rpc-dispatcher.test.d.ts +1 -0
  142. package/dist/test/rpc-dispatcher.test.js +190 -0
  143. package/dist/test/stats-rpc.test.d.ts +1 -0
  144. package/dist/test/stats-rpc.test.js +69 -0
  145. package/dist/test/status-tracker.test.d.ts +1 -0
  146. package/dist/test/status-tracker.test.js +85 -0
  147. package/dist/test/users-api.test.d.ts +1 -0
  148. package/dist/test/users-api.test.js +19 -0
  149. package/dist/types/audit.d.ts +53 -0
  150. package/dist/types/audit.js +14 -0
  151. package/dist/types/common.d.ts +16 -0
  152. package/dist/types/provenance.d.ts +45 -0
  153. package/dist/types/provenance.js +2 -0
  154. package/dist/types/viewer.d.ts +3 -0
  155. package/dist/viewer/doover-data-provider.d.ts +30 -34
  156. package/dist/viewer/doover-data-provider.js +82 -316
  157. package/package.json +1 -1
@@ -0,0 +1,744 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MultiplexClient = void 0;
4
+ const multiplex_merge_1 = require("./multiplex-merge");
5
+ const errors_1 = require("./errors");
6
+ const snowflake_1 = require("../utils/snowflake");
7
+ const multiplex_gateway_1 = require("./multiplex-gateway");
8
+ /** "<subclient>.<method>" → spec. Drives the generic non-core facades. */
9
+ const NONCORE_METHODS = {
10
+ // alarms
11
+ "alarms.listAlarms": { cap: "alarms.read", kind: "read-fanout-first", agentScoped: true },
12
+ "alarms.getAlarm": { cap: "alarms.read", kind: "read-fanout-first", agentScoped: true },
13
+ "alarms.createAlarm": { cap: "alarms.write", kind: "write-route", agentScoped: true },
14
+ "alarms.putAlarm": { cap: "alarms.write", kind: "write-route", agentScoped: true },
15
+ "alarms.patchAlarm": { cap: "alarms.write", kind: "write-route", agentScoped: true },
16
+ "alarms.deleteAlarm": { cap: "alarms.write", kind: "write-route", agentScoped: true },
17
+ // connections
18
+ "connections.getAgentConnections": { cap: "connections.read", kind: "read-fanout-first", agentScoped: true },
19
+ "connections.getAgentConnectionHistory": { cap: "connections.read", kind: "read-fanout-first", agentScoped: true },
20
+ "connections.getAgentSubscriptionHistory": { cap: "connections.read", kind: "read-fanout-first", agentScoped: true },
21
+ "connections.getConnection": { cap: "connections.read", kind: "read-fanout-first", agentScoped: false },
22
+ "connections.getChannelSubscriptions": { cap: "connections.read", kind: "read-fanout-first", agentScoped: true },
23
+ "connections.syncConnection": { cap: "connections.write", kind: "write-route", agentScoped: true },
24
+ // notifications (reads)
25
+ "notifications.getAgentNotifications": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: true },
26
+ "notifications.getAgentNotificationEndpoints": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: true },
27
+ "notifications.getAgentNotificationSubscriptions": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: true },
28
+ "notifications.getAgentDefaultNotificationSubscriptions": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: true },
29
+ "notifications.getAgentNotificationSubscribers": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: true },
30
+ "notifications.getWebPushPublicKey": { cap: "notifications.read", kind: "read-fanout-first", agentScoped: false },
31
+ // notifications (writes)
32
+ "notifications.createNotificationEndpoint": { cap: "notifications.write", kind: "write-route", agentScoped: true },
33
+ "notifications.updateNotificationEndpoint": { cap: "notifications.write", kind: "write-route", agentScoped: true },
34
+ "notifications.deleteNotificationEndpoint": { cap: "notifications.write", kind: "write-route", agentScoped: true },
35
+ "notifications.testNotificationEndpoint": { cap: "notifications.write", kind: "write-route", agentScoped: true },
36
+ "notifications.createNotificationSubscription": { cap: "notifications.write", kind: "write-route", agentScoped: true },
37
+ "notifications.deleteDefaultNotificationSubscription": { cap: "notifications.write", kind: "write-route", agentScoped: true },
38
+ "notifications.updateNotificationSubscription": { cap: "notifications.write", kind: "write-route", agentScoped: true },
39
+ "notifications.deleteNotificationSubscription": { cap: "notifications.write", kind: "write-route", agentScoped: true },
40
+ "notifications.updateMeWebPushEndpoint": { cap: "notifications.write", kind: "write-route", agentScoped: false },
41
+ // permissions
42
+ "permissions.getAgentPermission": { cap: "permissions.read", kind: "read-fanout-first", agentScoped: true },
43
+ "permissions.getAgentPermissionDebug": { cap: "permissions.read", kind: "read-fanout-first", agentScoped: true },
44
+ "permissions.syncPermissions": { cap: "permissions.write", kind: "write-route", agentScoped: false },
45
+ // processors
46
+ "processors.getScheduleInfo": { cap: "processors.read", kind: "read-fanout-first", agentScoped: false },
47
+ "processors.getScheduleInfoAlias": { cap: "processors.read", kind: "read-fanout-first", agentScoped: false },
48
+ "processors.getProcessorSubscriptionInfo": { cap: "processors.read", kind: "read-fanout-first", agentScoped: false },
49
+ "processors.getProcessorSubscriptionInfoAlias": { cap: "processors.read", kind: "read-fanout-first", agentScoped: false },
50
+ "processors.createProcessorSchedule": { cap: "processors.write", kind: "write-route", agentScoped: true },
51
+ "processors.deleteProcessorSchedule": { cap: "processors.write", kind: "write-route", agentScoped: true },
52
+ "processors.regenerateScheduleToken": { cap: "processors.write", kind: "write-route", agentScoped: true },
53
+ "processors.createProcessorSubscription": { cap: "processors.write", kind: "write-route", agentScoped: true },
54
+ "processors.deleteProcessorSubscription": { cap: "processors.write", kind: "write-route", agentScoped: true },
55
+ "processors.createIngestionEndpoint": { cap: "processors.write", kind: "write-route", agentScoped: true },
56
+ "processors.deleteIngestionEndpoint": { cap: "processors.write", kind: "write-route", agentScoped: true },
57
+ "processors.invokeIngestionEndpoint": { cap: "processors.write", kind: "write-route", agentScoped: true },
58
+ // turn / users
59
+ "turn.createTurnToken": { cap: "turn.credentials", kind: "read-fanout-first", agentScoped: false },
60
+ "users.getMe": { cap: "users.me", kind: "read-fanout-first", agentScoped: false },
61
+ };
62
+ class MultiplexClient {
63
+ constructor(options) {
64
+ this.registry = new Map();
65
+ this.eventListeners = new Map();
66
+ this.capabilitiesCache = null;
67
+ this.lastConflicts = [];
68
+ this.maxConflicts = 50;
69
+ this.statusListeners = new Set();
70
+ /** sourceId → member unsubscribe fn for its onStatusChange. */
71
+ this.memberStatusUnsubs = new Map();
72
+ this.factory = options.factory;
73
+ this.disconnectOnDisable = options.disconnectOnDisable ?? true;
74
+ this.clientId = options.clientId ?? "multiplex";
75
+ for (const d of options.register ?? [])
76
+ this.registerSource(d);
77
+ this.agents = this.makeAgentsFacade();
78
+ this.channels = this.makeChannelsFacade();
79
+ this.messages = this.makeMessagesFacade();
80
+ this.aggregates = this.makeAggregatesFacade();
81
+ this.alarms = this.makeReadFacade("alarms");
82
+ this.connections = this.makeReadFacade("connections");
83
+ this.notifications = this.makeReadFacade("notifications");
84
+ this.permissions = this.makeReadFacade("permissions");
85
+ this.processors = this.makeReadFacade("processors");
86
+ this.turn = this.makeReadFacade("turn");
87
+ this.users = this.makeReadFacade("users");
88
+ this.rpc = this.makeRpcFacade();
89
+ this.gateway = this.makeCompositeGateway();
90
+ const toEnable = options.enableAll
91
+ ? (options.register ?? []).map((d) => d.id)
92
+ : (options.enable ?? []);
93
+ for (const id of toEnable)
94
+ this.enableSource(id);
95
+ }
96
+ // ===== registry / activation =====
97
+ registerSource(descriptor) {
98
+ const existing = this.registry.get(descriptor.id);
99
+ if (existing) {
100
+ existing.descriptor = { ...existing.descriptor, ...descriptor };
101
+ return;
102
+ }
103
+ this.registry.set(descriptor.id, { descriptor, enabled: false });
104
+ }
105
+ setActiveSources(idsOrDescriptors) {
106
+ const wanted = new Set();
107
+ for (const item of idsOrDescriptors) {
108
+ if (typeof item === "string") {
109
+ if (!this.registry.has(item)) {
110
+ throw new Error(`MultiplexClient.setActiveSources: unknown source id "${item}" — pass a descriptor to auto-register.`);
111
+ }
112
+ wanted.add(item);
113
+ }
114
+ else {
115
+ this.registerSource(item);
116
+ wanted.add(item.id);
117
+ }
118
+ }
119
+ const currentlyEnabled = new Set([...this.registry.values()].filter((s) => s.enabled).map((s) => s.descriptor.id));
120
+ const same = wanted.size === currentlyEnabled.size && [...wanted].every((id) => currentlyEnabled.has(id));
121
+ if (same)
122
+ return;
123
+ for (const id of currentlyEnabled)
124
+ if (!wanted.has(id))
125
+ this.disableSource(id, { silent: true });
126
+ for (const id of wanted)
127
+ if (!currentlyEnabled.has(id))
128
+ this.enableSource(id, { silent: true });
129
+ this.invalidateCapabilities();
130
+ this.emit("change");
131
+ }
132
+ enableSource(id, opts) {
133
+ const src = this.registry.get(id);
134
+ if (!src)
135
+ throw new Error(`MultiplexClient.enableSource: unknown source id "${id}".`);
136
+ if (src.enabled)
137
+ return;
138
+ if (!src.client) {
139
+ const built = this.factory(src.descriptor);
140
+ if (built instanceof Promise) {
141
+ src.enabled = true;
142
+ void built.then((client) => {
143
+ src.client = client;
144
+ this.attachMemberListeners(src);
145
+ this.invalidateCapabilities();
146
+ this.emit("change");
147
+ });
148
+ if (!opts?.silent) {
149
+ this.invalidateCapabilities();
150
+ this.emit("change");
151
+ }
152
+ return;
153
+ }
154
+ src.client = built;
155
+ }
156
+ src.enabled = true;
157
+ this.attachMemberListeners(src);
158
+ if (!opts?.silent) {
159
+ this.invalidateCapabilities();
160
+ this.emit("change");
161
+ }
162
+ }
163
+ disableSource(id, opts) {
164
+ const src = this.registry.get(id);
165
+ if (!src || !src.enabled)
166
+ return;
167
+ src.enabled = false;
168
+ this.detachMemberListeners(src);
169
+ if (this.disconnectOnDisable && src.client) {
170
+ try {
171
+ src.client.gateway.disconnect();
172
+ }
173
+ catch { /* ignore */ }
174
+ }
175
+ if (!opts?.silent) {
176
+ this.invalidateCapabilities();
177
+ this.emit("change");
178
+ }
179
+ }
180
+ removeSource(id) {
181
+ const src = this.registry.get(id);
182
+ if (!src)
183
+ return;
184
+ if (src.enabled)
185
+ this.disableSource(id, { silent: true });
186
+ else if (src.client) {
187
+ try {
188
+ src.client.gateway.disconnect();
189
+ }
190
+ catch { /* ignore */ }
191
+ }
192
+ this.registry.delete(id);
193
+ this.invalidateCapabilities();
194
+ this.emit("change");
195
+ }
196
+ getActiveSources() {
197
+ return [...this.registry.values()].filter((s) => s.enabled);
198
+ }
199
+ getRegisteredSources() {
200
+ return [...this.registry.values()];
201
+ }
202
+ /** Enabled members that have a built `client`. */
203
+ enabledClients() {
204
+ return this.getActiveSources()
205
+ .filter((s) => !!s.client)
206
+ .map((s) => ({ id: s.descriptor.id, src: s, client: s.client }));
207
+ }
208
+ // ===== events =====
209
+ on(event, handler) {
210
+ (this.eventListeners.get(event) ?? this.eventListeners.set(event, new Set()).get(event)).add(handler);
211
+ }
212
+ off(event, handler) {
213
+ this.eventListeners.get(event)?.delete(handler);
214
+ }
215
+ emit(event, ...args) {
216
+ this.eventListeners.get(event)?.forEach((h) => h(...args));
217
+ // keep status observers in sync whenever the member set changes
218
+ if (event === "change")
219
+ this.emitStatus();
220
+ }
221
+ invalidateCapabilities() { this.capabilitiesCache = null; }
222
+ attachMemberListeners(src) {
223
+ if (!src.client)
224
+ return;
225
+ if (this.memberStatusUnsubs.has(src.descriptor.id))
226
+ return;
227
+ const unsub = src.client.onStatusChange(() => this.emitStatus());
228
+ this.memberStatusUnsubs.set(src.descriptor.id, unsub);
229
+ }
230
+ detachMemberListeners(src) {
231
+ const unsub = this.memberStatusUnsubs.get(src.descriptor.id);
232
+ if (unsub) {
233
+ unsub();
234
+ this.memberStatusUnsubs.delete(src.descriptor.id);
235
+ }
236
+ }
237
+ makeCompositeGateway() {
238
+ const host = {
239
+ gatewayMembers: () => this.enabledClients()
240
+ .filter(({ client }) => client.supports("gateway.subscribe"))
241
+ .map(({ id, client }) => ({ id, gateway: client.gateway })),
242
+ gatewayMembersForAgent: (agentId) => this.membersForAgentWithCapability(agentId, "gateway.subscribe")
243
+ .map(({ id, client }) => ({ id, gateway: client.gateway })),
244
+ };
245
+ return new multiplex_gateway_1.MultiplexGateway(host);
246
+ }
247
+ // ===== helpers =====
248
+ /** Resolve the optional `{ sources?: string[] }` bag from a method's trailing
249
+ * arg: returns the candidate members, narrowed to `sources` if given (each
250
+ * must be enabled), still capability-filtered by the caller. The bag is the
251
+ * LAST argument if it is a plain object with a `sources` array. */
252
+ splitSourcesOption(args) {
253
+ const last = args[args.length - 1];
254
+ if (last && typeof last === "object" && !Array.isArray(last) && Array.isArray(last.sources)) {
255
+ return { args: args.slice(0, -1), sources: last.sources };
256
+ }
257
+ return { args };
258
+ }
259
+ candidates(agentId, cap, sources) {
260
+ let members = this.membersForAgentWithCapability(agentId, cap);
261
+ if (sources) {
262
+ const allow = new Set(sources);
263
+ members = members.filter((m) => allow.has(m.id));
264
+ }
265
+ return members;
266
+ }
267
+ /** Extract the agent id from the first positional arg (string or identifier object). */
268
+ extractAgentId(args) {
269
+ return typeof args[0] === "string" ? args[0] : args[0]?.agentId;
270
+ }
271
+ /** Throw if no enabled member supports `cap` at all (regardless of agent). */
272
+ assertSomeMemberSupports(cap) {
273
+ if (!this.enabledClients().some(({ client }) => client.supports(cap))) {
274
+ throw new errors_1.UnsupportedCapabilityError(cap, this.clientId);
275
+ }
276
+ }
277
+ isNotFound(err) {
278
+ return !!err && typeof err === "object" && err.status === 404;
279
+ }
280
+ requiredMessagesCapability(params) {
281
+ if (params?.after)
282
+ return "messages.listHistorical";
283
+ if (params?.before) {
284
+ try {
285
+ const { timestamp: ts } = (0, snowflake_1.extractSnowflakeId)(params.before);
286
+ if (typeof ts === "number" && ts < Date.now() - 24 * 60 * 60 * 1000)
287
+ return "messages.listHistorical";
288
+ }
289
+ catch { /* not a valid snowflake, treat as latest */ }
290
+ }
291
+ return "messages.list";
292
+ }
293
+ unsupportedMethod(_method, cap) {
294
+ return () => Promise.reject(new errors_1.UnsupportedCapabilityError(cap, this.clientId));
295
+ }
296
+ guessNonCoreCap(subclient) {
297
+ switch (subclient) {
298
+ case "alarms": return "alarms.read";
299
+ case "connections": return "connections.read";
300
+ case "notifications": return "notifications.read";
301
+ case "permissions": return "permissions.read";
302
+ case "processors": return "processors.read";
303
+ case "turn": return "turn.credentials";
304
+ case "users": return "users.me";
305
+ default: return "channels.get";
306
+ }
307
+ }
308
+ recordConflict(method, agentId, channelName, values) {
309
+ // Only a conflict if the values differ (cheap structural compare).
310
+ const json = values.map((v) => JSON.stringify(v.value, (k, val) => (k === "__source" ? undefined : val)));
311
+ const allEqual = json.every((j) => j === json[0]);
312
+ if (allEqual)
313
+ return;
314
+ const conflict = { method, agentId, channelName, sourceIds: values.map((v) => v.id), values: values.map((v) => v.value), at: Date.now() };
315
+ this.lastConflicts.push(conflict);
316
+ if (this.lastConflicts.length > this.maxConflicts)
317
+ this.lastConflicts.shift();
318
+ this.emit("conflict", conflict);
319
+ }
320
+ getLastConflicts() { return this.lastConflicts; }
321
+ /**
322
+ * Returns a method that routes a write to exactly one member:
323
+ * (a) `{ sources: [oneId] }` scoping → that member;
324
+ * (b) else the single enabled member that owns the targeted agent and has `cap`;
325
+ * (c) else `UnsupportedCapabilityError` (none) or `AmbiguousWriteError` (>1).
326
+ * The targeted agent id is `args[0]` (string form) or `args[0].agentId` (identifier form).
327
+ * Writes are never fanned out.
328
+ */
329
+ routedWrite(method, cap) {
330
+ const self = this;
331
+ return async function (...rawArgs) {
332
+ const { args, sources } = self.splitSourcesOption(rawArgs);
333
+ const agentId = self.extractAgentId(args);
334
+ let members = self.candidates(agentId, cap, sources);
335
+ if (sources && sources.length === 1) {
336
+ const only = members.find((m) => m.id === sources[0]);
337
+ if (!only)
338
+ throw new errors_1.UnsupportedCapabilityError(cap, sources[0]);
339
+ members = [only];
340
+ }
341
+ if (members.length === 0)
342
+ throw new errors_1.UnsupportedCapabilityError(cap, self.clientId);
343
+ if (members.length > 1)
344
+ throw new errors_1.AmbiguousWriteError(cap, members.map((m) => m.id));
345
+ const target = members[0];
346
+ const subclient = method.split(".")[0];
347
+ const fnName = method.split(".")[1];
348
+ const fn = target.client[subclient][fnName];
349
+ return fn.apply(target.client[subclient], args);
350
+ };
351
+ }
352
+ /** Agent-scoped "first success" — used by messages.getTimeseries / getInvocationLogs. */
353
+ makeFanoutFirst(method, cap) {
354
+ const self = this;
355
+ return async function (...rawArgs) {
356
+ const { args, sources } = self.splitSourcesOption(rawArgs);
357
+ self.assertSomeMemberSupports(cap);
358
+ const agentId = self.extractAgentId(args);
359
+ const members = self.candidates(agentId, cap, sources);
360
+ if (members.length === 0)
361
+ throw new errors_1.UnsupportedCapabilityError(cap, self.clientId);
362
+ const subclient = method.split(".")[0];
363
+ const fnName = method.split(".")[1];
364
+ let lastErr;
365
+ for (const m of members) {
366
+ try {
367
+ return await m.client[subclient][fnName](...args);
368
+ }
369
+ catch (e) {
370
+ lastErr = e;
371
+ if (!self.isNotFound(e))
372
+ throw e;
373
+ }
374
+ }
375
+ throw lastErr ?? new Error(`${method}: no member returned a result`);
376
+ };
377
+ }
378
+ /** Used by messages.getMessageAttachment — returns the first member that yields a Blob. */
379
+ makeBlobFanout(method, cap) {
380
+ const self = this;
381
+ return async function (...rawArgs) {
382
+ const { args, sources } = self.splitSourcesOption(rawArgs);
383
+ self.assertSomeMemberSupports(cap);
384
+ const agentId = self.extractAgentId(args);
385
+ const members = self.candidates(agentId, cap, sources);
386
+ if (members.length === 0)
387
+ throw new errors_1.UnsupportedCapabilityError(cap, self.clientId);
388
+ const subclient = method.split(".")[0];
389
+ const fnName = method.split(".")[1];
390
+ let lastErr;
391
+ for (const m of members) {
392
+ try {
393
+ return await m.client[subclient][fnName](...args);
394
+ }
395
+ catch (e) {
396
+ lastErr = e;
397
+ if (!self.isNotFound(e))
398
+ throw e;
399
+ }
400
+ }
401
+ throw lastErr ?? new Error(`${method}: no member returned a result`);
402
+ };
403
+ }
404
+ // ===== core facades =====
405
+ makeChannelsFacade() {
406
+ const self = this;
407
+ return {
408
+ async listChannels(agentIdOrId, ...rest) {
409
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
410
+ const agentId = self.extractAgentId(args);
411
+ self.assertSomeMemberSupports("channels.list");
412
+ const members = self.candidates(agentId, "channels.list", sources);
413
+ const perMember = await Promise.all(members.map((m) => m.client.channels.listChannels(...args)));
414
+ return (0, multiplex_merge_1.dedupeBy)([].concat(...perMember), (c) => c.name);
415
+ },
416
+ async getChannel(agentIdOrId, ...rest) {
417
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
418
+ const agentId = self.extractAgentId(args);
419
+ const name = typeof args[0] === "string" ? args[1] : args[0].channelName;
420
+ self.assertSomeMemberSupports("channels.get");
421
+ const members = self.candidates(agentId, "channels.get", sources);
422
+ const settled = await Promise.allSettled(members.map((m) => m.client.channels.getChannel(...args)));
423
+ const ok = settled.flatMap((r, i) => r.status === "fulfilled" ? [{ value: r.value, id: members[i].id }] : (r.status === "rejected" && !self.isNotFound(r.reason) ? (() => { throw r.reason; })() : []));
424
+ if (ok.length === 0) {
425
+ const e = new Error(`channel ${agentId}/${name} not found`);
426
+ e.status = 404;
427
+ throw e;
428
+ }
429
+ if (ok.length > 1)
430
+ self.recordConflict("channels.getChannel", agentId, name, ok);
431
+ return ok[0].value;
432
+ },
433
+ createChannel: self.routedWrite("channels.createChannel", "channels.create"),
434
+ putChannel: self.routedWrite("channels.putChannel", "channels.create"),
435
+ archiveChannel: self.routedWrite("channels.archiveChannel", "channels.archive"),
436
+ unarchiveChannel: self.routedWrite("channels.unarchiveChannel", "channels.archive"),
437
+ listDataSeries: self.unsupportedMethod("channels.listDataSeries", "channels.dataSeries"),
438
+ };
439
+ }
440
+ makeAggregatesFacade() {
441
+ const self = this;
442
+ return {
443
+ async getAggregate(agentIdOrId, ...rest) {
444
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
445
+ const agentId = self.extractAgentId(args);
446
+ const name = typeof args[0] === "string" ? args[1] : args[0].channelName;
447
+ self.assertSomeMemberSupports("aggregates.get");
448
+ const members = self.candidates(agentId, "aggregates.get", sources);
449
+ const settled = await Promise.allSettled(members.map((m) => m.client.aggregates.getAggregate(...args)));
450
+ const ok = settled.flatMap((r, i) => r.status === "fulfilled" ? [{ value: r.value, id: members[i].id }] : (r.status === "rejected" && !self.isNotFound(r.reason) ? (() => { throw r.reason; })() : []));
451
+ if (ok.length === 0) {
452
+ const e = new Error(`aggregate ${agentId}/${name} not found`);
453
+ e.status = 404;
454
+ throw e;
455
+ }
456
+ if (ok.length > 1)
457
+ self.recordConflict("aggregates.getAggregate", agentId, name, ok);
458
+ return ok[0].value;
459
+ },
460
+ putAggregate: self.routedWrite("aggregates.putAggregate", "aggregates.put"),
461
+ patchAggregate: self.routedWrite("aggregates.patchAggregate", "aggregates.patch"),
462
+ async getAggregateAttachment(agentIdOrId, ...rest) {
463
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
464
+ const agentId = self.extractAgentId(args);
465
+ self.assertSomeMemberSupports("aggregates.attachment");
466
+ const members = self.candidates(agentId, "aggregates.attachment", sources);
467
+ for (const m of members) {
468
+ try {
469
+ return await m.client.aggregates.getAggregateAttachment(...args);
470
+ }
471
+ catch (e) {
472
+ if (!self.isNotFound(e))
473
+ throw e;
474
+ }
475
+ }
476
+ const e = new Error("attachment not found");
477
+ e.status = 404;
478
+ throw e;
479
+ },
480
+ };
481
+ }
482
+ makeMessagesFacade() {
483
+ const self = this;
484
+ return {
485
+ async listMessages(agentIdOrId, ...rest) {
486
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
487
+ const agentId = self.extractAgentId(args);
488
+ const params = (typeof args[0] === "string" ? args[2] : args[1]);
489
+ const implied = self.requiredMessagesCapability(params);
490
+ // a member is eligible if it has the implied cap; for "latest N" that IS messages.list.
491
+ self.assertSomeMemberSupports(implied);
492
+ const members = self.candidates(agentId, implied, sources);
493
+ const perMember = await Promise.all(members.map((m) => m.client.messages.listMessages(...args)));
494
+ return (0, multiplex_merge_1.mergeMessages)(perMember, { order: params?.order ?? "desc", limit: params?.limit });
495
+ },
496
+ async getMessage(agentIdOrId, ...rest) {
497
+ const { args, sources } = self.splitSourcesOption([agentIdOrId, ...rest]);
498
+ const agentId = self.extractAgentId(args);
499
+ self.assertSomeMemberSupports("messages.get");
500
+ const members = self.candidates(agentId, "messages.get", sources);
501
+ for (const m of members) {
502
+ try {
503
+ return await m.client.messages.getMessage(...args);
504
+ }
505
+ catch (e) {
506
+ if (!self.isNotFound(e))
507
+ throw e;
508
+ }
509
+ }
510
+ const e = new Error("message not found");
511
+ e.status = 404;
512
+ throw e;
513
+ },
514
+ postMessage: self.routedWrite("messages.postMessage", "messages.post"),
515
+ putMessage: self.routedWrite("messages.putMessage", "messages.put"),
516
+ patchMessage: self.routedWrite("messages.patchMessage", "messages.put"),
517
+ deleteMessage: self.routedWrite("messages.deleteMessage", "messages.delete"),
518
+ getTimeseries: self.makeFanoutFirst("messages.getTimeseries", "messages.timeseries"),
519
+ getMessageAttachment: self.makeBlobFanout("messages.getMessageAttachment", "messages.attachment"),
520
+ getInvocationLogs: self.makeFanoutFirst("messages.getInvocationLogs", "messages.invocationLogs"),
521
+ createMultipartPayload(jsonPayload, attachments) {
522
+ // pure helper — no member needed; mirror MessagesApi.createMultipartPayload
523
+ const formData = new FormData();
524
+ formData.set("json_payload", JSON.stringify(jsonPayload));
525
+ attachments.forEach((attachment, index) => {
526
+ formData.set(`attachment-${index}`, attachment);
527
+ });
528
+ return formData;
529
+ },
530
+ };
531
+ }
532
+ makeAgentsFacade() {
533
+ const self = this;
534
+ return {
535
+ async listAgents(...rawArgs) {
536
+ const { args, sources } = self.splitSourcesOption(rawArgs);
537
+ self.assertSomeMemberSupports("agents.list");
538
+ let members = self.enabledClients().filter(({ client }) => client.supports("agents.list"));
539
+ if (sources) {
540
+ const allow = new Set(sources);
541
+ members = members.filter((m) => allow.has(m.id));
542
+ }
543
+ const responses = await Promise.all(members.map((m) => m.client.agents.listAgents(...args)));
544
+ const merged = (0, multiplex_merge_1.dedupeBy)([].concat(...responses.map((r) => (r.agents ?? r.results ?? []))), (a) => a.id);
545
+ return { agents: merged, results: merged, count: merged.length };
546
+ },
547
+ async getMultiAgentMessages(channelName, ...rest) {
548
+ const { args, sources } = self.splitSourcesOption([channelName, ...rest]);
549
+ self.assertSomeMemberSupports("agents.multiAgentMessages");
550
+ let members = self.enabledClients().filter(({ client }) => client.supports("agents.multiAgentMessages"));
551
+ if (sources) {
552
+ const allow = new Set(sources);
553
+ members = members.filter((m) => allow.has(m.id));
554
+ }
555
+ const responses = await Promise.all(members.map((m) => m.client.agents.getMultiAgentMessages(...args)));
556
+ const merged = (0, multiplex_merge_1.mergeMessages)(responses.map((r) => r.results), { order: "desc" });
557
+ return { results: merged, count: merged.length };
558
+ },
559
+ async getMultiAgentAggregates(channelName, ...rest) {
560
+ const { args, sources } = self.splitSourcesOption([channelName, ...rest]);
561
+ self.assertSomeMemberSupports("agents.multiAgentAggregates");
562
+ let members = self.enabledClients().filter(({ client }) => client.supports("agents.multiAgentAggregates"));
563
+ if (sources) {
564
+ const allow = new Set(sources);
565
+ members = members.filter((m) => allow.has(m.id));
566
+ }
567
+ const responses = await Promise.all(members.map((m) => m.client.agents.getMultiAgentAggregates(...args)));
568
+ const merged = (0, multiplex_merge_1.dedupeBy)([].concat(...responses.map((r) => r.results)), (a) => a.agent_id);
569
+ return { results: merged, count: merged.length };
570
+ },
571
+ };
572
+ }
573
+ // ===== non-core generic facade =====
574
+ makeReadFacade(subclientName) {
575
+ const self = this;
576
+ return new Proxy({}, {
577
+ get(_t, prop) {
578
+ if (typeof prop !== "string")
579
+ return undefined;
580
+ const key = `${subclientName}.${prop}`;
581
+ const spec = NONCORE_METHODS[key];
582
+ if (!spec) {
583
+ // Unknown method on a non-core subclient — treat as unsupported (read).
584
+ return () => Promise.reject(new errors_1.UnsupportedCapabilityError(self.guessNonCoreCap(subclientName), self.clientId));
585
+ }
586
+ if (spec.kind === "write-route") {
587
+ return self.routedWrite(key, spec.cap);
588
+ }
589
+ return async (...rawArgs) => {
590
+ const { args, sources } = self.splitSourcesOption(rawArgs);
591
+ self.assertSomeMemberSupports(spec.cap);
592
+ const agentId = !spec.agentScoped ? undefined : self.extractAgentId(args);
593
+ let members = self.candidates(agentId, spec.cap, sources);
594
+ if (members.length === 0)
595
+ throw new errors_1.UnsupportedCapabilityError(spec.cap, self.clientId);
596
+ let lastErr;
597
+ for (const m of members) {
598
+ try {
599
+ return await m.client[subclientName][prop](...args);
600
+ }
601
+ catch (e) {
602
+ lastErr = e;
603
+ if (!self.isNotFound(e))
604
+ throw e;
605
+ }
606
+ }
607
+ throw lastErr ?? new Error(`${key}: no member returned a result`);
608
+ };
609
+ },
610
+ });
611
+ }
612
+ makeRpcFacade() {
613
+ const self = this;
614
+ const sendFn = self.routedWrite("rpc.send", "rpc.send");
615
+ return {
616
+ setStats() { },
617
+ send(channel, request, options) {
618
+ return sendFn(channel, request, options);
619
+ },
620
+ };
621
+ }
622
+ // --- DataClient: capabilities ---
623
+ getCapabilities() {
624
+ if (!this.capabilitiesCache) {
625
+ const u = new Set();
626
+ for (const { client } of this.enabledClients())
627
+ for (const c of client.getCapabilities())
628
+ u.add(c);
629
+ this.capabilitiesCache = u;
630
+ }
631
+ return this.capabilitiesCache;
632
+ }
633
+ supports(cap) { return this.getCapabilities().has(cap); }
634
+ // --- agent-scope routing ---
635
+ /**
636
+ * Enabled members eligible for a request targeting `agentId`: those whose
637
+ * scope is `{ mode: "all" }` (the cloud — no enumeration), those whose list
638
+ * contains `agentId`, and those whose scope is still `"unknown"` (included
639
+ * optimistically; their scope settles for the next call). If `agentId` is
640
+ * undefined, all enabled members are eligible.
641
+ */
642
+ membersForAgent(agentId) {
643
+ const all = this.enabledClients();
644
+ if (agentId === undefined)
645
+ return all;
646
+ return all.filter(({ client }) => {
647
+ const scope = client.getKnownAgentScope();
648
+ if (scope === "unknown")
649
+ return true;
650
+ if (scope.mode === "all")
651
+ return true;
652
+ return scope.agentIds.includes(agentId);
653
+ });
654
+ }
655
+ /** Members (enabled) that both own `agentId` and advertise `cap`. */
656
+ membersForAgentWithCapability(agentId, cap) {
657
+ return this.membersForAgent(agentId).filter(({ client }) => client.supports(cap));
658
+ }
659
+ getAgentScope() {
660
+ return Promise.all(this.enabledClients().map(({ client }) => client.getAgentScope())).then((scopes) => {
661
+ if (scopes.some((s) => s.mode === "all"))
662
+ return { mode: "all" };
663
+ const ids = new Set();
664
+ for (const s of scopes)
665
+ if (s.mode === "list")
666
+ for (const id of s.agentIds)
667
+ ids.add(id);
668
+ return { mode: "list", agentIds: [...ids] };
669
+ });
670
+ }
671
+ getKnownAgentScope() {
672
+ const known = this.enabledClients().map(({ client }) => client.getKnownAgentScope());
673
+ if (known.some((s) => s !== "unknown" && s.mode === "all"))
674
+ return { mode: "all" };
675
+ if (known.some((s) => s === "unknown"))
676
+ return "unknown";
677
+ const ids = new Set();
678
+ for (const s of known)
679
+ if (s !== "unknown" && s.mode === "list")
680
+ for (const id of s.agentIds)
681
+ ids.add(id);
682
+ return { mode: "list", agentIds: [...ids] };
683
+ }
684
+ isConnected() {
685
+ const gwMembers = this.enabledClients().filter(({ client }) => client.supports("gateway.subscribe"));
686
+ if (gwMembers.length === 0)
687
+ return false;
688
+ return gwMembers.every(({ client }) => client.isConnected());
689
+ }
690
+ getStatus() {
691
+ const allSources = this.getRegisteredSources();
692
+ const members = allSources.map((s) => ({
693
+ sourceId: s.descriptor.id,
694
+ ...(s.descriptor.label ? { label: s.descriptor.label } : {}),
695
+ status: s.enabled && s.client
696
+ ? s.client.getStatus()
697
+ : ({ clientId: s.descriptor.id, connected: false, state: "disconnected", agentScope: "unknown", at: Date.now() }),
698
+ }));
699
+ const enabledMembers = members.filter((m) => this.registry.get(m.sourceId)?.enabled);
700
+ const gwStatuses = enabledMembers
701
+ .filter((m) => this.registry.get(m.sourceId).client?.supports("gateway.subscribe"))
702
+ .map((m) => m.status);
703
+ const connected = this.isConnected();
704
+ let state;
705
+ if (enabledMembers.some((m) => m.status.state === "error"))
706
+ state = "error";
707
+ else if (gwStatuses.length === 0)
708
+ state = "disconnected";
709
+ else if (gwStatuses.every((s) => s.connected))
710
+ state = "connected";
711
+ else if (gwStatuses.some((s) => s.connected))
712
+ state = "degraded";
713
+ else if (gwStatuses.some((s) => s.state === "connecting"))
714
+ state = "connecting";
715
+ else
716
+ state = "disconnected";
717
+ // most-recent member values for scalar summary fields (locked decision #8)
718
+ const newest = enabledMembers.map((m) => m.status).sort((x, y) => y.at - x.at)[0];
719
+ const gatewaySession = this.gateway.getSession();
720
+ return {
721
+ clientId: this.clientId,
722
+ connected,
723
+ state,
724
+ session: gatewaySession ? { id: gatewaySession.session_id ?? "" } : null,
725
+ ...(newest?.lastEvent ? { lastEvent: newest.lastEvent } : {}),
726
+ latencyMs: newest?.latencyMs ?? null,
727
+ ...(newest?.lastError ? { lastError: newest.lastError } : {}),
728
+ agentScope: this.getKnownAgentScope(),
729
+ at: Date.now(),
730
+ members,
731
+ };
732
+ }
733
+ onStatusChange(listener) {
734
+ this.statusListeners.add(listener);
735
+ let active = true;
736
+ return () => { if (!active)
737
+ return; active = false; this.statusListeners.delete(listener); };
738
+ }
739
+ emitStatus() {
740
+ const snap = this.getStatus();
741
+ this.statusListeners.forEach((l) => l(snap));
742
+ }
743
+ }
744
+ exports.MultiplexClient = MultiplexClient;