@reddb-io/ui 0.1.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 (169) hide show
  1. package/LICENSE +17 -0
  2. package/bin/red-ui.js +259 -0
  3. package/build/CNAME +1 -0
  4. package/build/_app/env.js +1 -0
  5. package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
  6. package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
  7. package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
  8. package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
  9. package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  10. package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  11. package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  12. package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  13. package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  14. package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  15. package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  16. package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  17. package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  18. package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  19. package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  20. package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  21. package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
  22. package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
  23. package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
  24. package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
  25. package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
  26. package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
  27. package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
  28. package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
  29. package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
  30. package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
  31. package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
  32. package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
  33. package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
  34. package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
  35. package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
  36. package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
  37. package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
  38. package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
  39. package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
  40. package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
  41. package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
  42. package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
  43. package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
  44. package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
  45. package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
  46. package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
  47. package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
  48. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  49. package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
  50. package/build/_app/version.json +1 -0
  51. package/build/favicon.png +0 -0
  52. package/build/index.html +59 -0
  53. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
  54. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
  55. package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
  56. package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
  57. package/dist/embed/core-QuRYvMt8.js +118 -0
  58. package/dist/embed/core-QuRYvMt8.js.map +1 -0
  59. package/dist/embed/index-B6ZQhCsu.js +577 -0
  60. package/dist/embed/index-B6ZQhCsu.js.map +1 -0
  61. package/dist/embed/index-client-ItCEU64C.js +3087 -0
  62. package/dist/embed/index-client-ItCEU64C.js.map +1 -0
  63. package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
  64. package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
  65. package/dist/embed/index.js +11 -0
  66. package/dist/embed/index.js.map +1 -0
  67. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
  68. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
  69. package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
  70. package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
  71. package/dist/embed/ui.css +1 -0
  72. package/package.json +101 -0
  73. package/src/app.css +238 -0
  74. package/src/lib/ActivityIndicator.svelte +100 -0
  75. package/src/lib/ClusterNode.svelte +119 -0
  76. package/src/lib/ClusterView.svelte +541 -0
  77. package/src/lib/CollectionHistory.svelte +266 -0
  78. package/src/lib/CollectionsView.svelte +14 -0
  79. package/src/lib/CommandPalette.svelte +237 -0
  80. package/src/lib/ConnectDropdown.svelte +222 -0
  81. package/src/lib/EmptyState.svelte +27 -0
  82. package/src/lib/LiveChanges.svelte +195 -0
  83. package/src/lib/MasterPasswordDialog.svelte +168 -0
  84. package/src/lib/PageHeader.svelte +32 -0
  85. package/src/lib/PendingChangesPanel.svelte +153 -0
  86. package/src/lib/QueryEditor.svelte +122 -0
  87. package/src/lib/ResultsPane.svelte +747 -0
  88. package/src/lib/SchemaTree.svelte +193 -0
  89. package/src/lib/SecurityView.svelte +255 -0
  90. package/src/lib/ShortcutOverlay.svelte +104 -0
  91. package/src/lib/Splash.svelte +46 -0
  92. package/src/lib/StatusBar.svelte +163 -0
  93. package/src/lib/SubNav.svelte +62 -0
  94. package/src/lib/Topbar.svelte +119 -0
  95. package/src/lib/Workspace.svelte +247 -0
  96. package/src/lib/activity.svelte.ts +91 -0
  97. package/src/lib/build-info.ts +41 -0
  98. package/src/lib/capability.ts +221 -0
  99. package/src/lib/cn.ts +6 -0
  100. package/src/lib/collection-catalog.ts +116 -0
  101. package/src/lib/collection-pages.ts +33 -0
  102. package/src/lib/connection-history.ts +301 -0
  103. package/src/lib/connections.svelte.ts +493 -0
  104. package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
  105. package/src/lib/embed/index.ts +234 -0
  106. package/src/lib/graph-loader/contract.ts +336 -0
  107. package/src/lib/graph-loader/drilldown.ts +334 -0
  108. package/src/lib/graph-loader/index.ts +13 -0
  109. package/src/lib/index.ts +114 -0
  110. package/src/lib/pending-changes.svelte.ts +149 -0
  111. package/src/lib/query-tabs.svelte.ts +130 -0
  112. package/src/lib/reddb/boot-params.ts +254 -0
  113. package/src/lib/reddb/cdc-stream-client.ts +234 -0
  114. package/src/lib/reddb/client.ts +698 -0
  115. package/src/lib/reddb/connection-bootstrap.ts +260 -0
  116. package/src/lib/reddb/connection-provider.contract.ts +72 -0
  117. package/src/lib/reddb/connection-provider.ts +72 -0
  118. package/src/lib/reddb/index.ts +14 -0
  119. package/src/lib/reddb/injected-client-provider.ts +76 -0
  120. package/src/lib/reddb/local-url-provider.ts +245 -0
  121. package/src/lib/reddb/secure-store.ts +41 -0
  122. package/src/lib/reddb/server-capabilities.ts +71 -0
  123. package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
  124. package/src/lib/reddb/transports.ts +89 -0
  125. package/src/lib/reddb/types.ts +32 -0
  126. package/src/lib/reddb/uri.ts +54 -0
  127. package/src/lib/reddb/web-encrypted-store.ts +201 -0
  128. package/src/lib/renderers/DiffRenderer.svelte +184 -0
  129. package/src/lib/renderers/DocumentRenderer.svelte +134 -0
  130. package/src/lib/renderers/GraphRenderer.svelte +1328 -0
  131. package/src/lib/renderers/HypertableRenderer.svelte +253 -0
  132. package/src/lib/renderers/JsonRenderer.svelte +30 -0
  133. package/src/lib/renderers/KvRenderer.svelte +150 -0
  134. package/src/lib/renderers/QueueRenderer.svelte +286 -0
  135. package/src/lib/renderers/StatsRenderer.svelte +123 -0
  136. package/src/lib/renderers/TableRenderer.svelte +440 -0
  137. package/src/lib/renderers/VectorRenderer.svelte +388 -0
  138. package/src/lib/renderers/diff-render.ts +122 -0
  139. package/src/lib/renderers/diff.ts +9 -0
  140. package/src/lib/renderers/document-render.ts +64 -0
  141. package/src/lib/renderers/document.ts +9 -0
  142. package/src/lib/renderers/graph-render.ts +631 -0
  143. package/src/lib/renderers/graph-sigma.ts +395 -0
  144. package/src/lib/renderers/graph.ts +9 -0
  145. package/src/lib/renderers/hypertable-render.ts +320 -0
  146. package/src/lib/renderers/hypertable.ts +9 -0
  147. package/src/lib/renderers/index.ts +42 -0
  148. package/src/lib/renderers/json-render.ts +89 -0
  149. package/src/lib/renderers/json.ts +10 -0
  150. package/src/lib/renderers/kv-render.ts +193 -0
  151. package/src/lib/renderers/kv.ts +9 -0
  152. package/src/lib/renderers/queue-render.ts +197 -0
  153. package/src/lib/renderers/queue.ts +9 -0
  154. package/src/lib/renderers/registry.ts +68 -0
  155. package/src/lib/renderers/stats-render.ts +73 -0
  156. package/src/lib/renderers/stats.ts +9 -0
  157. package/src/lib/renderers/table-render.ts +156 -0
  158. package/src/lib/renderers/table.ts +11 -0
  159. package/src/lib/renderers/types.ts +33 -0
  160. package/src/lib/renderers/vector-render.ts +154 -0
  161. package/src/lib/renderers/vector.ts +9 -0
  162. package/src/lib/router.svelte.ts +163 -0
  163. package/src/lib/secureStore.svelte.ts +148 -0
  164. package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
  165. package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
  166. package/src/lib/shortcuts.ts +22 -0
  167. package/src/lib/surface.ts +80 -0
  168. package/src/lib/tabs.svelte.ts +115 -0
  169. package/src/lib/theme.svelte.ts +63 -0
@@ -0,0 +1,493 @@
1
+ // Live connections store. Delegates list/connect/whoami to a LocalUrlProvider
2
+ // from #reddb (the deep-module seam) and layers reactive Svelte
3
+ // state on top: which connection is active, its live probe (stats +
4
+ // replication), and a connected flag that gates the Connect screen.
5
+ //
6
+ // History persistence is Surface-aware:
7
+ // - Web stores target labels only. Full URLs/credentials are session memory.
8
+ // - Standalone stores full URLs in the vault and labels in localStorage.
9
+ // - Embedded and managed Web boot targets store nothing.
10
+
11
+ import {
12
+ BROWSER_TRANSPORTS,
13
+ DESKTOP_TRANSPORTS,
14
+ EMPTY_SERVER_CAPABILITIES,
15
+ LocalUrlProvider,
16
+ RedClient,
17
+ DEFAULT_PERSISTED_CONNECTION_KEY,
18
+ isUrlReachable,
19
+ parseBootParams,
20
+ resolveConnectionBootstrap,
21
+ type Connection,
22
+ type ConnectionBootstrap,
23
+ type ConnectionProvider,
24
+ type HandoffTokenConsumer,
25
+ type ReplicationStatus,
26
+ type ServerCapabilities,
27
+ type Stats,
28
+ type Transport,
29
+ } from "#reddb";
30
+ import {
31
+ SurfaceHistoryStore,
32
+ credentialPersistenceForSurface,
33
+ type HistoryEntry,
34
+ } from "./connection-history";
35
+ import { secureStore } from "./secureStore.svelte";
36
+ import { activity } from "./activity.svelte";
37
+
38
+ export type { HistoryEntry } from "./connection-history";
39
+
40
+ export type ConnectionPreset = Connection & { description: string };
41
+
42
+ export const PRESETS: ConnectionPreset[] = [
43
+ {
44
+ id: "embedded",
45
+ label: "Embedded",
46
+ url: "http://localhost:5055",
47
+ role: "embedded",
48
+ description: "Local file via ./scripts/embedded.sh.",
49
+ },
50
+ {
51
+ id: "docker-primary",
52
+ label: "Docker primary",
53
+ url: "http://localhost:15055",
54
+ role: "primary",
55
+ description: "Docker compose primary (./docker/compose.yml).",
56
+ },
57
+ {
58
+ id: "docker-replica",
59
+ label: "Docker replica",
60
+ url: "http://localhost:25055",
61
+ role: "replica",
62
+ description: "Read-only mirror of docker primary.",
63
+ },
64
+ ];
65
+
66
+ interface ProbeResult {
67
+ reachable: boolean;
68
+ rtt_ms?: number;
69
+ stats?: Stats;
70
+ replication?: ReplicationStatus;
71
+ error?: string;
72
+ }
73
+
74
+ const STORAGE_KEY = DEFAULT_PERSISTED_CONNECTION_KEY;
75
+
76
+ function persist(c: ConnectionPreset) {
77
+ if (typeof localStorage === "undefined") return;
78
+ const mode = currentCredentialPersistence();
79
+ if (mode !== "vault") {
80
+ try {
81
+ localStorage.removeItem(STORAGE_KEY);
82
+ } catch {}
83
+ return;
84
+ }
85
+ try {
86
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(c));
87
+ } catch {}
88
+ }
89
+
90
+ const MANAGED_WEB_TOKEN_KEY =
91
+ /(handoff|token|secret|password|passwd|auth|credential|api[-_]?key|bearer)/i;
92
+
93
+ let managedWebHandoffActive = false;
94
+
95
+ function markManagedWebHandoff() {
96
+ if (secureStore.surface === "web") managedWebHandoffActive = true;
97
+ }
98
+
99
+ function hasManagedWebHandoff(): boolean {
100
+ if (managedWebHandoffActive) return true;
101
+ if (secureStore.surface !== "web" || typeof location === "undefined")
102
+ return false;
103
+ for (const raw of [location.search, location.hash]) {
104
+ const params = new URLSearchParams(
105
+ raw.startsWith("#") ? raw.slice(1) : raw
106
+ );
107
+ for (const key of params.keys()) {
108
+ if (MANAGED_WEB_TOKEN_KEY.test(key)) return true;
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+
114
+ function currentCredentialPersistence() {
115
+ return credentialPersistenceForSurface(
116
+ secureStore.surface,
117
+ hasManagedWebHandoff()
118
+ );
119
+ }
120
+
121
+ const historyStore = new SurfaceHistoryStore({
122
+ surface: () => secureStore.surface,
123
+ secureStore: () => secureStore.store,
124
+ managedWebTarget: hasManagedWebHandoff,
125
+ });
126
+
127
+ /**
128
+ * Normalize a user-typed connection string to a URL the browser can speak
129
+ * (always http/https against the HTTP API port 5055).
130
+ *
131
+ * red://host[:port][/path] → http://host:5055
132
+ * reds://host[:port][/path] → https://host:5055
133
+ * http(s)://... → unchanged
134
+ * host[:port] → http://host:port|5055
135
+ */
136
+ export function normalizeUrl(input: string): string {
137
+ let raw = input.trim().replace(/\/$/, "");
138
+ if (!raw) return raw;
139
+
140
+ if (!/:\/\//.test(raw)) raw = "http://" + raw;
141
+
142
+ const m = raw.match(/^([a-z+]+):\/\/(.+)$/i);
143
+ if (!m) return raw;
144
+ const [, scheme, rest] = m;
145
+ const s = scheme.toLowerCase();
146
+
147
+ if (s === "red" || s === "red+tcp" || s === "red+wire") {
148
+ const [host] = rest.split("/")[0].split(":");
149
+ return `http://${host}:5055${rest.includes("/") ? "/" + rest.split("/").slice(1).join("/") : ""}`;
150
+ }
151
+ if (s === "reds" || s === "red+tls") {
152
+ const [host] = rest.split("/")[0].split(":");
153
+ return `https://${host}:5055`;
154
+ }
155
+ return raw;
156
+ }
157
+
158
+ export function makeCustomConnection(input: string): ConnectionPreset {
159
+ const url = normalizeUrl(input);
160
+ let host = url;
161
+ try {
162
+ host = new URL(url).host;
163
+ } catch {}
164
+ return {
165
+ id: url,
166
+ label: host || url,
167
+ url,
168
+ role: "primary",
169
+ description: "User-provided connection string.",
170
+ };
171
+ }
172
+
173
+ // Transport reachability is a Surface property (#34, ADR-0003): a Tauri shell
174
+ // can open native unix/embedded connections the browser can't. Detect the
175
+ // Surface here so the default provider declares the right reachable set.
176
+ function surfaceTransports(): Transport[] {
177
+ const tauri =
178
+ typeof window !== "undefined" &&
179
+ ("__TAURI_INTERNALS__" in window || "__TAURI__" in window);
180
+ return [...(tauri ? DESKTOP_TRANSPORTS : BROWSER_TRANSPORTS)];
181
+ }
182
+
183
+ function isHandoffTokenConsumer(
184
+ candidate: ConnectionProvider
185
+ ): candidate is ConnectionProvider & HandoffTokenConsumer {
186
+ return (
187
+ typeof (candidate as Partial<HandoffTokenConsumer>).useHandoffToken ===
188
+ "function"
189
+ );
190
+ }
191
+
192
+ // Default provider — the PWA/Desktop Surfaces' LocalUrlProvider. History is
193
+ // split: labels in plain localStorage so the dropdown renders pre-unlock;
194
+ // URLs encrypted at rest.
195
+ export const provider: ConnectionProvider & {
196
+ forget(url: string): void;
197
+ } = new LocalUrlProvider({
198
+ presets: PRESETS,
199
+ history: historyStore,
200
+ transports: surfaceTransports(),
201
+ // Boot-params pre-configuration (#36, ADR-0005): a Surface (the MCP App)
202
+ // seeds the endpoint + initial route in the app URL; the Core reads it
203
+ // through the provider. Query tokens are ignored and hash tokens are not
204
+ // surfaced to BootParams, so connection history cannot persist them.
205
+ bootParams: parseBootParams(
206
+ typeof location !== "undefined" ? location.search : "",
207
+ typeof location !== "undefined" ? location.hash : ""
208
+ ),
209
+ });
210
+
211
+ // The Core acquires its client EXCLUSIVELY through this provider (ADR-0001 —
212
+ // ConnectionProvider is the sole connection seam). It defaults to the
213
+ // LocalUrlProvider above, so the PWA/Desktop Connect flow is unchanged. An
214
+ // embedding host swaps in its own provider (e.g. InjectedClientProvider)
215
+ // before mount via setConnectionProvider(), so the host can own auth without
216
+ // the Core ever constructing a client itself.
217
+ let injectedProvider: ConnectionProvider = provider;
218
+
219
+ /** The provider the Core currently connects through. */
220
+ export function getConnectionProvider(): ConnectionProvider {
221
+ return injectedProvider;
222
+ }
223
+
224
+ /**
225
+ * Replace the connection provider. Call before mounting the Core (the embed
226
+ * Surface does this with an InjectedClientProvider). Followed by
227
+ * `connection.adoptInjected()` when the provider is already authenticated and
228
+ * the Connect flow should be skipped entirely.
229
+ */
230
+ export function setConnectionProvider(p: ConnectionProvider): void {
231
+ injectedProvider = p;
232
+ }
233
+
234
+ class ConnectionStore {
235
+ /** Set after a probe succeeds for the first time — used to gate the Connect screen. */
236
+ connected = $state<boolean>(false);
237
+ active = $state<ConnectionPreset>(PRESETS[1]);
238
+ probe = $state<ProbeResult>({ reachable: false });
239
+ history = $state<HistoryEntry[]>([]);
240
+
241
+ /**
242
+ * The live client, supplied by the injected ConnectionProvider's connect().
243
+ * The Core never constructs a RedClient itself (ADR-0001) — this is null
244
+ * until the first successful connect and is replaced on every switch.
245
+ */
246
+ #client = $state<RedClient | null>(null);
247
+
248
+ /**
249
+ * Server capability map for the active connection (#22), resolved at connect.
250
+ * Defaults to the empty (all-unsupported) map so controls stay hidden until
251
+ * a connection proves a feature is present — a newer UI never assumes a
252
+ * capability an older server lacks.
253
+ */
254
+ #capabilities = $state<ServerCapabilities>({ ...EMPTY_SERVER_CAPABILITIES });
255
+
256
+ /**
257
+ * Whether a real connection target has been resolved (#21, acceptance #4):
258
+ * true when one came from Connection Bootstrap or an explicit connect; false
259
+ * when `active` is only the default-preset fallback. Gates
260
+ * automatic network so nothing probes before the user (or a Surface) has
261
+ * actually chosen a target.
262
+ */
263
+ #targetResolved = $state<boolean>(false);
264
+
265
+ constructor() {
266
+ this.history = historyStore.uiEntries();
267
+ historyStore.setOnChange(() => {
268
+ this.history = historyStore.uiEntries();
269
+ });
270
+ }
271
+
272
+ /** See {@link ConnectionStore.#targetResolved}. */
273
+ get targetResolved(): boolean {
274
+ return this.#targetResolved;
275
+ }
276
+
277
+ get client(): RedClient | null {
278
+ return this.#client;
279
+ }
280
+
281
+ /**
282
+ * Read-only state derived from the server's reported `/stats` (#23). True
283
+ * only when the connected server reports `read_only` — never hardcoded. When
284
+ * the server reports no signal (the common case) this stays false and the UI
285
+ * behaves normally.
286
+ */
287
+ get readOnly(): boolean {
288
+ return this.connected && this.probe.stats?.read_only === true;
289
+ }
290
+
291
+ /** Optional human-facing reason for the read-only badge tooltip. */
292
+ get readOnlyReason(): string | undefined {
293
+ return this.probe.stats?.read_only_reason;
294
+ }
295
+
296
+ /**
297
+ * Negotiated server capabilities (#22). Controls gate on these — e.g.
298
+ * `connection.capabilities.vcs` hides version-history affordances on servers
299
+ * that don't support VCS. All-false until a successful connect resolves it.
300
+ */
301
+ get capabilities(): ServerCapabilities {
302
+ return this.#capabilities;
303
+ }
304
+
305
+ /**
306
+ * Wire transports the active provider can reach on this Surface (#34). The
307
+ * Connect UI offers only these. Falls back to the browser-reachable set when
308
+ * a provider doesn't declare its transports.
309
+ */
310
+ get supportedTransports(): Transport[] {
311
+ return getConnectionProvider().transports?.() ?? [...BROWSER_TRANSPORTS];
312
+ }
313
+
314
+ /**
315
+ * Whether a user-typed connection string is reachable from this Surface.
316
+ * The Connect UI uses this to hide/guard unreachable options so a choice
317
+ * never ends in a transport-unsupported error.
318
+ */
319
+ canReach(url: string): boolean {
320
+ return isUrlReachable(url, this.supportedTransports);
321
+ }
322
+
323
+ async forget(url: string) {
324
+ await historyStore.forgetByUrl(url);
325
+ }
326
+
327
+ async forgetById(id: string) {
328
+ await historyStore.forgetById(id);
329
+ }
330
+
331
+ /** Called from +layout once secureStore.store binds. */
332
+ async hydrateUrls() {
333
+ await historyStore.hydrate();
334
+ }
335
+
336
+ /**
337
+ * Resolve the boot connection once, in source priority order:
338
+ * Tauri IPC → host global → Open Contract URL/hash → persisted local store.
339
+ * `target: null` is the well-formed empty case, so cold opens stay on the
340
+ * Connect flow without probing a default preset.
341
+ */
342
+ async bootstrap(
343
+ resolve: () => Promise<ConnectionBootstrap> = resolveConnectionBootstrap
344
+ ): Promise<string | null> {
345
+ const boot = await resolve();
346
+ const route = boot.route ?? null;
347
+ if (!boot.target) return route;
348
+ const preset = makeCustomConnection(boot.target);
349
+ if (boot.token) {
350
+ markManagedWebHandoff();
351
+ const provider = getConnectionProvider();
352
+ if (isHandoffTokenConsumer(provider)) {
353
+ provider.useHandoffToken(preset.url, boot.token);
354
+ }
355
+ }
356
+ await this.tryConnect(preset);
357
+ return route;
358
+ }
359
+
360
+ async switch(preset: ConnectionPreset) {
361
+ this.active = preset;
362
+ persist(preset);
363
+ // Switching connects through the provider — that's where the new client
364
+ // comes from. (Previously refresh() rebuilt a client from the URL; the
365
+ // Core no longer constructs clients itself.)
366
+ await this.tryConnect(preset);
367
+ }
368
+
369
+ async tryConnect(preset: ConnectionPreset): Promise<boolean> {
370
+ // An explicit connect attempt means a target has been chosen — unblocks
371
+ // automatic refresh from here on (#21).
372
+ this.#targetResolved = true;
373
+ try {
374
+ const active = await activity.track(`connect · ${preset.label}`, () =>
375
+ getConnectionProvider().connect(preset.id)
376
+ );
377
+ this.#client = active.client;
378
+ this.active = {
379
+ ...preset,
380
+ ...active.connection,
381
+ description: preset.description,
382
+ };
383
+ persist(this.active);
384
+ const [stats, replication, capabilities] = await Promise.all([
385
+ activity
386
+ .track(`connect · ${preset.label} stats`, () => active.client.stats())
387
+ .catch(() => undefined),
388
+ activity
389
+ .track(`connect · ${preset.label} replication`, () =>
390
+ active.client.replication()
391
+ )
392
+ .catch(() => undefined),
393
+ // Capability negotiation fails safe to the empty map (hide controls).
394
+ activity
395
+ .track(`connect · ${preset.label} capabilities`, () =>
396
+ active.client.capabilities()
397
+ )
398
+ .catch(() => ({ ...EMPTY_SERVER_CAPABILITIES })),
399
+ ]);
400
+ this.probe = {
401
+ reachable: true,
402
+ rtt_ms: active.rtt_ms,
403
+ stats,
404
+ replication,
405
+ };
406
+ this.#capabilities = capabilities;
407
+ this.connected = true;
408
+ return true;
409
+ } catch (e) {
410
+ this.probe = { reachable: false, error: (e as Error).message };
411
+ return false;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Adopt a host-injected provider (ADR-0001 embed Surface). Call after
417
+ * setConnectionProvider(hostProvider): connects to the provider's sole
418
+ * connection so the Core renders connected without ever showing the Connect
419
+ * flow. Returns false (leaving the Core in its disconnected state) when the
420
+ * provider exposes no connection.
421
+ */
422
+ async adoptInjected(): Promise<boolean> {
423
+ const [first] = await getConnectionProvider().list();
424
+ if (!first) return false;
425
+ const preset: ConnectionPreset = {
426
+ ...first,
427
+ description: first.description ?? "Injected by the embedding host.",
428
+ };
429
+ return this.tryConnect(preset);
430
+ }
431
+
432
+ /**
433
+ * Connect from Surface-seeded boot params. Reads the endpoint + initial route
434
+ * the provider was seeded with and, when an endpoint is present, connects to
435
+ * it without the Connect flow. Returns the seeded initial route/view for the
436
+ * caller to navigate to, even if the connection fails. Tokens are never read
437
+ * here; provider boot params intentionally exclude them.
438
+ */
439
+ async connectFromBootParams(): Promise<string | null> {
440
+ const boot = getConnectionProvider().bootParams?.();
441
+ if (!boot) return null;
442
+ const route = boot.to ?? boot.view ?? null;
443
+ if (!boot.endpoint) return route;
444
+ await this.tryConnect(makeCustomConnection(boot.endpoint));
445
+ return route;
446
+ }
447
+
448
+ async refresh() {
449
+ const client = this.client;
450
+ if (!client) {
451
+ // No client yet — acquire one through the provider rather than building
452
+ // it from a URL. A failed connect leaves `connected` false (the Connect
453
+ // flow stays available on the PWA/Desktop Surfaces).
454
+ await this.tryConnect(this.active);
455
+ return;
456
+ }
457
+ try {
458
+ const ping = await activity.track(
459
+ `heartbeat · ${this.active.label}`,
460
+ () => client.ping()
461
+ );
462
+ if (!ping.ok) {
463
+ this.probe = { reachable: false, error: ping.error };
464
+ return;
465
+ }
466
+ const [stats, replication] = await Promise.all([
467
+ activity
468
+ .track(`heartbeat · stats`, () => client.stats())
469
+ .catch(() => undefined),
470
+ activity
471
+ .track(`heartbeat · replication`, () => client.replication())
472
+ .catch(() => undefined),
473
+ ]);
474
+ this.probe = { reachable: true, rtt_ms: ping.rtt_ms, stats, replication };
475
+ this.connected = true;
476
+ } catch (e) {
477
+ this.probe = { reachable: false, error: (e as Error).message };
478
+ }
479
+ }
480
+
481
+ disconnect() {
482
+ this.connected = false;
483
+ this.probe = { reachable: false };
484
+ this.#client = null;
485
+ this.#capabilities = { ...EMPTY_SERVER_CAPABILITIES };
486
+ // Back to "no active target" — auto-network stays off until the user (or a
487
+ // Surface) resolves a target again (#21).
488
+ this.#targetResolved = false;
489
+ managedWebHandoffActive = false;
490
+ }
491
+ }
492
+
493
+ export const connection = new ConnectionStore();
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { BitsConfig } from 'bits-ui'
3
+ import Workspace from '../Workspace.svelte'
4
+ import type { ConnectionProvider } from '../reddb'
5
+ import type { Theme } from '../theme.svelte'
6
+
7
+ let {
8
+ connectionProvider,
9
+ portalTarget,
10
+ shadowHost,
11
+ initialRoute,
12
+ initialTheme = 'dark',
13
+ }: {
14
+ connectionProvider: ConnectionProvider
15
+ portalTarget: HTMLElement
16
+ shadowHost: HTMLElement
17
+ initialRoute?: string
18
+ initialTheme?: Theme
19
+ } = $props()
20
+ </script>
21
+
22
+ <BitsConfig defaultPortalTo={portalTarget}>
23
+ <Workspace
24
+ {connectionProvider}
25
+ {initialRoute}
26
+ showConnect={false}
27
+ themeTarget={shadowHost}
28
+ persistTheme={false}
29
+ {initialTheme}
30
+ />
31
+ </BitsConfig>