@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.
@@ -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