From 54b8a4a60a8f9174745f74f8d07cfacf9a5cd44b Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Wed, 4 Nov 2026 08:19:40 +0100 Subject: [PATCH 1/2] fix: log failed saves and dropped frames; plugin.version matches deno.json Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 12 ++++++++++++ src/commit.ts | 9 +++++++-- src/log.ts | 29 +++++++++++++++++++++++++++++ src/manager.ts | 5 ++++- src/mod.ts | 2 +- src/ws-server.ts | 14 ++++++++++---- tests/log_test.ts | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 src/log.ts create mode 100644 tests/log_test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d3836..f09ae8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] + +### Fixed + +- **Failed saves and dropped frames are no longer silent.** A failed write of the + collaborative document (on each update, on idle eviction, on the last client + leaving), a failed flush, a throwing `onPageUpdate` hook, and any error while + handling a websocket frame were swallowed by empty `catch`es. They now log a + warning, at most one per kind per minute. +- **`plugin.version` reported `2.1.4`.** It now matches `deno.json`, and a test + keeps the two in step. + ## [2.2.3] — 2026-10-27 ### Fixed diff --git a/src/commit.ts b/src/commit.ts index 2176235..2d0fc37 100644 --- a/src/commit.ts +++ b/src/commit.ts @@ -10,6 +10,7 @@ * before writing to the Y.js text; what we persist here is that Markdown. */ +import { warnThrottled } from "./log.ts"; import type * as Y from "yjs"; import { parse as parseYaml, stringify as stringifyYaml } from "@std/yaml"; import type { StorageAdapter } from "@dune/core"; @@ -107,7 +108,9 @@ export async function commitDoc(opts: { // Write to disk. await storage.write(filePath, committed); - hooks?.fire("onPageUpdate", { sourcePath }).catch(() => {}); + hooks?.fire("onPageUpdate", { sourcePath }).catch((err) => + warnThrottled("hook", "onPageUpdate hook failed", err) + ); } // ── Field patch ──────────────────────────────────────────────────────────────── @@ -170,5 +173,7 @@ export async function patchFrontmatterFields(opts: { await storage.write(filePath, patched); - hooks?.fire("onPageUpdate", { sourcePath }).catch(() => {}); + hooks?.fire("onPageUpdate", { sourcePath }).catch((err) => + warnThrottled("hook", "onPageUpdate hook failed", err) + ); } diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..18b6bd1 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,29 @@ +/** + * Rate-limited warnings for failures that must not break the editing session + * but should not be invisible either (a failed save, a dropped frame, a + * throwing hook). At most one line per key per minute, so a misbehaving client + * cannot flood the log. + * + * @module + */ + +const INTERVAL_MS = 60_000; +const lastLogged = new Map(); + +/** Log `message` and `err` unless the same `key` was logged in the last minute. */ +export function warnThrottled( + key: string, + message: string, + err: unknown, + now: number = Date.now(), +): void { + const last = lastLogged.get(key); + if (last !== undefined && now - last < INTERVAL_MS) return; + lastLogged.set(key, now); + console.warn(`[inline-edit] ${message}:`, err); +} + +/** Test helper: forget what has been logged. */ +export function resetWarnThrottle(): void { + lastLogged.clear(); +} diff --git a/src/manager.ts b/src/manager.ts index 6484dbb..b844252 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -6,6 +6,7 @@ * connected clients. */ +import { warnThrottled } from "./log.ts"; import type * as Y from "yjs"; import * as awarenessProtocol from "y-protocols/awareness"; import type { InlineEditManager } from "@dune/core/inline-edit"; @@ -105,7 +106,9 @@ export function createInlineEditManager( now - session.lastActivity > SESSION_IDLE_TTL_MS ) { // Persist before evicting. - persistYDoc(dataDir, path, doc, storage).catch(() => {}); + persistYDoc(dataDir, path, doc, storage).catch((err) => + warnThrottled("persist", `saving ${path} before eviction failed`, err) + ); if (session.flushTimer !== undefined) { clearTimeout(session.flushTimer); } diff --git a/src/mod.ts b/src/mod.ts index fb39839..ca9e561 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -41,7 +41,7 @@ import { injectAdminBar } from "./bar.ts"; const plugin: DunePlugin = { name: "inline-edit", - version: "2.1.4", + version: "2.2.3", description: "Y.js-backed real-time inline content editing (TipTap WYSIWYG) with admin bar.", hooks: {}, diff --git a/src/ws-server.ts b/src/ws-server.ts index 71706c1..88895a5 100644 --- a/src/ws-server.ts +++ b/src/ws-server.ts @@ -21,6 +21,7 @@ import * as decoding from "lib0/decoding"; import type { InlineEditClient, InlineEditSession } from "./types.ts"; import { persistYDoc } from "./ydoc-manager.ts"; +import { warnThrottled } from "./log.ts"; import type { StorageAdapter } from "@dune/core"; // ── Protocol constants ──────────────────────────────────────────────────────── @@ -201,7 +202,9 @@ export function connectClient(opts: { scheduleFlush(session, autoFlushMs, onFlush); // Persist asynchronously (fire-and-forget on each update; compacts). - persistYDoc(dataDir, session.sourcePath, doc, storage).catch(() => {}); + persistYDoc(dataDir, session.sourcePath, doc, storage).catch((err) => + warnThrottled("persist", `saving ${session.sourcePath} failed`, err) + ); } } else if (messageType === MESSAGE_AWARENESS) { // Update awareness; broadcast to all clients including sender (standard @@ -211,8 +214,9 @@ export function connectClient(opts: { const changedClients = Array.from(awareness.getStates().keys()); broadcast(session, encodeAwarenessUpdate(awareness, changedClients)); } - } catch { - // Malformed frame — silently drop. + } catch (err) { + // Malformed frame (or a failure applying it): drop it, but say so. + warnThrottled("frame", `dropped a websocket message for ${session.sourcePath}`, err); } }; @@ -236,7 +240,9 @@ export function connectClient(opts: { // Auto-flush when last client leaves if there's pending content. if (session.clients.size === 0) { clearFlushTimer(session); - onFlush(session.sourcePath).catch(() => {}); + onFlush(session.sourcePath).catch((err) => + warnThrottled("flush", `flushing ${session.sourcePath} failed`, err) + ); } }; diff --git a/tests/log_test.ts b/tests/log_test.ts new file mode 100644 index 0000000..2a745a2 --- /dev/null +++ b/tests/log_test.ts @@ -0,0 +1,36 @@ +import { assertEquals } from "@std/assert"; +import { resetWarnThrottle, warnThrottled } from "../src/log.ts"; +import plugin from "../src/mod.ts"; + +function capture(fn: () => void): unknown[][] { + const calls: unknown[][] = []; + const orig = console.warn; + console.warn = (...a: unknown[]) => void calls.push(a); + try { + fn(); + } finally { + console.warn = orig; + } + return calls; +} + +Deno.test("warnThrottled logs once per key per minute", () => { + resetWarnThrottle(); + const err = new Error("boom"); + const calls = capture(() => { + warnThrottled("k", "first", err, 1_000); + warnThrottled("k", "second", err, 30_000); // within a minute: dropped + warnThrottled("other", "third", err, 30_000); // different key: logged + warnThrottled("k", "fourth", err, 62_000); // a minute later: logged + }); + assertEquals(calls.length, 3); + assertEquals(calls[0][0], "[inline-edit] first:"); + assertEquals(calls[0][1], err); +}); + +Deno.test("plugin.version matches deno.json", async () => { + const pkg = JSON.parse( + await Deno.readTextFile(new URL("../deno.json", import.meta.url)), + ); + assertEquals(plugin.version, pkg.version); +}); From b1e23134f5bec6bed4dd64ef04764ebbdaa6aca6 Mon Sep 17 00:00:00 2001 From: zumbrunn Date: Wed, 4 Nov 2026 08:31:38 +0100 Subject: [PATCH 2/2] docs(changelog): reference #3 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f09ae8d..19e8752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- **Failed saves and dropped frames are no longer silent.** A failed write of the +- **Failed saves and dropped frames are no longer silent (#3).** A failed write of the collaborative document (on each update, on idle eviction, on the last client leaving), a failed flush, a throwing `onPageUpdate` hook, and any error while handling a websocket frame were swallowed by empty `catch`es. They now log a