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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.58.0",
"version": "2.59.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down
16 changes: 10 additions & 6 deletions apps/desktop/src/main/undo-history-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
MAX_UNDO_HISTORY_AGE_MS,
MAX_UNDO_HISTORY_BYTES,
MAX_UNDO_HISTORY_FILES,
UNDO_HISTORY_DIR,
clearUndoHistories,
pruneUndoHistories,
Expand Down Expand Up @@ -94,21 +93,26 @@ describe('pruning undo history files', () => {
expect((await readdir(path.dirname(oldFile))).length).toBe(1)
})

// Checked against a small cap, not the real 400: writing 400 histories took
// over five seconds on the Windows runner, the test timed out, and vitest's
// cleanup then raced the abandoned writes into an ENOTEMPTY.
it('keeps the most recently written notes when a vault has too many', async () => {
const cap = 4
const extra = 3
for (let n = 0; n < MAX_UNDO_HISTORY_FILES + extra; n++) {
for (let n = 0; n < cap + extra; n++) {
await writeUndoHistory(base, VAULT, `inbox/${n}.md`, String(n))
const file = undoHistoryFile(base, VAULT, `inbox/${n}.md`)!
const at = new Date(Date.now() - (MAX_UNDO_HISTORY_FILES + extra - n) * 1000)
const at = new Date(Date.now() - (cap + extra - n) * 1000)
await utimes(file, at, at)
}
await pruneUndoHistories(base)
await pruneUndoHistories(base, Date.now(), cap)
expect(await readUndoHistory(base, VAULT, 'inbox/0.md')).toBeNull()
expect(await readUndoHistory(base, VAULT, `inbox/${extra - 1}.md`)).toBeNull()
expect(await readUndoHistory(base, VAULT, `inbox/${extra}.md`)).toBe(String(extra))
expect(await readFile(undoHistoryFile(base, VAULT, `inbox/${MAX_UNDO_HISTORY_FILES}.md`)!, 'utf8')).toBe(
String(MAX_UNDO_HISTORY_FILES)
expect(await readFile(undoHistoryFile(base, VAULT, `inbox/${cap + extra - 1}.md`)!, 'utf8')).toBe(
String(cap + extra - 1)
)
expect(await readdir(path.dirname(undoHistoryFile(base, VAULT, 'inbox/0.md')!))).toHaveLength(cap)
})

it('is fine with nothing to prune', async () => {
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/main/undo-history-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ export async function clearUndoHistories(baseDir: string): Promise<void> {
* oldest ones beyond the per-vault count, and any stray partial writes. Cheap
* enough to run once per launch.
*/
export async function pruneUndoHistories(baseDir: string, now: number = Date.now()): Promise<void> {
export async function pruneUndoHistories(
baseDir: string,
now: number = Date.now(),
maxFiles: number = MAX_UNDO_HISTORY_FILES
): Promise<void> {
const root = path.join(baseDir, UNDO_HISTORY_DIR)
let vaults: string[]
try {
Expand Down Expand Up @@ -134,7 +138,7 @@ export async function pruneUndoHistories(baseDir: string, now: number = Date.now
}
}
kept.sort((a, b) => b.mtimeMs - a.mtimeMs)
for (const { file } of kept.slice(MAX_UNDO_HISTORY_FILES)) {
for (const { file } of kept.slice(maxFiles)) {
await fsp.rm(file, { force: true }).catch(() => undefined)
}
if (kept.length === 0) await fsp.rmdir(dir).catch(() => undefined)
Expand Down
20 changes: 10 additions & 10 deletions apps/desktop/terminal-release.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@
"release": {
"repository": "ZenNotes/tui",
"protocol": 1,
"version": "0.4.1",
"commit": "86e2149b94fc27587a704fc02afd25e5a42c5cc2",
"version": "0.6.0",
"commit": "5d18fc86b58f94d7ffe90cbf9767904a20f33e9e",
"artifacts": {
"darwin-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_darwin_arm64.tar.gz",
"sha256": "3c83f847268c26279121537c807785dfab0403fa0cae248f149d09a11a2e2eda"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_darwin_arm64.tar.gz",
"sha256": "5102fce1a2ee2cf9c7358dd0eb8d929023a5675ceb693a96ad2fa50fc6936795"
},
"darwin-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_darwin_amd64.tar.gz",
"sha256": "d541dcc3fbfd15d1f702a71ff4d629d1a1907a21d055c23df27934925e9f4557"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_darwin_amd64.tar.gz",
"sha256": "514a642c820bbbba9f93b6e29626757ba165bc8f69ee967b6304054ad65633b2"
},
"linux-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_linux_arm64.tar.gz",
"sha256": "55d6999163b66fc0e29a3a35a449f37175c448bb7e6e22072ee1c970769fd739"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_linux_arm64.tar.gz",
"sha256": "9c7bfb76ad248585cccd61fedea9a0e6547afd1f14cc96191799996478118142"
},
"linux-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.4.1/zn_0.4.1_linux_amd64.tar.gz",
"sha256": "42e5e64edde08f3145883ffcfeaf197d6290d966074ce0e28f054d254bb806c1"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.6.0/zn_0.6.0_linux_amd64.tar.gz",
"sha256": "7e41439f78cceeb80e10a7d0caa68c7ae80986e821cca4d0f5787fdd597c426b"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/share-viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/share-viewer",
"private": true,
"version": "2.58.0",
"version": "2.59.0",
"type": "module",
"description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website",
"homepage": "https://zennotes.org",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/web",
"private": true,
"version": "2.58.0",
"version": "2.59.0",
"type": "module",
"description": "ZenNotes web client for self-hosted and hosted deployments",
"homepage": "https://zennotes.org",
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-monorepo",
"private": true,
"version": "2.58.0",
"version": "2.59.0",
"description": "ZenNotes monorepo for desktop, web, and self-hosted server builds",
"packageManager": "npm@10.9.2",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion packages/app-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/app-core",
"private": true,
"version": "2.58.1",
"version": "2.59.0",
"type": "module",
"exports": {
"./main": "./src/main.tsx",
Expand Down
5 changes: 5 additions & 0 deletions packages/app-core/src/lib/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,11 @@ export const HELP_CLI: HelpCard[] = [
body:
'Builds with the Go terminal tool include `zn tui`. Update and open ZenNotes once to upgrade an existing desktop-managed CLI; keep using the same `zn` commands. Settings shows the installed terminal version and offers Repair if an upgrade needs attention. Desktop-installed commands keep following the desktop vault, while the TUI remembers its own selection. Explicit `--vault` and `--server` flags still win. Set `ZENNOTES_WORKSPACE_SOURCE=terminal` to use the terminal default for a command, or `ZENNOTES_CLI_ENGINE=legacy` to run the previous CLI during the transition. Homebrew and manual installations stay managed by their own installer. A shortcut left behind by a moved Mac app or an old AppImage can be repaired from Settings: review the old target, replacement and backup path before choosing Repair shortcut. Note saves preserve creation dates in small files under `.zennotes/note-metadata` without changing Markdown; keep the `.zennotes` folder with vault backups. Explicit legacy rollback needs the original app resources to remain available.'
},
{
title: 'CLI 0.6.0 in desktop 2.59.0',
body:
'On macOS and Linux, desktop 2.59.0 bundles CLI 0.6.0. Run `zn` in an interactive terminal or `zn tui` to open the editor. It includes editable settings, restored splits and sessions, comments, attachments, templates, bulk actions, and cross-note yank and paste. Use `:version` in the TUI for a persistent report and press `c` or `y` to copy it. `zn status`, `zn doctor`, `zn config`, and `zn completion` help with setup and scripts; `zn server` manages native local servers, also available through `:servers`. Run `zn update --check` to check the CLI release; a desktop-managed CLI updates with the desktop app. The full command reference is at https://zennotes.org/tui/docs.'
},
{
title: 'No app required',
body:
Expand Down
41 changes: 24 additions & 17 deletions packages/app-core/src/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,31 +87,38 @@ function installZen(overrides: Record<string, unknown> = {}): void {
})
}

// The most recent store module a test loaded. `vi.resetModules` gives every
// test a fresh module, but timers the OLD module already scheduled keep
// running: `updateNoteBody` debounces a `persistNote` at 350ms
// (pathSaveTimers), and database edits debounce a write at 400ms
// (databaseSaveTimers). A test that schedules either without awaiting it
// leaves a straggler that fires against whichever `window.zen` spy is
// installed one or two tests later; a partial mock there turns the write
// into `undefined.catch`, which is an unhandled error that fails the whole
// run (seen on the macOS runner). The cure is the afterEach below: it
// starves both timers' bail-out checks on the old store, so a stray
// persistNote finds nothing dirty and a stray database write finds no doc,
// and each returns before touching `window.zen`.
let lastLoadedStore: { useStore: { setState: (partial: object) => void } } | null = null
// Every store module the running test loaded (a relaunch test loads two).
// `vi.resetModules` gives every test a fresh module, but timers the OLD
// module already scheduled keep running: `updateNoteBody` debounces a
// `persistNote` at 350ms (pathSaveTimers), database edits debounce a write at
// 400ms (databaseSaveTimers), and opening a vault refreshes its assets 2s
// later (scheduleAssetsRefreshForVault). A test that schedules any of them
// without awaiting it leaves a straggler that fires against whichever
// `window.zen` spy is installed tests later; a partial mock there turns the
// write into `undefined.catch`, which is an unhandled error that fails the
// whole run (seen on the macOS runner). The asset refresh can also outlive
// the file: on a slow runner it fired after vitest had torn down the jsdom
// environment, and `window is not defined` failed the Windows build. The cure
// is the afterEach below: it starves every timer's bail-out check on the old
// stores, so a stray persistNote finds nothing dirty, a stray database write
// finds no doc and a stray asset refresh finds its vault closed, and each
// returns before touching `window`.
type LoadedStoreModule = { useStore: { setState: (partial: object) => void } }
const loadedStores = new Set<LoadedStoreModule>()

async function loadStore() {
vi.resetModules()
localStorage.clear()
const mod = await import('./store')
lastLoadedStore = mod as unknown as typeof lastLoadedStore
loadedStores.add(mod as unknown as LoadedStoreModule)
return mod
}

afterEach(() => {
lastLoadedStore?.useStore.setState({ noteDirty: {}, databases: {} })
lastLoadedStore = null
for (const loaded of loadedStores) {
loaded.useStore.setState({ noteDirty: {}, databases: {}, vault: null })
}
loadedStores.clear()
})

type LoadedStore = Awaited<ReturnType<typeof loadStore>>['useStore']
Expand Down Expand Up @@ -842,7 +849,7 @@ describe('per-note panels survive a restart (#794)', () => {
async function relaunch() {
vi.resetModules()
const mod = await import('./store')
lastLoadedStore = mod as unknown as typeof lastLoadedStore
loadedStores.add(mod as unknown as LoadedStoreModule)
return mod
}

Expand Down
2 changes: 1 addition & 1 deletion packages/bridge-contract/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/bridge-contract",
"private": true,
"version": "2.58.1",
"version": "2.59.0",
"type": "module",
"exports": {
"./bridge": "./src/bridge.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-domain/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/shared-domain",
"private": true,
"version": "2.58.1",
"version": "2.59.0",
"type": "module",
"exports": {
"./*": "./src/*.ts"
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/shared-ui",
"private": true,
"version": "2.58.0",
"version": "2.59.0",
"type": "module",
"exports": {
".": "./src/index.ts"
Expand Down
Loading