UNPKG

alinea

Version:
192 lines (188 loc) 5.78 kB
import "../../chunks/chunk-NZLE2WMY.js"; // src/dashboard/boot/BootDev.ts import { Client } from "alinea/core/Client"; // node_modules/shared-event-source/dist/index.js var noop = Function.prototype; var SharedEventSource = class _SharedEventSource extends EventTarget { url; withCredentials; readyState; onerror = noop; onmessage = noop; onopen = noop; static CONNECTING = 0; static OPEN = 1; static CLOSED = 2; isLeader = false; #id; #channel; #realEventSource = null; #clients = /* @__PURE__ */ new Set(); #lockReleaseResolver = null; constructor(url, eventSourceInitDict) { super(); if (!globalThis.BroadcastChannel || !navigator.locks) { throw new Error("EventSourceChannel requires a browser environment with BroadcastChannel and Web Locks API support."); } this.url = url; this.withCredentials = eventSourceInitDict?.withCredentials ?? false; this.readyState = _SharedEventSource.CONNECTING; this.#id = crypto.randomUUID(); const channelName = `eventsource-channel:${this.url}`; this.#channel = new BroadcastChannel(channelName); this.#channel.onmessage = this.#handleBroadcastMessage.bind(this); this.addEventListener("open", (e) => this.onopen(e)); this.addEventListener("message", (e) => this.onmessage(e)); this.addEventListener("error", (e) => this.onerror(e)); this.#attemptToBecomeLeader(); this.#broadcast({ type: "client-add", payload: { id: this.#id } }); } close() { if (this.readyState === _SharedEventSource.CLOSED) { return; } this.readyState = _SharedEventSource.CLOSED; this.#broadcast({ type: "client-remove", payload: { id: this.#id } }); this.#cleanup(); } #attemptToBecomeLeader() { const lockName = `eventsource-leader-lock:${this.url}`; navigator.locks.request(lockName, async () => { this.isLeader = true; this.#setupLeader(); await new Promise((resolve) => { this.#lockReleaseResolver = resolve; }); this.isLeader = false; this.#realEventSource?.close(); this.#realEventSource = null; }); } #setupLeader() { this.#clients.add(this.#id); this.#realEventSource = new EventSource(this.url, { withCredentials: this.withCredentials }); this.#realEventSource.onopen = () => { this.readyState = _SharedEventSource.OPEN; this.#broadcast({ type: "event-open" }); }; this.#realEventSource.onmessage = (event) => { this.#broadcast({ type: "event-message", payload: { data: event.data, origin: event.origin, lastEventId: event.lastEventId } }); }; this.#realEventSource.onerror = () => { this.readyState = _SharedEventSource.CLOSED; this.#broadcast({ type: "event-error" }); }; } #handleBroadcastMessage(event) { const { type, payload } = event.data; if (this.isLeader) { if (type === "client-add") { this.#clients.add(payload.id); if (this.#realEventSource?.readyState === EventSource.OPEN) { this.#broadcast({ type: "event-open" }); } } else if (type === "client-remove") { this.#clients.delete(payload.id); if (this.#clients.size === 0) { this.close(); } } } switch (type) { case "event-open": this.readyState = _SharedEventSource.OPEN; this.dispatchEvent(new Event("open")); break; case "event-message": this.dispatchEvent(new MessageEvent("message", payload)); break; case "event-error": this.readyState = _SharedEventSource.CLOSED; this.dispatchEvent(new Event("error")); break; } } #broadcast(message) { this.#channel.postMessage(message); if (this.isLeader) { this.#handleBroadcastMessage({ data: message }); } } #cleanup() { if (this.#lockReleaseResolver) { this.#lockReleaseResolver(); this.#lockReleaseResolver = null; } this.#channel.close(); this.onopen = noop; this.onmessage = noop; this.onerror = noop; } }; // src/dashboard/boot/BootDev.ts import { boot } from "./Boot.js"; function bootDev() { return boot(getConfig()); } async function* getConfig() { const buildId = process.env.ALINEA_BUILD_ID; let revision = buildId; const source = new SharedEventSource("/~dev"); const url = new URL("/api", location.href).href; const createConfig = async (revision2) => { const { cms, views } = await loadConfig(revision2); const { config } = cms; const client = new Client({ config, url }); return { local: true, alineaDev: Boolean(process.env.ALINEA_DEV), revision: revision2, config, views, client }; }; let batch; while (true) { const next = batch?.revision !== revision ? await createConfig(revision) : batch; yield next; revision = await new Promise((resolve) => { source.addEventListener( "message", (event) => { console.info(`[reload] received ${event.data}`); const info = JSON.parse(event.data); switch (info.type) { case "refresh": return resolve(info.revision); case "reload": if (typeof window === "undefined") return resolve(info.revision); return window.location.reload(); case "refetch": return resolve(revision); } }, { once: true } ); }); } } async function loadConfig(revision) { const exports = await import(`/config.js?${revision}`); if (!("cms" in exports)) throw new Error(`No config found in "/config.js"`); return exports; } export { bootDev };