tiddlywiki-plugin-dev
Version:
[](https://github.com/tiddly-gittly)
149 lines (148 loc) • 4.04 kB
JavaScript
import { WebSocketServer, WebSocket } from "ws";
const refreshHeartBeat = (ws) => {
ws.isAlive = true;
if (ws.heartBeatInterval) {
clearInterval(ws.heartBeatInterval);
}
ws.heartBeatInterval = setInterval(() => {
if (ws.isAlive === false) {
clearInterval(ws.heartBeatInterval);
delete ws.heartBeatInterval;
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
}, 5e3);
};
const DEV_WS_PATHNAME = "/__dev_ws";
const parseClientGeneration = (url) => {
if (!url) {
return 0;
}
try {
const generation = Number(
new URL(url, "http://localhost").searchParams.get("gen")
);
return Number.isFinite(generation) ? generation : 0;
} catch {
return 0;
}
};
const parseRequestPathname = (url) => {
if (!url) {
return "";
}
try {
return new URL(url, "http://localhost").pathname;
} catch {
return url;
}
};
const createNotifyServer = async () => {
const server = new WebSocketServer({ noServer: true });
let connectionCount = 0;
let currentGeneration = 0;
let saveBusy = false;
const saveBusyCallbacks = [];
server.on("connection", (ws, request) => {
connectionCount++;
const clientId = connectionCount;
const clientGeneration = parseClientGeneration(request?.url);
console.log(
`[Modern.TiddlyDev] [ws] Client #${clientId} connected (total: ${server.clients.size})`
);
refreshHeartBeat(ws);
ws.ping();
ws.on("pong", () => refreshHeartBeat(ws));
ws.on("message", (data) => {
const msg = String(data);
if (msg === "save-start") {
if (!saveBusy) {
saveBusy = true;
saveBusyCallbacks.forEach((cb) => cb(true));
}
return;
}
if (msg === "save-end") {
if (saveBusy) {
saveBusy = false;
saveBusyCallbacks.forEach((cb) => cb(false));
}
return;
}
});
ws.on("close", () => {
if (ws.heartBeatInterval) {
clearInterval(ws.heartBeatInterval);
}
const remaining = server.clients?.size ?? 0;
console.log(
`[Modern.TiddlyDev] [ws] Client #${clientId} disconnected (total: ${remaining})`
);
});
if (currentGeneration > 0 && clientGeneration < currentGeneration && ws.readyState === WebSocket.OPEN) {
console.log(
`[Modern.TiddlyDev] [refresh] Client #${clientId} is from build ${clientGeneration} (current ${currentGeneration}) — telling it to refresh`
);
ws.send("refresh");
}
});
const onUpgrade = (request, socket, head) => {
if (parseRequestPathname(request.url) === DEV_WS_PATHNAME) {
server.handleUpgrade(request, socket, head, (ws) => {
server.emit("connection", ws, request);
});
}
};
const attachToHttpServer = (httpServer) => {
httpServer.on("upgrade", onUpgrade);
return () => {
httpServer.removeListener("upgrade", onUpgrade);
};
};
const notifyRefresh = () => {
const count = server.clients.size;
if (count === 0) {
console.log(
`[Modern.TiddlyDev] [refresh] No clients connected — skipping browser notification`
);
} else {
console.log(
`[Modern.TiddlyDev] [refresh] Notifying ${count} client(s) to refresh`
);
server.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send("refresh");
}
});
}
};
const closeAllClients = () => {
for (const ws of server.clients) {
ws.terminate();
}
};
const setBuildGeneration = (generation) => {
if (Number.isFinite(generation)) {
currentGeneration = generation;
}
};
return {
server,
attachToHttpServer,
closeAllClients,
notifyRefresh,
setBuildGeneration,
onSaveBusyChange: (cb) => {
saveBusyCallbacks.push(cb);
return () => {
const i = saveBusyCallbacks.indexOf(cb);
if (i !== -1)
saveBusyCallbacks.splice(i, 1);
};
}
};
};
export {
createNotifyServer
};