Skip to content
This repository was archived by the owner on Feb 27, 2026. It is now read-only.

Commit 883365c

Browse files
committed
chore(release): 0.8.4
1 parent 249c0a4 commit 883365c

File tree

13 files changed

+935
-152
lines changed

13 files changed

+935
-152
lines changed

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ OCR_LOG_LEVEL=
6565
# If unset, the server uses a temp directory.
6666
PADDLEOCR_HOME=
6767

68+
# Redis (required for BullMQ reminder scheduling and optional Yjs pub/sub bridge)
69+
REDIS_HOST=
70+
REDIS_PORT=
71+
REDIS_PASSWORD=
72+
73+
# Reminder worker runs in same Node process and consumes BullMQ jobs.
74+
# true => process delayed reminder jobs and send push notifications
75+
# false => do not start worker in this instance
76+
ENABLE_REMINDER_WORKER=
77+
78+
# Optional: cross-instance Yjs update propagation using Redis Pub/Sub.
79+
# true => enable yjs:doc:* publish/subscribe bridge
80+
# false => single-instance mode (default behavior)
81+
ENABLE_REDIS_PUBSUB=
82+
6883
# Notes:
6984
# - `.env.example` must never contain real secret values. Keep this file in source control.
7085
# - Put real secrets in `.env` (which is ignored) or in your deployment environment.

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ Adheres to Semantic Versioning (MAJOR.MINOR.PATCH).
1515
### Fixed
1616
-
1717

18+
## [0.8.4] - 2026-02-18
19+
20+
### Added
21+
- Redis-backed reminder queue architecture using BullMQ (`server/src/lib/queue.ts`, `server/src/workers/reminderWorker.ts`, `server/src/lib/redis.ts`) with delayed jobs, retry/backoff, startup resync, and graceful shutdown handling.
22+
- Optional Redis Pub/Sub bridge for cross-instance Yjs propagation (`server/src/lib/yjsRedisBridge.ts`) with instance tagging and rebroadcast loop prevention.
23+
- New environment configuration for Redis and feature toggles in `.env.example`:
24+
- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`
25+
- `ENABLE_REMINDER_WORKER`
26+
- `ENABLE_REDIS_PUBSUB`
27+
28+
### Changed
29+
- Server startup now initializes reminder queue state from DB and conditionally starts the in-process reminder worker in `server/src/index.ts`.
30+
- Reminder scheduling flow moved from polling to queue-driven execution; note create/update/archive/trash/delete lifecycle paths now keep reminder jobs in sync (`server/src/notes.ts`).
31+
- Reminder resync now includes pending due reminders (including past-due unsent reminders), improving restart/downtime recovery behavior.
32+
- Legacy polling module `server/src/reminderPushJob.ts` is retained as a deprecated no-op compatibility shim.
33+
- Release metadata/version bumped to `0.8.4`.
34+
35+
### Fixed
36+
- Reduced unnecessary DB writes during note-card drag/layout and idle states:
37+
- deduplicated order persistence writes in `client/src/components/NotesGrid.tsx`,
38+
- disabled per-card realtime Yjs sessions in `client/src/components/NoteCard.tsx` (editor realtime remains active).
39+
- Reduced Yjs persistence write pressure by debouncing DB snapshot writes in `server/src/index.ts` while preserving realtime websocket collaboration.
40+
- Removed file-based Prisma DB event logging to `db.log` and restored normal environment-based Prisma log output in `server/src/prismaClient.ts`.
41+
1842
## [0.8.3] - 2026-02-16
1943

2044
### Added

client/src/components/NoteCard.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,12 +1005,18 @@ export default function NoteCard({
10051005
};
10061006
}, [token, viewerCollections, neededCollectionIdsKey, collectionPathById]);
10071007
// Subscribe to Yjs checklist for live card updates
1008+
// IMPORTANT: card previews should not establish per-card collaborative sessions.
1009+
// Creating a websocket + Y.Doc for every visible card causes heavy DB churn
1010+
// (server persists yData on Yjs updates), especially during drag/re-layout.
1011+
// Full realtime collaboration remains in editors.
1012+
const enableCardRealtimeCollab = false;
10081013
const ydoc = React.useMemo(() => new Y.Doc(), [note.id]);
10091014
const collabRoom = React.useMemo(() => noteCollabRoomFromNote(note), [note.id, (note as any)?.createdAt]);
10101015
const providerRef = React.useRef<WebsocketProvider | null>(null);
10111016
const yarrayRef = React.useRef<Y.Array<Y.Map<any>> | null>(null);
10121017

10131018
React.useEffect(() => {
1019+
if (!enableCardRealtimeCollab) return;
10141020
let disposed = false;
10151021
let cleanup: (() => void) | null = null;
10161022

@@ -1028,9 +1034,10 @@ export default function NoteCard({
10281034
disposed = true;
10291035
try { cleanup && cleanup(); } catch {}
10301036
};
1031-
}, [collabRoom, ydoc]);
1037+
}, [collabRoom, ydoc, enableCardRealtimeCollab]);
10321038

10331039
React.useEffect(() => {
1040+
if (!enableCardRealtimeCollab) return;
10341041
const room = collabRoom;
10351042
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
10361043
const serverUrl = `${proto}://${window.location.host}/collab`;
@@ -1054,7 +1061,7 @@ export default function NoteCard({
10541061
yarr.observeDeep(updateFromY);
10551062
provider.on('sync', (isSynced: boolean) => { if (isSynced) updateFromY(); });
10561063
return () => { try { yarr.unobserveDeep(updateFromY as any); } catch {}; try { provider.destroy(); } catch {}; };
1057-
}, [collabRoom, ydoc]);
1064+
}, [collabRoom, ydoc, enableCardRealtimeCollab]);
10581065

10591066
React.useEffect(() => {
10601067
const onUploadSuccess = (evt: Event) => {
@@ -1083,6 +1090,7 @@ export default function NoteCard({
10831090

10841091
// Subscribe to Yjs text doc for real-time HTML preview on cards (TEXT notes)
10851092
React.useEffect(() => {
1093+
if (!enableCardRealtimeCollab) { setRtHtmlFromY(null); return; }
10861094
if (note.type !== 'TEXT') { setRtHtmlFromY(null); return; }
10871095
let ed: Editor | null = null;
10881096
try {
@@ -1124,7 +1132,7 @@ export default function NoteCard({
11241132
} catch {
11251133
// ignore editor init failures
11261134
}
1127-
}, [note.id, note.type, ydoc]);
1135+
}, [note.id, note.type, ydoc, enableCardRealtimeCollab]);
11281136
// Render a minimal formatted HTML preview from TipTap JSON stored in note.body
11291137
function bodyHtmlPreview(): string {
11301138
const raw = textBody || '';

client/src/components/NotesGrid.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ export default function NotesGrid({
563563
const swapCandidateIdRef = useRef<number | null>(null);
564564
const swapDwellTimerRef = useRef<number | null>(null);
565565
const swapAnimColsRef = useRef<null | { section: 'pinned' | 'others'; heights: number[] }>(null);
566+
const orderPersistSigRef = useRef<string>('');
566567

567568
function getAnimMs(kind: 'resize'|'swap'|'rearrange') {
568569
try {
@@ -3218,9 +3219,19 @@ export default function NotesGrid({
32183219

32193220
async function persistOrder(currentNotes: any[]) {
32203221
// always save locally
3221-
const pinnedIds = currentNotes.filter((n: any) => !!(n as any)?.pinned).map((n: any) => n.id);
3222-
const otherIds = currentNotes.filter((n: any) => !(n as any)?.pinned).map((n: any) => n.id);
3222+
const pinnedIds = currentNotes
3223+
.filter((n: any) => !!(n as any)?.pinned)
3224+
.map((n: any) => Number((n as any)?.id))
3225+
.filter((id: number) => Number.isFinite(id));
3226+
const otherIds = currentNotes
3227+
.filter((n: any) => !(n as any)?.pinned)
3228+
.map((n: any) => Number((n as any)?.id))
3229+
.filter((id: number) => Number.isFinite(id));
32233230
const ids = [...pinnedIds, ...otherIds];
3231+
const sig = ids.join(',');
3232+
if (!sig) return;
3233+
if (orderPersistSigRef.current === sig) return;
3234+
orderPersistSigRef.current = sig;
32243235
try { localStorage.setItem('notesOrder', JSON.stringify(ids)); } catch (e) {}
32253236
// attempt server persistence if authenticated
32263237
if (!token) return;

0 commit comments

Comments
 (0)