@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,260 @@
|
|
|
1
|
+
import { parseOpenContract } from "./boot-params";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PERSISTED_CONNECTION_KEY = "red-ui:connection";
|
|
4
|
+
|
|
5
|
+
export interface ConnectionBootstrap {
|
|
6
|
+
target: string | null;
|
|
7
|
+
token?: string;
|
|
8
|
+
route?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ConnectionBootstrapPayload =
|
|
12
|
+
| {
|
|
13
|
+
target?: unknown;
|
|
14
|
+
token?: unknown;
|
|
15
|
+
route?: unknown;
|
|
16
|
+
}
|
|
17
|
+
| null
|
|
18
|
+
| undefined;
|
|
19
|
+
|
|
20
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
21
|
+
type BootstrapSource = () => MaybePromise<ConnectionBootstrapPayload>;
|
|
22
|
+
type TauriInvoke = <T>(
|
|
23
|
+
command: string,
|
|
24
|
+
args?: Record<string, unknown>
|
|
25
|
+
) => Promise<T>;
|
|
26
|
+
|
|
27
|
+
export interface BootstrapStorage {
|
|
28
|
+
getItem(key: string): string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UrlBootstrapLocation {
|
|
32
|
+
pathname: string;
|
|
33
|
+
search: string;
|
|
34
|
+
hash: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UrlBootstrapHistory {
|
|
38
|
+
readonly state: unknown;
|
|
39
|
+
replaceState(data: unknown, unused: string, url?: string | URL | null): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UrlBootstrapReadOptions {
|
|
43
|
+
consume?: boolean;
|
|
44
|
+
location?: UrlBootstrapLocation;
|
|
45
|
+
history?: UrlBootstrapHistory;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ConnectionBootstrapResolverOptions {
|
|
49
|
+
readTauri?: BootstrapSource;
|
|
50
|
+
readInjectedGlobal?: BootstrapSource;
|
|
51
|
+
readUrl?: BootstrapSource;
|
|
52
|
+
readPersisted?: BootstrapSource;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const GLOBAL_BOOTSTRAP_KEYS = [
|
|
56
|
+
"__RED_BOOTSTRAP__",
|
|
57
|
+
"__RED_UI_CONNECTION_BOOTSTRAP__",
|
|
58
|
+
"__RED_UI_BOOTSTRAP__",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function clean(value: unknown): string | undefined {
|
|
62
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeBootstrap(
|
|
66
|
+
payload: ConnectionBootstrapPayload
|
|
67
|
+
): ConnectionBootstrap | null {
|
|
68
|
+
if (!payload || typeof payload !== "object") return null;
|
|
69
|
+
|
|
70
|
+
const target = clean(payload.target);
|
|
71
|
+
const token = clean(payload.token);
|
|
72
|
+
const route = clean(payload.route);
|
|
73
|
+
|
|
74
|
+
if (!target && !token && !route) return null;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
target: target ?? null,
|
|
78
|
+
...(token ? { token } : {}),
|
|
79
|
+
...(route ? { route } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function firstPresent(
|
|
84
|
+
sources: BootstrapSource[]
|
|
85
|
+
): Promise<ConnectionBootstrap | null> {
|
|
86
|
+
for (const source of sources) {
|
|
87
|
+
let resolved: ConnectionBootstrap | null;
|
|
88
|
+
try {
|
|
89
|
+
resolved = normalizeBootstrap(await source());
|
|
90
|
+
} catch {
|
|
91
|
+
resolved = null;
|
|
92
|
+
}
|
|
93
|
+
if (resolved) return resolved;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadTauriInvoke(): Promise<TauriInvoke | null> {
|
|
99
|
+
if (typeof window === "undefined") return null;
|
|
100
|
+
if (!("__TAURI_INTERNALS__" in window || "__TAURI__" in window)) return null;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const mod = await import("@tauri-apps/api/core");
|
|
104
|
+
return mod.invoke as TauriInvoke;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function readTauriIpcBootstrap(
|
|
111
|
+
invoke?: TauriInvoke
|
|
112
|
+
): Promise<ConnectionBootstrapPayload> {
|
|
113
|
+
const call = invoke ?? (await loadTauriInvoke());
|
|
114
|
+
if (!call) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return await call<ConnectionBootstrapPayload>("connection_bootstrap");
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function readInjectedGlobalBootstrap(
|
|
124
|
+
root: unknown = globalThis
|
|
125
|
+
): Promise<ConnectionBootstrapPayload> {
|
|
126
|
+
if (!root || typeof root !== "object") return null;
|
|
127
|
+
const bag = root as Record<string, unknown>;
|
|
128
|
+
|
|
129
|
+
for (const key of GLOBAL_BOOTSTRAP_KEYS) {
|
|
130
|
+
const candidate = bag[key];
|
|
131
|
+
if (!candidate) continue;
|
|
132
|
+
try {
|
|
133
|
+
if (typeof candidate === "function") {
|
|
134
|
+
return await (
|
|
135
|
+
candidate as () => MaybePromise<ConnectionBootstrapPayload>
|
|
136
|
+
)();
|
|
137
|
+
}
|
|
138
|
+
return candidate as ConnectionBootstrapPayload;
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
delete bag[key];
|
|
142
|
+
} catch {
|
|
143
|
+
bag[key] = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function liveUrlBootstrapOptions(): UrlBootstrapReadOptions {
|
|
152
|
+
return {
|
|
153
|
+
consume: true,
|
|
154
|
+
location:
|
|
155
|
+
typeof location === "undefined"
|
|
156
|
+
? undefined
|
|
157
|
+
: {
|
|
158
|
+
pathname: location.pathname,
|
|
159
|
+
search: location.search,
|
|
160
|
+
hash: location.hash,
|
|
161
|
+
},
|
|
162
|
+
history:
|
|
163
|
+
typeof history === "undefined"
|
|
164
|
+
? undefined
|
|
165
|
+
: {
|
|
166
|
+
state: history.state,
|
|
167
|
+
replaceState: (data, unused, url) =>
|
|
168
|
+
history.replaceState(data, unused, url),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function stripTokenFromHistory(options: UrlBootstrapReadOptions): void {
|
|
174
|
+
if (!options.consume || !options.location || !options.history) return;
|
|
175
|
+
const hash = options.location.hash;
|
|
176
|
+
if (!hash) return;
|
|
177
|
+
|
|
178
|
+
const params = new URLSearchParams(
|
|
179
|
+
hash.startsWith("#") ? hash.slice(1) : hash
|
|
180
|
+
);
|
|
181
|
+
if (!params.has("token")) return;
|
|
182
|
+
params.delete("token");
|
|
183
|
+
|
|
184
|
+
const nextHash = params.toString();
|
|
185
|
+
const nextUrl = `${options.location.pathname}${options.location.search}${
|
|
186
|
+
nextHash ? `#${nextHash}` : ""
|
|
187
|
+
}`;
|
|
188
|
+
try {
|
|
189
|
+
options.history.replaceState(options.history.state, "", nextUrl);
|
|
190
|
+
} catch {
|
|
191
|
+
/* best-effort URL cleanup */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function readUrlOpenContractBootstrap(
|
|
196
|
+
search = typeof location === "undefined" ? "" : location.search,
|
|
197
|
+
hash = typeof location === "undefined" ? "" : location.hash,
|
|
198
|
+
options: UrlBootstrapReadOptions = liveUrlBootstrapOptions()
|
|
199
|
+
): ConnectionBootstrapPayload {
|
|
200
|
+
try {
|
|
201
|
+
const contract = parseOpenContract(search, hash);
|
|
202
|
+
if (contract.token) stripTokenFromHistory(options);
|
|
203
|
+
if (!contract.cs && !contract.to && !contract.token) return null;
|
|
204
|
+
return {
|
|
205
|
+
target: contract.cs,
|
|
206
|
+
token: contract.token,
|
|
207
|
+
route: contract.to,
|
|
208
|
+
};
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function defaultStorage(): BootstrapStorage | undefined {
|
|
215
|
+
try {
|
|
216
|
+
return typeof localStorage === "undefined" ? undefined : localStorage;
|
|
217
|
+
} catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function readPersistedLocalBootstrap(
|
|
223
|
+
storage = defaultStorage(),
|
|
224
|
+
key = DEFAULT_PERSISTED_CONNECTION_KEY
|
|
225
|
+
): ConnectionBootstrapPayload {
|
|
226
|
+
if (!storage) return null;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const raw = storage.getItem(key);
|
|
230
|
+
if (!raw) return null;
|
|
231
|
+
|
|
232
|
+
const parsed = JSON.parse(raw) as { url?: unknown };
|
|
233
|
+
const target = clean(parsed?.url);
|
|
234
|
+
return target ? { target } : null;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function resolveConnectionBootstrap(
|
|
241
|
+
options: ConnectionBootstrapResolverOptions = {}
|
|
242
|
+
): Promise<ConnectionBootstrap> {
|
|
243
|
+
const readUrl = options.readUrl ?? (() => readUrlOpenContractBootstrap());
|
|
244
|
+
const resolved = await firstPresent([
|
|
245
|
+
options.readTauri ?? readTauriIpcBootstrap,
|
|
246
|
+
options.readInjectedGlobal ?? readInjectedGlobalBootstrap,
|
|
247
|
+
readUrl,
|
|
248
|
+
options.readPersisted ?? (() => readPersistedLocalBootstrap()),
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
if (resolved && !options.readUrl) {
|
|
252
|
+
try {
|
|
253
|
+
readUrlOpenContractBootstrap();
|
|
254
|
+
} catch {
|
|
255
|
+
/* best-effort live URL cleanup after a higher-priority boot source */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return resolved ?? { target: null };
|
|
260
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Contract tests for ConnectionProvider. Every implementation (LocalUrlProvider,
|
|
2
|
+
// the in-memory mock, future ReddbIoSsoProvider) runs through this suite, so
|
|
3
|
+
// the seam stays honest: behaviour, not just types.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
NotConnectedError,
|
|
8
|
+
UnknownConnectionError,
|
|
9
|
+
type Connection,
|
|
10
|
+
type ConnectionProvider,
|
|
11
|
+
} from './connection-provider'
|
|
12
|
+
|
|
13
|
+
export interface ContractFixture {
|
|
14
|
+
provider: ConnectionProvider
|
|
15
|
+
/** A connection id the provider promises to resolve in connect(). */
|
|
16
|
+
knownId: string
|
|
17
|
+
/** A connection id the provider promises NOT to resolve. */
|
|
18
|
+
unknownId: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runConnectionProviderContract(
|
|
22
|
+
name: string,
|
|
23
|
+
build: () => ContractFixture | Promise<ContractFixture>,
|
|
24
|
+
) {
|
|
25
|
+
describe(`ConnectionProvider contract — ${name}`, () => {
|
|
26
|
+
it('list() returns Connection objects with id/label/url', async () => {
|
|
27
|
+
const { provider } = await build()
|
|
28
|
+
const items = await provider.list()
|
|
29
|
+
expect(Array.isArray(items)).toBe(true)
|
|
30
|
+
for (const c of items) {
|
|
31
|
+
expect(typeof c.id).toBe('string')
|
|
32
|
+
expect(typeof c.label).toBe('string')
|
|
33
|
+
expect(typeof c.url).toBe('string')
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('list() entries have unique ids', async () => {
|
|
38
|
+
const { provider } = await build()
|
|
39
|
+
const items: Connection[] = await provider.list()
|
|
40
|
+
const ids = items.map((c) => c.id)
|
|
41
|
+
expect(new Set(ids).size).toBe(ids.length)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('connect(knownId) resolves to an ActiveConnection', async () => {
|
|
45
|
+
const { provider, knownId } = await build()
|
|
46
|
+
const active = await provider.connect(knownId)
|
|
47
|
+
expect(active.connection.id).toBe(knownId)
|
|
48
|
+
expect(active.client).toBeTruthy()
|
|
49
|
+
expect(typeof active.rtt_ms).toBe('number')
|
|
50
|
+
expect(active.rtt_ms).toBeGreaterThanOrEqual(0)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('connect(unknownId) rejects with UnknownConnectionError', async () => {
|
|
54
|
+
const { provider, unknownId } = await build()
|
|
55
|
+
await expect(provider.connect(unknownId)).rejects.toBeInstanceOf(UnknownConnectionError)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('whoami() before connect rejects with NotConnectedError', async () => {
|
|
59
|
+
const { provider } = await build()
|
|
60
|
+
await expect(provider.whoami()).rejects.toBeInstanceOf(NotConnectedError)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('whoami() after connect resolves to an Identity', async () => {
|
|
64
|
+
const { provider, knownId } = await build()
|
|
65
|
+
await provider.connect(knownId)
|
|
66
|
+
const id = await provider.whoami()
|
|
67
|
+
expect(typeof id.authenticated).toBe('boolean')
|
|
68
|
+
expect(typeof id.username).toBe('string')
|
|
69
|
+
expect(typeof id.role).toBe('string')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ConnectionProvider — deep module: the UI's only seam for "how do I
|
|
2
|
+
// discover and select databases?". Local OSS lives behind LocalUrlProvider;
|
|
3
|
+
// cloud SSO (ReddbIoSsoProvider) will land in a separate slice without
|
|
4
|
+
// touching the consumers.
|
|
5
|
+
|
|
6
|
+
import type { RedClient } from './client'
|
|
7
|
+
import type { Transport } from './types'
|
|
8
|
+
import type { BootParams } from './boot-params'
|
|
9
|
+
|
|
10
|
+
export type ConnectionRole = 'primary' | 'replica' | 'embedded'
|
|
11
|
+
|
|
12
|
+
export interface Connection {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
url: string
|
|
16
|
+
role?: ConnectionRole
|
|
17
|
+
description?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Identity {
|
|
21
|
+
authenticated: boolean
|
|
22
|
+
username: string
|
|
23
|
+
role: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ActiveConnection {
|
|
27
|
+
connection: Connection
|
|
28
|
+
client: RedClient
|
|
29
|
+
rtt_ms: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ConnectionProvider {
|
|
33
|
+
list(): Promise<Connection[]>
|
|
34
|
+
connect(id: string): Promise<ActiveConnection>
|
|
35
|
+
whoami(): Promise<Identity>
|
|
36
|
+
/**
|
|
37
|
+
* The wire transports this provider can actually materialize on its Surface
|
|
38
|
+
* (#34, ADR-0003). The Connect UI offers only these, so the user never picks
|
|
39
|
+
* a transport that fails. Optional: a provider that omits it is treated as
|
|
40
|
+
* browser-reachable (http/https + coerced red://) by the consumer.
|
|
41
|
+
*/
|
|
42
|
+
transports?(): Transport[]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Non-secret connection config seeded by the Surface (#36, ADR-0005) — the
|
|
46
|
+
* endpoint and initial view from the app URL. The Core reads it through this
|
|
47
|
+
* seam (not a special MCP branch) to connect without the Connect flow.
|
|
48
|
+
* Tokens never appear here. Returns null when nothing was seeded.
|
|
49
|
+
*/
|
|
50
|
+
bootParams?(): BootParams | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class UnknownConnectionError extends Error {
|
|
54
|
+
constructor(public readonly id: string) {
|
|
55
|
+
super(`Unknown connection: ${id}`)
|
|
56
|
+
this.name = 'UnknownConnectionError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class NotConnectedError extends Error {
|
|
61
|
+
constructor() {
|
|
62
|
+
super('No active connection — call connect() first')
|
|
63
|
+
this.name = 'NotConnectedError'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class UnreachableConnectionError extends Error {
|
|
68
|
+
constructor(public readonly id: string, public readonly reason: string) {
|
|
69
|
+
super(`Connection ${id} unreachable: ${reason}`)
|
|
70
|
+
this.name = 'UnreachableConnectionError'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./uri";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./client";
|
|
4
|
+
export * from "./connection-provider";
|
|
5
|
+
export * from "./local-url-provider";
|
|
6
|
+
export * from "./injected-client-provider";
|
|
7
|
+
export * from "./server-capabilities";
|
|
8
|
+
export * from "./transports";
|
|
9
|
+
export * from "./boot-params";
|
|
10
|
+
export * from "./connection-bootstrap";
|
|
11
|
+
export * from "./cdc-stream-client";
|
|
12
|
+
export * from "./secure-store";
|
|
13
|
+
export * from "./web-encrypted-store";
|
|
14
|
+
export * from "./tauri-encrypted-store";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// InjectedClientProvider — the embed-Surface implementation of
|
|
2
|
+
// ConnectionProvider. The embedding host owns auth (ADR-0001 / ADR-0005): it
|
|
3
|
+
// constructs and authenticates a RedClient, then hands it to the Core. The
|
|
4
|
+
// Core stays agnostic — it acquires its client through the same
|
|
5
|
+
// ConnectionProvider seam as the PWA/Desktop Surfaces, never knowing whether
|
|
6
|
+
// the connection came from a Connect flow or from its host.
|
|
7
|
+
//
|
|
8
|
+
// Unlike LocalUrlProvider this is a *single-connection* provider: there is no
|
|
9
|
+
// discovery, no history, no Connect flow. `list()` surfaces exactly one
|
|
10
|
+
// connection and `connect()` returns the host-supplied client immediately. A
|
|
11
|
+
// ping only measures RTT for the status bar — a failed probe does NOT block
|
|
12
|
+
// the mount, because the host vouches for the client it injected.
|
|
13
|
+
|
|
14
|
+
import type { RedClient } from './client'
|
|
15
|
+
import {
|
|
16
|
+
NotConnectedError,
|
|
17
|
+
UnknownConnectionError,
|
|
18
|
+
type ActiveConnection,
|
|
19
|
+
type Connection,
|
|
20
|
+
type ConnectionProvider,
|
|
21
|
+
type Identity,
|
|
22
|
+
} from './connection-provider'
|
|
23
|
+
|
|
24
|
+
export interface InjectedClientOptions {
|
|
25
|
+
/** The host-supplied, already-authenticated client. */
|
|
26
|
+
client: RedClient
|
|
27
|
+
/** UI metadata for the single connection this provider exposes. */
|
|
28
|
+
connection?: Partial<Connection>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class InjectedClientProvider implements ConnectionProvider {
|
|
32
|
+
private readonly client: RedClient
|
|
33
|
+
/** The sole connection this provider resolves. */
|
|
34
|
+
readonly connection: Connection
|
|
35
|
+
private connected = false
|
|
36
|
+
|
|
37
|
+
constructor(opts: InjectedClientOptions) {
|
|
38
|
+
this.client = opts.client
|
|
39
|
+
this.connection = {
|
|
40
|
+
id: opts.connection?.id ?? 'host',
|
|
41
|
+
label: opts.connection?.label ?? 'Host connection',
|
|
42
|
+
url: opts.connection?.url ?? '',
|
|
43
|
+
role: opts.connection?.role ?? 'primary',
|
|
44
|
+
description: opts.connection?.description ?? 'Injected by the embedding host.',
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async list(): Promise<Connection[]> {
|
|
49
|
+
return [this.connection]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the single host connection. Any other id throws
|
|
54
|
+
* UnknownConnectionError — there is nothing else to connect to. The probe is
|
|
55
|
+
* best-effort: an unreachable host still yields an ActiveConnection (rtt 0)
|
|
56
|
+
* rather than rejecting, since the host owns the client's lifecycle.
|
|
57
|
+
*/
|
|
58
|
+
async connect(id: string): Promise<ActiveConnection> {
|
|
59
|
+
if (id !== this.connection.id) throw new UnknownConnectionError(id)
|
|
60
|
+
let rtt_ms = 0
|
|
61
|
+
try {
|
|
62
|
+
const ping = await this.client.ping()
|
|
63
|
+
if (ping.ok) rtt_ms = ping.rtt_ms
|
|
64
|
+
} catch {
|
|
65
|
+
/* host vouches for the client — surface rtt 0, don't block the mount */
|
|
66
|
+
}
|
|
67
|
+
this.connected = true
|
|
68
|
+
return { connection: this.connection, client: this.client, rtt_ms }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async whoami(): Promise<Identity> {
|
|
72
|
+
if (!this.connected) throw new NotConnectedError()
|
|
73
|
+
const w = await this.client.whoami()
|
|
74
|
+
return { authenticated: w.authenticated, username: w.username, role: w.role }
|
|
75
|
+
}
|
|
76
|
+
}
|