Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [Unreleased]

### Fixed

- **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
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
Expand Down
9 changes: 7 additions & 2 deletions src/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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)
);
}
29 changes: 29 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>();

/** 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();
}
5 changes: 4 additions & 1 deletion src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},

Expand Down
14 changes: 10 additions & 4 deletions src/ws-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
};

Expand All @@ -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)
);
}
};

Expand Down
36 changes: 36 additions & 0 deletions tests/log_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading