@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.
- package/LICENSE +17 -0
- package/bin/red-ui.js +259 -0
- package/build/CNAME +1 -0
- package/build/_app/env.js +1 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
- package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
- package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
- package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
- package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
- package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
- package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
- package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
- package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
- package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
- package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
- package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
- package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
- package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
- package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
- package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
- package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
- package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
- package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
- package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
- package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
- package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
- package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
- package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
- package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
- package/build/_app/version.json +1 -0
- package/build/favicon.png +0 -0
- package/build/index.html +59 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
- package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
- package/dist/embed/core-QuRYvMt8.js +118 -0
- package/dist/embed/core-QuRYvMt8.js.map +1 -0
- package/dist/embed/index-B6ZQhCsu.js +577 -0
- package/dist/embed/index-B6ZQhCsu.js.map +1 -0
- package/dist/embed/index-client-ItCEU64C.js +3087 -0
- package/dist/embed/index-client-ItCEU64C.js.map +1 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
- package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
- package/dist/embed/index.js +11 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
- package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
- package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
- package/dist/embed/ui.css +1 -0
- package/package.json +101 -0
- package/src/app.css +238 -0
- package/src/lib/ActivityIndicator.svelte +100 -0
- package/src/lib/ClusterNode.svelte +119 -0
- package/src/lib/ClusterView.svelte +541 -0
- package/src/lib/CollectionHistory.svelte +266 -0
- package/src/lib/CollectionsView.svelte +14 -0
- package/src/lib/CommandPalette.svelte +237 -0
- package/src/lib/ConnectDropdown.svelte +222 -0
- package/src/lib/EmptyState.svelte +27 -0
- package/src/lib/LiveChanges.svelte +195 -0
- package/src/lib/MasterPasswordDialog.svelte +168 -0
- package/src/lib/PageHeader.svelte +32 -0
- package/src/lib/PendingChangesPanel.svelte +153 -0
- package/src/lib/QueryEditor.svelte +122 -0
- package/src/lib/ResultsPane.svelte +747 -0
- package/src/lib/SchemaTree.svelte +193 -0
- package/src/lib/SecurityView.svelte +255 -0
- package/src/lib/ShortcutOverlay.svelte +104 -0
- package/src/lib/Splash.svelte +46 -0
- package/src/lib/StatusBar.svelte +163 -0
- package/src/lib/SubNav.svelte +62 -0
- package/src/lib/Topbar.svelte +119 -0
- package/src/lib/Workspace.svelte +247 -0
- package/src/lib/activity.svelte.ts +91 -0
- package/src/lib/build-info.ts +41 -0
- package/src/lib/capability.ts +221 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/collection-catalog.ts +116 -0
- package/src/lib/collection-pages.ts +33 -0
- package/src/lib/connection-history.ts +301 -0
- package/src/lib/connections.svelte.ts +493 -0
- package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
- package/src/lib/embed/index.ts +234 -0
- package/src/lib/graph-loader/contract.ts +336 -0
- package/src/lib/graph-loader/drilldown.ts +334 -0
- package/src/lib/graph-loader/index.ts +13 -0
- package/src/lib/index.ts +114 -0
- package/src/lib/pending-changes.svelte.ts +149 -0
- package/src/lib/query-tabs.svelte.ts +130 -0
- package/src/lib/reddb/boot-params.ts +254 -0
- package/src/lib/reddb/cdc-stream-client.ts +234 -0
- package/src/lib/reddb/client.ts +698 -0
- package/src/lib/reddb/connection-bootstrap.ts +260 -0
- package/src/lib/reddb/connection-provider.contract.ts +72 -0
- package/src/lib/reddb/connection-provider.ts +72 -0
- package/src/lib/reddb/index.ts +14 -0
- package/src/lib/reddb/injected-client-provider.ts +76 -0
- package/src/lib/reddb/local-url-provider.ts +245 -0
- package/src/lib/reddb/secure-store.ts +41 -0
- package/src/lib/reddb/server-capabilities.ts +71 -0
- package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
- package/src/lib/reddb/transports.ts +89 -0
- package/src/lib/reddb/types.ts +32 -0
- package/src/lib/reddb/uri.ts +54 -0
- package/src/lib/reddb/web-encrypted-store.ts +201 -0
- package/src/lib/renderers/DiffRenderer.svelte +184 -0
- package/src/lib/renderers/DocumentRenderer.svelte +134 -0
- package/src/lib/renderers/GraphRenderer.svelte +1328 -0
- package/src/lib/renderers/HypertableRenderer.svelte +253 -0
- package/src/lib/renderers/JsonRenderer.svelte +30 -0
- package/src/lib/renderers/KvRenderer.svelte +150 -0
- package/src/lib/renderers/QueueRenderer.svelte +286 -0
- package/src/lib/renderers/StatsRenderer.svelte +123 -0
- package/src/lib/renderers/TableRenderer.svelte +440 -0
- package/src/lib/renderers/VectorRenderer.svelte +388 -0
- package/src/lib/renderers/diff-render.ts +122 -0
- package/src/lib/renderers/diff.ts +9 -0
- package/src/lib/renderers/document-render.ts +64 -0
- package/src/lib/renderers/document.ts +9 -0
- package/src/lib/renderers/graph-render.ts +631 -0
- package/src/lib/renderers/graph-sigma.ts +395 -0
- package/src/lib/renderers/graph.ts +9 -0
- package/src/lib/renderers/hypertable-render.ts +320 -0
- package/src/lib/renderers/hypertable.ts +9 -0
- package/src/lib/renderers/index.ts +42 -0
- package/src/lib/renderers/json-render.ts +89 -0
- package/src/lib/renderers/json.ts +10 -0
- package/src/lib/renderers/kv-render.ts +193 -0
- package/src/lib/renderers/kv.ts +9 -0
- package/src/lib/renderers/queue-render.ts +197 -0
- package/src/lib/renderers/queue.ts +9 -0
- package/src/lib/renderers/registry.ts +68 -0
- package/src/lib/renderers/stats-render.ts +73 -0
- package/src/lib/renderers/stats.ts +9 -0
- package/src/lib/renderers/table-render.ts +156 -0
- package/src/lib/renderers/table.ts +11 -0
- package/src/lib/renderers/types.ts +33 -0
- package/src/lib/renderers/vector-render.ts +154 -0
- package/src/lib/renderers/vector.ts +9 -0
- package/src/lib/router.svelte.ts +163 -0
- package/src/lib/secureStore.svelte.ts +148 -0
- package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
- package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
- package/src/lib/shortcuts.ts +22 -0
- package/src/lib/surface.ts +80 -0
- package/src/lib/tabs.svelte.ts +115 -0
- 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>
|