alinea
Version:
Headless git-based CMS
192 lines (188 loc) • 5.78 kB
JavaScript
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
};