@powerhousedao/connect-core 6.2.3-dev.82 → 6.2.3-dev.84
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/dist/index-ZFFeYX4A.d.ts +365 -0
- package/dist/index-ZFFeYX4A.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{multi-reactor-CVwr_LdS.js → multi-reactor-Dvwv69u1.js} +45 -28
- package/dist/multi-reactor-Dvwv69u1.js.map +1 -0
- package/dist/runtime-LvAkxbdf.js +609 -0
- package/dist/runtime-LvAkxbdf.js.map +1 -0
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +2 -2
- package/package.json +8 -8
- package/src/runtime/app-reactor-client.ts +71 -29
- package/src/runtime/drive-placement.ts +210 -0
- package/src/runtime/drive-placements.ts +361 -0
- package/src/runtime/index.ts +30 -0
- package/src/runtime/multi-reactor.ts +74 -33
- package/dist/index-CCs6HTQV.d.ts +0 -187
- package/dist/index-CCs6HTQV.d.ts.map +0 -1
- package/dist/multi-reactor-CVwr_LdS.js.map +0 -1
- package/dist/runtime-Cb3HQhKo.js +0 -239
- package/dist/runtime-Cb3HQhKo.js.map +0 -1
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { getRuntimeConfig } from "./runtime-config.js";
|
|
2
|
+
import { n as resolveRuntimeFlag, t as isRuntimeFlagEnabled } from "./runtime-flag-DvxW_SD4.js";
|
|
3
|
+
import { n as setDriveRelister } from "./backend-auth-gate-DNKg_v4Y.js";
|
|
4
|
+
import { bindPHStoreToWindow, createPHStore, getActivePHStore, getDrives, getFullReactorClient, getPHStore, recordBackendAuthError, setBackendAuth, setDrives, setFullReactorClient } from "@powerhousedao/reactor-browser";
|
|
5
|
+
import { logger } from "document-model";
|
|
6
|
+
import { switchboardOfDrive } from "@powerhousedao/shared/host-tokens";
|
|
7
|
+
import { DriveCollectionId } from "@powerhousedao/reactor";
|
|
8
|
+
import { driveDocumentType } from "@powerhousedao/shared/document-drive";
|
|
9
|
+
//#region src/runtime/multi-reactor-flag.ts
|
|
10
|
+
function isMultiReactorEnabled() {
|
|
11
|
+
return getRuntimeConfig().connect.instance?.multiReactor ?? false;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/runtime/reactor-worker-flag.ts
|
|
15
|
+
const REACTOR_WORKER_QUERY_KEY = "reactorWorker";
|
|
16
|
+
const REACTOR_WORKER_STORAGE_KEY = "ph:reactorWorker";
|
|
17
|
+
function resolveReactorWorkerEnabled(input) {
|
|
18
|
+
return resolveRuntimeFlag(input);
|
|
19
|
+
}
|
|
20
|
+
function isReactorWorkerEnabled() {
|
|
21
|
+
return isRuntimeFlagEnabled({
|
|
22
|
+
queryKey: REACTOR_WORKER_QUERY_KEY,
|
|
23
|
+
storageKey: REACTOR_WORKER_STORAGE_KEY,
|
|
24
|
+
readConfigFlag: () => getRuntimeConfig().connect.instance?.reactorWorker ?? false
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/runtime/reactor-client-mode.ts
|
|
29
|
+
const MODES = /* @__PURE__ */ new Set([
|
|
30
|
+
"in-process",
|
|
31
|
+
"worker",
|
|
32
|
+
"remote",
|
|
33
|
+
"router"
|
|
34
|
+
]);
|
|
35
|
+
const REACTOR_CLIENT_QUERY_KEY = "reactorClient";
|
|
36
|
+
const REACTOR_CLIENT_STORAGE_KEY = "ph:reactorClient";
|
|
37
|
+
const asMode = (value) => value && MODES.has(value) ? value : void 0;
|
|
38
|
+
function fromTab() {
|
|
39
|
+
if (typeof window === "undefined") return void 0;
|
|
40
|
+
let query;
|
|
41
|
+
try {
|
|
42
|
+
query = asMode(new URLSearchParams(window.location.search).get(REACTOR_CLIENT_QUERY_KEY));
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
if (query) {
|
|
48
|
+
window.localStorage.setItem(REACTOR_CLIENT_STORAGE_KEY, query);
|
|
49
|
+
return query;
|
|
50
|
+
}
|
|
51
|
+
return asMode(window.localStorage.getItem(REACTOR_CLIENT_STORAGE_KEY));
|
|
52
|
+
} catch {
|
|
53
|
+
return query;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveReactorClientMode() {
|
|
57
|
+
const override = fromTab();
|
|
58
|
+
if (override) return override;
|
|
59
|
+
const configured = getRuntimeConfig().connect.instance?.reactorClient;
|
|
60
|
+
if (configured && MODES.has(configured)) return configured;
|
|
61
|
+
if (isMultiReactorEnabled()) return "router";
|
|
62
|
+
if (isReactorWorkerEnabled()) return "worker";
|
|
63
|
+
return "in-process";
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/runtime/app-reactor-client.ts
|
|
67
|
+
const REMOTE_BACKEND_NAME = "switchboard-remote";
|
|
68
|
+
const loadMultiReactor = () => import("./multi-reactor-Dvwv69u1.js");
|
|
69
|
+
function planReactorClient(mode = resolveReactorClientMode()) {
|
|
70
|
+
const needsLocalReactor = mode !== "remote";
|
|
71
|
+
return {
|
|
72
|
+
mode,
|
|
73
|
+
multiReactor: mode === "router",
|
|
74
|
+
needsLocalReactor,
|
|
75
|
+
localAsWorker: needsLocalReactor && (mode === "worker" || mode === "router" && isReactorWorkerEnabled())
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Builds the local reactor the plan needs, then selects the app's client. */
|
|
79
|
+
async function createAppReactorClient(options) {
|
|
80
|
+
const { localReactor, ...params } = options;
|
|
81
|
+
const plan = options.plan ?? planReactorClient();
|
|
82
|
+
let module;
|
|
83
|
+
if (plan.needsLocalReactor) {
|
|
84
|
+
if (!localReactor) throw new Error(`reactorClient mode "${plan.mode}" needs a local reactor; pass localReactor`);
|
|
85
|
+
module = await localReactor(plan);
|
|
86
|
+
}
|
|
87
|
+
const { client, router } = await selectClient({
|
|
88
|
+
...params,
|
|
89
|
+
mode: plan.mode,
|
|
90
|
+
module
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
plan,
|
|
94
|
+
module,
|
|
95
|
+
client,
|
|
96
|
+
router
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Pick the client for the resolved mode:
|
|
101
|
+
* - `in-process` / `worker`: the local client, router module never loaded.
|
|
102
|
+
* - `router`: the local reactor plus remote backends.
|
|
103
|
+
* - `remote`: a remote-only router — a full IReactorClient that proxies to
|
|
104
|
+
* the Switchboard over GraphQL with NO local reactor (no PGlite).
|
|
105
|
+
*/
|
|
106
|
+
async function selectAppReactorClient(params) {
|
|
107
|
+
return (await selectClient(params)).client;
|
|
108
|
+
}
|
|
109
|
+
/** The app's client, and the router when it is one. */
|
|
110
|
+
async function selectClient(params) {
|
|
111
|
+
if (params.mode === "remote") {
|
|
112
|
+
setFullReactorClient(void 0);
|
|
113
|
+
const remote = await buildRemoteOnly(params);
|
|
114
|
+
setFullReactorClient(remote);
|
|
115
|
+
return {
|
|
116
|
+
client: remote,
|
|
117
|
+
router: remote
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (params.mode !== "router") {
|
|
121
|
+
if (getPHStore()?.fullReactorClient) setFullReactorClient(void 0);
|
|
122
|
+
return { client: requireModule(params).client };
|
|
123
|
+
}
|
|
124
|
+
setFullReactorClient(void 0);
|
|
125
|
+
const routed = await buildRouter(params);
|
|
126
|
+
setFullReactorClient(routed);
|
|
127
|
+
return routed ? {
|
|
128
|
+
client: routed,
|
|
129
|
+
router: routed
|
|
130
|
+
} : { client: requireModule(params).client };
|
|
131
|
+
}
|
|
132
|
+
function requireModule(params) {
|
|
133
|
+
if (!params.module) throw new Error(`reactorClient mode "${params.mode}" needs a local reactor, but none was built`);
|
|
134
|
+
return params.module;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A Switchboard that cannot answer must not fail boot: list the tab's drives.
|
|
138
|
+
*
|
|
139
|
+
* A Switchboard that REFUSES the caller (401/403) must not fail boot either:
|
|
140
|
+
* the refusal is recorded (`backendAuth`) so the root view shows the sign-in
|
|
141
|
+
* gate, and the drive list is empty until the user is someone the backend
|
|
142
|
+
* accepts. A remote-only client has no local drives to fall back on.
|
|
143
|
+
*/
|
|
144
|
+
async function getAppDrives(client, local) {
|
|
145
|
+
if (client === local) try {
|
|
146
|
+
const drives = await getDrives(local);
|
|
147
|
+
setBackendAuth(void 0);
|
|
148
|
+
return drives;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (recordBackendAuthError(error, "switchboard")) {
|
|
151
|
+
logger.warn("The Switchboard refused this caller; showing the sign-in gate: @error", error);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const drives = await getDrives(client);
|
|
158
|
+
setBackendAuth(void 0);
|
|
159
|
+
return drives;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (recordBackendAuthError(error, "switchboard")) logger.warn("A remote backend refused this caller; listing the local reactor's drives: @error", error);
|
|
162
|
+
else logger.warn("Could not list drives through the router; listing the local reactor's: @error", error);
|
|
163
|
+
return getDrives(local);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The boot refresh of the drives store. It lists through `getAppDrives`
|
|
168
|
+
* whichever client is in use, so a Switchboard that refuses the caller is
|
|
169
|
+
* recorded for the sign-in gate here too: the remote-only client is its own
|
|
170
|
+
* `local`, and a bare `refreshReactorDataClient` would let the 401 escape
|
|
171
|
+
* and fail the boot before the gate can render.
|
|
172
|
+
*/
|
|
173
|
+
async function refreshAppDrives(client, local) {
|
|
174
|
+
setDrives(await getAppDrives(client, local));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Lists the drives again as the caller the tab now is, through the clients
|
|
178
|
+
* boot left in the store: after a sign-in landing or a sign-out that followed
|
|
179
|
+
* a backend's refusal. Nothing to do before boot has set a client.
|
|
180
|
+
*/
|
|
181
|
+
async function relistAppDrives() {
|
|
182
|
+
const client = getFullReactorClient();
|
|
183
|
+
if (!client) return;
|
|
184
|
+
await refreshAppDrives(client, getPHStore()?.reactorClientModule?.client ?? client);
|
|
185
|
+
}
|
|
186
|
+
setDriveRelister(relistAppDrives);
|
|
187
|
+
/** The switchboards to route to: the host's remote backends, else the default drive's. */
|
|
188
|
+
function switchboardRemotes(params) {
|
|
189
|
+
const listed = params.remoteBackends?.length ? params.remoteBackends : params.remoteDriveUrl ? [{
|
|
190
|
+
name: REMOTE_BACKEND_NAME,
|
|
191
|
+
url: params.remoteDriveUrl
|
|
192
|
+
}] : [];
|
|
193
|
+
const remotes = [];
|
|
194
|
+
const unparsed = [];
|
|
195
|
+
for (const { name, url } of listed) {
|
|
196
|
+
const graphqlUrl = switchboardOfDrive(url)?.graphql;
|
|
197
|
+
if (graphqlUrl) remotes.push({
|
|
198
|
+
name,
|
|
199
|
+
graphqlUrl
|
|
200
|
+
});
|
|
201
|
+
else unparsed.push(url);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
remotes,
|
|
205
|
+
unparsed
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function buildRouter(params) {
|
|
209
|
+
const { remotes, unparsed } = switchboardRemotes(params);
|
|
210
|
+
for (const url of unparsed) logger.warn("multiReactor is on but the remote drive URL @url does not parse; skipping it", url);
|
|
211
|
+
if (remotes.length === 0) {
|
|
212
|
+
logger.warn("multiReactor is on but no remote drive URL is configured; using the local reactor only");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let multiReactor;
|
|
216
|
+
try {
|
|
217
|
+
multiReactor = await (params.loadRouter ?? loadMultiReactor)();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.error("Could not load the multi-reactor router: @error", error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
logger.info("Multi-reactor routing to @urls", remotes.map((remote) => remote.graphqlUrl).join(", "));
|
|
223
|
+
return multiReactor.buildMultiReactorClient({
|
|
224
|
+
module: requireModule(params),
|
|
225
|
+
remotes,
|
|
226
|
+
collections: params.collections,
|
|
227
|
+
signer: params.signer,
|
|
228
|
+
documentModelModules: params.documentModelModules
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The `remote` mode: a remote-only router — a full IReactorClient that proxies
|
|
233
|
+
* to the Switchboard over GraphQL with NO local backend, so no local reactor
|
|
234
|
+
* and no PGlite are ever built. A missing/unparseable remote URL is a hard
|
|
235
|
+
* config error here (unlike `router`, there is no local reactor to fall back
|
|
236
|
+
* to).
|
|
237
|
+
*/
|
|
238
|
+
async function buildRemoteOnly(params) {
|
|
239
|
+
const { remotes, unparsed } = switchboardRemotes(params);
|
|
240
|
+
if (unparsed.length > 0) throw new Error(`reactorClient "remote": the remote drive URL "${unparsed[0]}" does not resolve to a Switchboard GraphQL endpoint`);
|
|
241
|
+
if (remotes.length === 0) throw new Error("reactorClient \"remote\" needs a remote drive URL — declare a remote backend / default drive the client can proxy to");
|
|
242
|
+
const multiReactor = await (params.loadRouter ?? loadMultiReactor)();
|
|
243
|
+
logger.info("Remote reactor client → @urls", remotes.map((remote) => remote.graphqlUrl).join(", "));
|
|
244
|
+
return multiReactor.buildMultiReactorClient({
|
|
245
|
+
module: void 0,
|
|
246
|
+
remotes,
|
|
247
|
+
collections: params.collections,
|
|
248
|
+
signer: params.signer,
|
|
249
|
+
documentModelModules: params.documentModelModules
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/runtime/drive-placement.ts
|
|
254
|
+
/** The router backend of the reactor in this browser. */
|
|
255
|
+
const LOCAL_BACKEND_NAME = "connect-local";
|
|
256
|
+
function keepsLocalCopy(drive) {
|
|
257
|
+
return !("url" in drive) || drive.sync !== false;
|
|
258
|
+
}
|
|
259
|
+
/** Remote default drives with a backend, then collections. */
|
|
260
|
+
function switchboardDrives(config) {
|
|
261
|
+
const drives = [];
|
|
262
|
+
for (const drive of config.drives?.defaultDrives ?? []) {
|
|
263
|
+
if (!("url" in drive) || !drive.backend) continue;
|
|
264
|
+
const slug = switchboardOfDrive(drive.url)?.slug;
|
|
265
|
+
if (slug === void 0) continue;
|
|
266
|
+
drives.push({
|
|
267
|
+
slug,
|
|
268
|
+
url: drive.url,
|
|
269
|
+
remote: drive.backend,
|
|
270
|
+
configured: drive.sync === false ? drive.backend : LOCAL_BACKEND_NAME,
|
|
271
|
+
collection: false
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const remotes = config.instance?.remoteBackends ?? [];
|
|
275
|
+
for (const [slug, entry] of Object.entries(config.instance?.collections ?? {})) {
|
|
276
|
+
const remote = remotes.find((r) => r.name === entry.backend);
|
|
277
|
+
const base = remote && switchboardOfDrive(remote.url)?.base;
|
|
278
|
+
if (!base || drives.some((drive) => drive.slug === slug)) continue;
|
|
279
|
+
drives.push({
|
|
280
|
+
slug,
|
|
281
|
+
url: `${base}/d/${encodeURIComponent(slug)}`,
|
|
282
|
+
remote: entry.backend,
|
|
283
|
+
configured: entry.sync === false ? entry.backend : LOCAL_BACKEND_NAME,
|
|
284
|
+
collection: true
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return drives;
|
|
288
|
+
}
|
|
289
|
+
/** Stale: no such drive, a backend it can't open on, back to config, or a changed config value. */
|
|
290
|
+
function applyPlacementEntries(drives, entries) {
|
|
291
|
+
const applied = [];
|
|
292
|
+
const stale = [];
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const drive = drives.find((d) => d.slug === entry.slug);
|
|
295
|
+
(drive !== void 0 && entry.configWas === drive.configured && entry.backend !== drive.configured && (entry.backend === drive.remote || entry.backend === "connect-local") ? applied : stale).push(entry);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
applied,
|
|
299
|
+
stale
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** A served slug names a declared one as is, or hashed (`<slug>-<8 hex>`). */
|
|
303
|
+
function servesSlug(declared, served) {
|
|
304
|
+
return served === declared || served.startsWith(`${declared}-`) && /^[0-9a-f]{8}$/.test(served.slice(declared.length + 1));
|
|
305
|
+
}
|
|
306
|
+
function withinDeadline(work, timeoutMs) {
|
|
307
|
+
let timer;
|
|
308
|
+
const deadline = new Promise((_, reject) => {
|
|
309
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`no answer in ${timeoutMs} ms`)), timeoutMs);
|
|
310
|
+
});
|
|
311
|
+
return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
|
|
312
|
+
}
|
|
313
|
+
/** An unreachable drive keeps only the slug in its address. */
|
|
314
|
+
async function resolveSwitchboardDrives(drives, fetchInfo, { timeoutMs = 5e3, findServed } = {}) {
|
|
315
|
+
return Promise.all(drives.map(async (drive) => {
|
|
316
|
+
try {
|
|
317
|
+
const { id, slug } = await withinDeadline(fetchInfo(drive.url), timeoutMs);
|
|
318
|
+
return {
|
|
319
|
+
...drive,
|
|
320
|
+
id,
|
|
321
|
+
servedSlug: slug
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (drive.collection && findServed) try {
|
|
325
|
+
const served = await withinDeadline(findServed(drive), timeoutMs);
|
|
326
|
+
if (served) return {
|
|
327
|
+
...drive,
|
|
328
|
+
id: served.id,
|
|
329
|
+
servedSlug: served.slug
|
|
330
|
+
};
|
|
331
|
+
} catch (findError) {
|
|
332
|
+
logger.warn("Could not find drive @slug on @remote: @error", drive.slug, drive.remote, findError);
|
|
333
|
+
}
|
|
334
|
+
logger.warn("Could not read drive @url; routing it by its address only: @error", drive.url, error);
|
|
335
|
+
const slug = switchboardOfDrive(drive.url)?.slug;
|
|
336
|
+
return slug === void 0 ? drive : {
|
|
337
|
+
...drive,
|
|
338
|
+
servedSlug: slug
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
/** The address the drive is served at. */
|
|
344
|
+
function servedUrl(drive) {
|
|
345
|
+
const at = switchboardOfDrive(drive.url);
|
|
346
|
+
return at && drive.servedSlug && drive.servedSlug !== at.slug ? `${at.base}/d/${encodeURIComponent(drive.servedSlug)}` : drive.url;
|
|
347
|
+
}
|
|
348
|
+
/** The keys a placement is installed under: the id and the served slug. */
|
|
349
|
+
function placementKeys(drive) {
|
|
350
|
+
return [drive.id, drive.servedSlug].filter((key) => key !== void 0 && key !== "");
|
|
351
|
+
}
|
|
352
|
+
/** Drops the sync remote of every given drive; its local data is left unread. */
|
|
353
|
+
async function removeLocalCopies(sync, resolved) {
|
|
354
|
+
for (const drive of resolved) {
|
|
355
|
+
if (!drive.id) continue;
|
|
356
|
+
const collectionId = DriveCollectionId.forDrive(drive.id);
|
|
357
|
+
const remotes = sync.list().filter((remote) => remote.meta.collectionId.equals(collectionId));
|
|
358
|
+
for (const remote of remotes) await sync.remove(remote.meta.name);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/runtime/drive-placements.ts
|
|
363
|
+
/** A collection by its declared slug among its switchboard's drives, e.g. one served hashed. */
|
|
364
|
+
async function findServedDrive(router, drive) {
|
|
365
|
+
const backend = router.backends.find((b) => b.name === drive.remote);
|
|
366
|
+
if (!backend) return void 0;
|
|
367
|
+
const { results } = await backend.api.find({ type: driveDocumentType });
|
|
368
|
+
const served = results.find((doc) => doc.header.slug && servesSlug(drive.slug, doc.header.slug));
|
|
369
|
+
return served && {
|
|
370
|
+
id: served.header.id,
|
|
371
|
+
slug: served.header.slug
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Where a drive opens given its stored move, if any: the move, else the host config. */
|
|
375
|
+
function effectiveBackend(drive, entry) {
|
|
376
|
+
return entry?.backend ?? drive.configured;
|
|
377
|
+
}
|
|
378
|
+
/** A drive opening on `backend` reads its copy here, which must arrive first. */
|
|
379
|
+
function needsCopy(backend) {
|
|
380
|
+
return backend === LOCAL_BACKEND_NAME;
|
|
381
|
+
}
|
|
382
|
+
/** A copy has caught up once every scope is at least at the source's revision. */
|
|
383
|
+
function caughtUp(local, source) {
|
|
384
|
+
return Object.entries(source).every(([scope, revision]) => (local[scope] ?? -1) >= revision);
|
|
385
|
+
}
|
|
386
|
+
/** Config placement plus this browser's moves: store, then sync remote, then router. */
|
|
387
|
+
var DrivePlacements = class DrivePlacements {
|
|
388
|
+
drives;
|
|
389
|
+
store;
|
|
390
|
+
sync;
|
|
391
|
+
applied = /* @__PURE__ */ new Map();
|
|
392
|
+
resolved = [];
|
|
393
|
+
router;
|
|
394
|
+
busy = /* @__PURE__ */ new Map();
|
|
395
|
+
attached = false;
|
|
396
|
+
listeners = /* @__PURE__ */ new Set();
|
|
397
|
+
snapshot = /* @__PURE__ */ new Map();
|
|
398
|
+
constructor(drives, applied, store, sync) {
|
|
399
|
+
this.drives = drives;
|
|
400
|
+
this.store = store;
|
|
401
|
+
this.sync = sync;
|
|
402
|
+
for (const entry of applied) this.applied.set(entry.slug, entry);
|
|
403
|
+
}
|
|
404
|
+
/** Reads the store and drops the entries the config has overtaken (O2). */
|
|
405
|
+
static async load(drives, store, sync) {
|
|
406
|
+
let entries = [];
|
|
407
|
+
try {
|
|
408
|
+
entries = await store.list();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger.warn("Could not read this browser's drive moves: @error", error);
|
|
411
|
+
}
|
|
412
|
+
const { applied, stale } = applyPlacementEntries(drives, entries);
|
|
413
|
+
for (const entry of stale) store.delete(entry.slug).catch((error) => {
|
|
414
|
+
logger.warn("Could not drop drive move @slug: @error", entry.slug, error);
|
|
415
|
+
});
|
|
416
|
+
return new DrivePlacements(drives, applied, store, sync);
|
|
417
|
+
}
|
|
418
|
+
/** Where `drive` opens in this browser: a move, else the config. */
|
|
419
|
+
backendOf(drive) {
|
|
420
|
+
return effectiveBackend(drive, this.applied.get(drive.slug));
|
|
421
|
+
}
|
|
422
|
+
/** Whether a remote default drive at `url` keeps a copy here. */
|
|
423
|
+
keepsCopy(url) {
|
|
424
|
+
const drive = this.drives.find((d) => d.url === url);
|
|
425
|
+
return !drive || this.backendOf(drive) === "connect-local";
|
|
426
|
+
}
|
|
427
|
+
/** Drives that open on their switchboard with no copy here. */
|
|
428
|
+
placed() {
|
|
429
|
+
return this.drives.filter((drive) => this.backendOf(drive) !== LOCAL_BACKEND_NAME);
|
|
430
|
+
}
|
|
431
|
+
/** Collections this browser keeps a copy of; default drives are added with the others. */
|
|
432
|
+
keptCollections() {
|
|
433
|
+
return this.resolved.filter((drive) => drive.collection && drive.id !== void 0 && this.backendOf(drive) === "connect-local");
|
|
434
|
+
}
|
|
435
|
+
/** Installs both layers for every drive the router can be told about. */
|
|
436
|
+
attach(router, resolved) {
|
|
437
|
+
this.router = router;
|
|
438
|
+
this.resolved = [...resolved];
|
|
439
|
+
this.attached = true;
|
|
440
|
+
for (const drive of this.resolved) this.install(drive);
|
|
441
|
+
this.notify();
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Attaches, follows the moves other tabs made since `load`, then brings
|
|
445
|
+
* the sync remotes in line; returns the collections to keep a copy of.
|
|
446
|
+
*/
|
|
447
|
+
async start(router, resolved, syncsDrives) {
|
|
448
|
+
this.attach(router, resolved);
|
|
449
|
+
await this.reload();
|
|
450
|
+
if (!syncsDrives) return [];
|
|
451
|
+
const placed = new Set(this.placed().map((drive) => drive.slug));
|
|
452
|
+
await removeLocalCopies(this.sync, this.resolved.filter((drive) => placed.has(drive.slug) && !this.busy.has(drive.slug)));
|
|
453
|
+
return this.keptCollections();
|
|
454
|
+
}
|
|
455
|
+
/** The drive's placement in this browser; undefined for a drive the host does not place on a switchboard. */
|
|
456
|
+
placementOf(driveId) {
|
|
457
|
+
return this.snapshot.get(driveId);
|
|
458
|
+
}
|
|
459
|
+
subscribe(listener) {
|
|
460
|
+
this.listeners.add(listener);
|
|
461
|
+
return () => {
|
|
462
|
+
this.listeners.delete(listener);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/** Opens the drive on `backend` in this browser: its switchboard, or a copy here. */
|
|
466
|
+
async move(driveId, backend) {
|
|
467
|
+
const drive = this.resolved.find((d) => d.id === driveId);
|
|
468
|
+
if (!drive?.id) throw new Error(`Drive ${driveId} is not on a switchboard this app links`);
|
|
469
|
+
if (backend !== drive.remote && backend !== "connect-local") throw new Error(`Drive ${drive.slug} can't open on ${backend}`);
|
|
470
|
+
if (this.busy.has(drive.slug) || this.backendOf(drive) === backend) return;
|
|
471
|
+
const previous = this.applied.get(drive.slug);
|
|
472
|
+
const entry = backend === drive.configured ? void 0 : {
|
|
473
|
+
slug: drive.slug,
|
|
474
|
+
backend,
|
|
475
|
+
configWas: drive.configured
|
|
476
|
+
};
|
|
477
|
+
const id = drive.id;
|
|
478
|
+
this.busy.set(drive.slug, backend);
|
|
479
|
+
this.notify();
|
|
480
|
+
try {
|
|
481
|
+
await this.record(drive.slug, entry);
|
|
482
|
+
if (needsCopy(backend)) try {
|
|
483
|
+
await this.sync.add(servedUrl(drive), id);
|
|
484
|
+
await this.sync.untilCopied(id, drive.remote);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
await this.record(drive.slug, previous);
|
|
487
|
+
await removeLocalCopies(this.sync, [drive]);
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
else try {
|
|
491
|
+
await removeLocalCopies(this.sync, [drive]);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
await this.record(drive.slug, previous);
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
this.install(drive);
|
|
497
|
+
} finally {
|
|
498
|
+
this.busy.delete(drive.slug);
|
|
499
|
+
this.notify();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Back to where the host config opens the drive. */
|
|
503
|
+
reset(driveId) {
|
|
504
|
+
const drive = this.resolved.find((d) => d.id === driveId);
|
|
505
|
+
return drive ? this.move(driveId, drive.configured) : Promise.resolve();
|
|
506
|
+
}
|
|
507
|
+
/** Re-reads the store after another tab moved a drive; true when something changed. */
|
|
508
|
+
async reload() {
|
|
509
|
+
if (!this.attached) return false;
|
|
510
|
+
const next = await this.readMoves();
|
|
511
|
+
if (!next) return false;
|
|
512
|
+
const changed = this.drives.filter((drive) => !this.busy.has(drive.slug) && effectiveBackend(drive, next.get(drive.slug)) !== this.backendOf(drive));
|
|
513
|
+
if (changed.length === 0) return false;
|
|
514
|
+
await Promise.all(changed.map((drive) => this.follow(drive, next.get(drive.slug))));
|
|
515
|
+
this.notify();
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
/** A copy kept by another tab is routed here only once it has arrived. */
|
|
519
|
+
async follow(drive, entry) {
|
|
520
|
+
const resolved = this.resolved.find((d) => d.slug === drive.slug);
|
|
521
|
+
if (needsCopy(effectiveBackend(drive, entry)) && resolved?.id) {
|
|
522
|
+
this.busy.set(drive.slug, LOCAL_BACKEND_NAME);
|
|
523
|
+
this.notify();
|
|
524
|
+
try {
|
|
525
|
+
await this.sync.untilCopied(resolved.id, resolved.remote);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
logger.warn("Drive @slug was not copied into this browser; leaving it where it opens: @error", drive.slug, error);
|
|
528
|
+
return;
|
|
529
|
+
} finally {
|
|
530
|
+
this.busy.delete(drive.slug);
|
|
531
|
+
}
|
|
532
|
+
const latest = await this.readMoves();
|
|
533
|
+
if (!latest) return;
|
|
534
|
+
entry = latest.get(drive.slug);
|
|
535
|
+
}
|
|
536
|
+
if (entry) this.applied.set(drive.slug, entry);
|
|
537
|
+
else this.applied.delete(drive.slug);
|
|
538
|
+
if (resolved) this.install(resolved);
|
|
539
|
+
}
|
|
540
|
+
async readMoves() {
|
|
541
|
+
let entries;
|
|
542
|
+
try {
|
|
543
|
+
entries = await this.store.list();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
logger.warn("Could not read this browser's drive moves: @error", error);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const { applied } = applyPlacementEntries(this.drives, entries);
|
|
549
|
+
return new Map(applied.map((entry) => [entry.slug, entry]));
|
|
550
|
+
}
|
|
551
|
+
async record(slug, entry) {
|
|
552
|
+
if (entry) {
|
|
553
|
+
await this.store.put(entry);
|
|
554
|
+
this.applied.set(slug, entry);
|
|
555
|
+
} else {
|
|
556
|
+
await this.store.delete(slug);
|
|
557
|
+
this.applied.delete(slug);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
install(drive) {
|
|
561
|
+
const router = this.router;
|
|
562
|
+
if (!router) return;
|
|
563
|
+
const configured = drive.configured === drive.remote ? drive.remote : void 0;
|
|
564
|
+
const runtime = this.applied.get(drive.slug)?.backend;
|
|
565
|
+
for (const key of placementKeys(drive)) {
|
|
566
|
+
router.setConfiguredPlacement(key, configured);
|
|
567
|
+
router.setPlacement(key, runtime);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
notify() {
|
|
571
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
572
|
+
for (const drive of this.resolved) {
|
|
573
|
+
if (!drive.id) continue;
|
|
574
|
+
const backend = this.backendOf(drive);
|
|
575
|
+
snapshot.set(drive.id, {
|
|
576
|
+
drive,
|
|
577
|
+
backend,
|
|
578
|
+
moved: backend !== drive.configured,
|
|
579
|
+
busy: this.busy.has(drive.slug),
|
|
580
|
+
...this.busy.has(drive.slug) ? { movingTo: this.busy.get(drive.slug) } : {}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
this.snapshot = snapshot;
|
|
584
|
+
for (const listener of [...this.listeners]) listener();
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/runtime/ph-store.ts
|
|
589
|
+
/** The app's PH store: the active one, else a new one bound to `window.ph`.
|
|
590
|
+
* Values set on `window.ph` before carry over. */
|
|
591
|
+
function getConnectPHStore() {
|
|
592
|
+
const active = getActivePHStore();
|
|
593
|
+
if (active) return active;
|
|
594
|
+
const store = createPHStore();
|
|
595
|
+
bindPHStoreToWindow(store);
|
|
596
|
+
return store;
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/runtime/local-package.ts
|
|
600
|
+
/** Starts loading the project's package; a loader runs once, here. */
|
|
601
|
+
function loadLocalPackage(source) {
|
|
602
|
+
const pkg = typeof source === "function" ? source() : source;
|
|
603
|
+
if (pkg instanceof Promise) pkg.catch(() => void 0);
|
|
604
|
+
return pkg;
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
607
|
+
export { isMultiReactorEnabled as A, REACTOR_CLIENT_QUERY_KEY as C, REACTOR_WORKER_STORAGE_KEY as D, REACTOR_WORKER_QUERY_KEY as E, isReactorWorkerEnabled as O, selectAppReactorClient as S, resolveReactorClientMode as T, createAppReactorClient as _, effectiveBackend as a, refreshAppDrives as b, LOCAL_BACKEND_NAME as c, placementKeys as d, removeLocalCopies as f, switchboardDrives as g, servesSlug as h, caughtUp as i, resolveReactorWorkerEnabled as k, applyPlacementEntries as l, servedUrl as m, getConnectPHStore as n, findServedDrive as o, resolveSwitchboardDrives as p, DrivePlacements as r, needsCopy as s, loadLocalPackage as t, keepsLocalCopy as u, getAppDrives as v, REACTOR_CLIENT_STORAGE_KEY as w, relistAppDrives as x, planReactorClient as y };
|
|
608
|
+
|
|
609
|
+
//# sourceMappingURL=runtime-LvAkxbdf.js.map
|