This commit is contained in:
Mateusz Gruszczyński
2026-07-22 11:15:01 +02:00
parent 1be2023e36
commit 8bf45938ea
19 changed files with 189 additions and 33 deletions
+11 -2
View File
@@ -1,19 +1,28 @@
import { logDebug, logError, logWarn } from "./logger.js";
export async function api(path, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 12000);
try {
const headers = new Headers(options.headers || {});
if (!(options.body instanceof FormData) && !headers.has("content-type")) headers.set("content-type", "application/json");
const started = performance.now();
logDebug("api.request", { method: options.method || "GET", path });
const response = await fetch(path, { ...options, headers, signal: controller.signal });
const durationMs = Math.round(performance.now() - started);
logDebug("api.response", { method: options.method || "GET", path, status: response.status, durationMs });
const contentType = response.headers.get("content-type") || "";
const data = contentType.includes("application/json") ? await response.json().catch(() => ({})) : {};
if (!response.ok) {
const defaults = { 400: "Invalid request.", 401: "Authentication required.", 403: "Access denied.", 404: "The requested resource was not found.", 405: "This operation is not allowed.", 409: "The requested change conflicts with existing data.", 413: "The uploaded data is too large.", 429: "Too many requests. Try again later.", 500: "Server error. Try again later.", 503: "Service temporarily unavailable." };
throw new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`);
const requestError = new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`);
logWarn("api.failed", { method: options.method || "GET", path, status: response.status, message: requestError.message });
throw requestError;
}
return data;
} catch (error) {
if (error.name === "AbortError") throw new Error("Timed out");
if (error.name === "AbortError") { logWarn("api.timeout", { method: options.method || "GET", path }); throw new Error("Timed out"); }
logError("api.network_error", error, { method: options.method || "GET", path });
throw error;
} finally { clearTimeout(timeout); }
}
+3
View File
@@ -1,3 +1,6 @@
import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { bindIdentityDialog, handleResetToken, logoutCurrentSession, validateCurrentSession } from "./auth-ui.js";
import { api } from "@rustpad/api";
+32
View File
@@ -0,0 +1,32 @@
const PREFIX = "[RustPad]";
const LEVELS = Object.freeze({ off: 0, error: 1, warn: 2, info: 3, debug: 4 });
function configuredLevel() {
const configured = window.__RUSTPAD_CONFIG__?.frontendLogLevel;
return Object.prototype.hasOwnProperty.call(LEVELS, configured) ? configured : "warn";
}
function enabled(level) {
return LEVELS[configuredLevel()] >= LEVELS[level];
}
function safeDetails(details) {
if (!details || typeof details !== "object") return details;
const blocked = /password|token|secret|authorization|cookie|email|content/i;
return Object.fromEntries(Object.entries(details).map(([key, value]) => [key, blocked.test(key) ? "[redacted]" : value]));
}
export function logInfo(event, details = {}) { if (enabled("info")) console.info(PREFIX, event, safeDetails(details)); }
export function logWarn(event, details = {}) { if (enabled("warn")) console.warn(PREFIX, event, safeDetails(details)); }
export function logError(event, error, details = {}) {
if (enabled("error")) console.error(PREFIX, event, { ...safeDetails(details), error: error instanceof Error ? error.message : String(error) });
}
export function logDebug(event, details = {}) { if (enabled("debug")) console.debug(PREFIX, event, safeDetails(details)); }
export function installGlobalDiagnostics() {
logInfo("frontend.initialized", { path: location.pathname, assetVersion: document.body?.dataset.assetVersion || "unknown", logLevel: configuredLevel() });
window.addEventListener("error", (event) => logError("frontend.uncaught_error", event.error || event.message, { file: event.filename, line: event.lineno, column: event.colno }));
window.addEventListener("unhandledrejection", (event) => logError("frontend.unhandled_rejection", event.reason));
window.addEventListener("online", () => logInfo("network.online"));
window.addEventListener("offline", () => logWarn("network.offline"));
}
+3
View File
@@ -1,3 +1,6 @@
import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
+3
View File
@@ -1,3 +1,6 @@
import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
+3
View File
@@ -1,3 +1,6 @@
import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { renderMarkdown } from "@rustpad/markdown";
+4 -2
View File
@@ -1,11 +1,13 @@
import { logDebug, logError, logInfo, logWarn } from "./logger.js";
export class NoteSocket {
constructor({ workspaceSlug, noteSlug, password, nickname, sessionToken, onStatus, onAuthenticated, onDocument, onError }) { Object.assign(this, { workspaceSlug, noteSlug, password, nickname, sessionToken, onStatus, onAuthenticated, onDocument, onError }); this.socket=null; this.timer=null; this.closed=false; }
connect() { clearTimeout(this.timer); this.closed=false; this.onStatus?.("connecting"); const protocol=location.protocol==="https:"?"wss:":"ws:"; this.socket=new WebSocket(`${protocol}//${location.host}/ws/${encodeURIComponent(this.workspaceSlug)}/${encodeURIComponent(this.noteSlug)}`); this.socket.addEventListener("open",()=>this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null,session_token:this.sessionToken||null}))); this.socket.addEventListener("message",event=>{const m=JSON.parse(event.data); if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();} if(m.type==="authenticated"){this.onStatus?.("online");this.onAuthenticated?.(m);} if(m.type==="document")this.onDocument?.(m);}); this.socket.addEventListener("close",()=>{if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}}); this.socket.addEventListener("error",()=>{this.onError?.("Failed to connect to the WebSocket server");this.socket.close();}); }
connect() { clearTimeout(this.timer); this.closed=false; this.onStatus?.("connecting"); const protocol=location.protocol==="https:"?"wss:":"ws:"; this.socket=new WebSocket(`${protocol}//${location.host}/ws/${encodeURIComponent(this.workspaceSlug)}/${encodeURIComponent(this.noteSlug)}`); this.socket.addEventListener("open",()=>{logInfo("websocket.open",{kind:"note",workspace:this.workspaceSlug,note:this.noteSlug});this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null,session_token:this.sessionToken||null}));}); this.socket.addEventListener("message",event=>{const m=JSON.parse(event.data); if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();} if(m.type==="authenticated"){logInfo("websocket.authenticated",{kind:"note",workspace:this.workspaceSlug,note:this.noteSlug});this.onStatus?.("online");this.onAuthenticated?.(m);} if(m.type==="document")this.onDocument?.(m);}); this.socket.addEventListener("close",event=>{logWarn("websocket.close",{kind:"note",code:event.code,reason:event.reason||"",intentional:this.closed});if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}}); this.socket.addEventListener("error",event=>{logError("websocket.error",event,{kind:"note"});this.onError?.("Failed to connect to the WebSocket server");this.socket.close();}); }
update(content, ownerMap="[]") { if(this.socket?.readyState===WebSocket.OPEN)this.socket.send(JSON.stringify({type:"update",content,owner_map:ownerMap})); }
stop(){this.closed=true;clearTimeout(this.timer);this.socket?.close();}
}
export class PadSocket {
constructor({slug,password,nickname,sessionToken,onStatus,onAuthenticated,onDocument,onError}){Object.assign(this,{slug,password,nickname,sessionToken,onStatus,onAuthenticated,onDocument,onError});this.socket=null;this.timer=null;this.closed=false;}
connect(){clearTimeout(this.timer);this.closed=false;this.onStatus?.("connecting");const protocol=location.protocol==="https:"?"wss:":"ws:";this.socket=new WebSocket(`${protocol}//${location.host}/ws/p/${encodeURIComponent(this.slug)}`);this.socket.addEventListener("open",()=>this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null,session_token:this.sessionToken||null})));this.socket.addEventListener("message",e=>{const m=JSON.parse(e.data);if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();}if(m.type==="authenticated"){this.onStatus?.("online");this.onAuthenticated?.(m);}if(m.type==="document")this.onDocument?.(m);});this.socket.addEventListener("close",()=>{if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}});this.socket.addEventListener("error",()=>{this.onError?.("Failed to connect to the WebSocket server");this.socket.close();});}
connect(){clearTimeout(this.timer);this.closed=false;this.onStatus?.("connecting");const protocol=location.protocol==="https:"?"wss:":"ws:";this.socket=new WebSocket(`${protocol}//${location.host}/ws/p/${encodeURIComponent(this.slug)}`);this.socket.addEventListener("open",()=>{logInfo("websocket.open",{kind:"note",workspace:this.workspaceSlug,note:this.noteSlug});this.socket.send(JSON.stringify({type:"authenticate",password:this.password||null,nickname:this.nickname||null,session_token:this.sessionToken||null}));});this.socket.addEventListener("message",e=>{const m=JSON.parse(e.data);if(m.type==="error"){this.onError?.(m.message);this.closed=true;this.socket.close();}if(m.type==="authenticated"){logInfo("websocket.authenticated",{kind:"note",workspace:this.workspaceSlug,note:this.noteSlug});this.onStatus?.("online");this.onAuthenticated?.(m);}if(m.type==="document")this.onDocument?.(m);});this.socket.addEventListener("close",event=>{logWarn("websocket.close",{kind:"note",code:event.code,reason:event.reason||"",intentional:this.closed});if(!this.closed){this.onStatus?.("offline");this.timer=setTimeout(()=>this.connect(),1500);}});this.socket.addEventListener("error",event=>{logError("websocket.error",event,{kind:"note"});this.onError?.("Failed to connect to the WebSocket server");this.socket.close();});}
update(content,ownerMap="[]"){if(this.socket?.readyState===WebSocket.OPEN)this.socket.send(JSON.stringify({type:"update",content,owner_map:ownerMap}));} stop(){this.closed=true;clearTimeout(this.timer);this.socket?.close();}
}
+3
View File
@@ -1,3 +1,6 @@
import { installGlobalDiagnostics, logInfo } from "./logger.js";
installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { getNickname, getPassword, setPassword } from "@rustpad/session";