AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6

Merged
bvandeusen merged 214 commits from dev into main 2026-10-21 21:40:00 -04:00
Showing only changes of commit 729d0dadf1 - Show all commits
+63 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useNotesStore } from "../stores/notes";
import Icon from "./Icon.vue";
import LabelPicker from "./LabelPicker.vue";
@@ -165,6 +165,63 @@ async function flush(): Promise<void> {
}
}
// ---- idle autosave ----
//
// This editor used to write ONLY on close, and the reason was cost: a body write
// snapshotted a revision, so saving often meant a version history of thirty
// snapshots of one paragraph being typed. The price was durability — a tab closed
// mid-paragraph lost the paragraph, which is the one thing a notes app must not do.
//
// That trade is gone. Both engines now coalesce snapshots to one per editing
// session (`revisions.py` and `store.rs`'s `should_snapshot`, Scribe #2971), so a
// write costs a write. Writing on an idle pause is what collects the refund; the
// Android editor already does the same.
const AUTOSAVE_MS = 1000;
let autosaveTimer: ReturnType<typeof setTimeout> | null = null;
function cancelAutosave(): void {
if (autosaveTimer !== null) {
clearTimeout(autosaveTimer);
autosaveTimer = null;
}
}
async function autosave(): Promise<void> {
// `flush` returns without writing while a save is in flight, which would silently
// drop everything typed since that save began. Re-arming rather than skipping is
// what keeps that from being a lost paragraph.
if (saving.value) {
scheduleAutosave();
return;
}
try {
await flush();
} catch {
// Swallowed on purpose. An autosave that interrupts typing with an error is
// worse than one that waits for the next pause, and `close` still surfaces a
// real failure at the moment the person is looking at the editor.
}
}
function scheduleAutosave(): void {
cancelAutosave();
autosaveTimer = setTimeout(() => {
autosaveTimer = null;
void autosave();
}, AUTOSAVE_MS);
}
// EDIT mode only, deliberately. In compose, `dismiss` discards a note that was
// never persisted, so that an accidental keystroke or a type-to-compose never
// litters the board — and an autosave that created the row would take that away
// without anyone asking for it. Materialising a compose on first keystroke is a
// separate decision (Scribe #2967), not a side effect of this one.
watch(body, () => {
if (!isCreate.value) scheduleAutosave();
});
onBeforeUnmount(cancelAutosave);
function resetCompose(): void {
noteId.value = null;
setBody("");
@@ -225,6 +282,10 @@ async function finish(): Promise<void> {
// Persist (create in compose, save in edit) and close the editor.
async function close(): Promise<void> {
// Cancelled first: a timer that fires during the leave animation would write
// through a component on its way out, after `flush` has already saved the same
// text.
cancelAutosave();
await flush();
await finish();
}
@@ -233,6 +294,7 @@ async function close(): Promise<void> {
// commit it explicitly (Done, Ctrl/Cmd+Enter, or Shift+Enter). An existing note, or a
// compose already persisted by a rich action, closes normally (saving its text).
async function dismiss(): Promise<void> {
cancelAutosave();
if (isCreate.value) {
await finish();
return;