mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-01 00:07:06 +00:00
130 lines
4.7 KiB
JavaScript
130 lines
4.7 KiB
JavaScript
import { createRequire } from "node:module";
|
|
import { pathToFileURL } from "node:url";
|
|
import { RTCPeerConnection } from "werift";
|
|
import { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
|
|
|
|
const requireFromProbe = createRequire(import.meta.url);
|
|
const commonlibEntry = requireFromProbe.resolve("@vrtmrz/livesync-commonlib/context");
|
|
const requireFromCommonlib = createRequire(commonlibEntry);
|
|
const nostrEntry = requireFromCommonlib.resolve("@trystero-p2p/nostr");
|
|
const { getRelaySockets, joinRoom, pauseRelayReconnection } = await import(pathToFileURL(nostrEntry).href);
|
|
|
|
const relayUrl = process.env.RELAY ?? "ws://nostr-relay:7777/";
|
|
const timeoutMs = Number(process.env.RELAY_TIMEOUT_MS ?? 15_000);
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function waitFor(description, predicate) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() <= deadline) {
|
|
if (predicate()) return;
|
|
await delay(50);
|
|
}
|
|
throw new Error(`Timed out waiting for ${description}`);
|
|
}
|
|
|
|
async function waitForSocketOpen(socket, description) {
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
// Node can expose OPEN before it dispatches the event. Yield once so
|
|
// Trystero's previously registered onopen handler has completed before
|
|
// this probe starts the close handshake.
|
|
await delay(0);
|
|
if (socket.readyState === WebSocket.OPEN) return;
|
|
}
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error(`Timed out waiting for ${description}; readyState=${socket.readyState}`));
|
|
}, timeoutMs);
|
|
const cleanup = () => {
|
|
clearTimeout(timeout);
|
|
socket.removeEventListener("open", onOpen);
|
|
socket.removeEventListener("close", onClose);
|
|
socket.removeEventListener("error", onError);
|
|
};
|
|
const onOpen = () => {
|
|
cleanup();
|
|
resolve();
|
|
};
|
|
const onClose = () => {
|
|
cleanup();
|
|
reject(new Error(`${description} closed before opening`));
|
|
};
|
|
const onError = () => {
|
|
cleanup();
|
|
reject(new Error(`${description} failed before opening`));
|
|
};
|
|
|
|
// Trystero installs its onopen handler while constructing the socket,
|
|
// before this observer is registered. Waiting for the actual event
|
|
// therefore establishes transport readiness without a fixed delay.
|
|
socket.addEventListener("open", onOpen, { once: true });
|
|
socket.addEventListener("close", onClose, { once: true });
|
|
socket.addEventListener("error", onError, { once: true });
|
|
});
|
|
}
|
|
|
|
const room = joinRoom(
|
|
{
|
|
appId: `livesync-relay-disconnect-probe-${Date.now()}`,
|
|
password: "local-test-only",
|
|
relayConfig: {
|
|
urls: [relayUrl],
|
|
manualReconnection: true,
|
|
},
|
|
rtcPolyfill: RTCPeerConnection,
|
|
},
|
|
"disconnect-probe"
|
|
);
|
|
|
|
try {
|
|
await waitFor("the relay WebSocket to be registered", () => Object.values(getRelaySockets()).length > 0);
|
|
const originalSockets = Object.values(getRelaySockets());
|
|
await Promise.all(
|
|
originalSockets.map((socket, index) => waitForSocketOpen(socket, `relay WebSocket ${index + 1} to open`))
|
|
);
|
|
|
|
const replicator = new TrysteroReplicator(
|
|
{},
|
|
{
|
|
close: async () => undefined,
|
|
dispatchConnectionStatus: async () => undefined,
|
|
}
|
|
);
|
|
replicator.disconnectFromServer();
|
|
|
|
await waitFor("all relay WebSockets to close", () =>
|
|
originalSockets.every((socket) => socket.readyState === WebSocket.CLOSED)
|
|
);
|
|
await delay(4_000);
|
|
if (!originalSockets.every((socket) => socket.readyState === WebSocket.CLOSED)) {
|
|
throw new Error("A relay WebSocket reconnected while reconnection was paused");
|
|
}
|
|
|
|
replicator.allowReconnection();
|
|
await waitFor("a replacement relay WebSocket to be registered", () =>
|
|
Object.values(getRelaySockets()).some((socket) => !originalSockets.includes(socket))
|
|
);
|
|
const replacementSockets = Object.values(getRelaySockets()).filter((socket) => !originalSockets.includes(socket));
|
|
await Promise.all(
|
|
replacementSockets.map((socket, index) =>
|
|
waitForSocketOpen(socket, `replacement relay WebSocket ${index + 1} to open`)
|
|
)
|
|
);
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
socketsClosed: originalSockets.length,
|
|
stayedDisconnectedWhilePaused: true,
|
|
reconnectedAfterResume: true,
|
|
})
|
|
);
|
|
} finally {
|
|
await room.leave();
|
|
pauseRelayReconnection();
|
|
for (const socket of Object.values(getRelaySockets())) socket.close();
|
|
}
|