test(temporary-notes): cover the create race-guard, broadcast deadline + cache patch; unify page->tree-node mappers
Address review comment 2159 on the temporary-notes UI work. Tests: - tree-model: cover handleCreate's race-guard temporaryExpiresAt patch — (a) server node inserted WITHOUT a deadline + create response carries one => node gains the deadline; (b) node already has a deadline => not overwritten, prev returned by reference. - ws-tree.service.spec: broadcastPageCreated now asserts the deadline is carried when present and pinned to null (`?? null`) when absent. - page-embed-query (new spec): syncTemporaryExpiresInCache patches the in-tree node's temporaryExpiresAt, and leaves the atom value at the same reference when the id is absent from the loaded tree (no write). Refactor (closes the drift bug-class at the root): - Client: extract one canonical pageToTreeNode(page, overrides) mapper in tree/utils and route buildTree, handleCreate's optimistic insert, the restore mutation and the duplicate handler through it. Restore stays permanent (server nulls temporaryExpiresAt) and duplicate stays permanent (server arms no timer) — both now reflect the server without a reload, where before they dropped the field entirely. - Server: extract one toTreeNodeSnapshot(page) helper called by both the PAGE_CREATED event enrichment (page.repo) and the addTreeNode broadcast (ws-tree.service), so the optional temporaryExpiresAt can't drift between the two literals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,36 @@ export interface TreeNodeSnapshot {
|
||||
temporaryExpiresAt?: Date | string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single canonical builder for a `TreeNodeSnapshot` from a page-like row. Both
|
||||
* the `PAGE_CREATED` event enrichment (`page.repo.insertPage`) and the
|
||||
* `addTreeNode` broadcast (`WsTreeService.broadcastPageCreated`) build this same
|
||||
* snapshot; routing both through here keeps the optional `temporaryExpiresAt`
|
||||
* (and the `?? null` normalisation that pins a permanent note to an explicit
|
||||
* null) from silently drifting between the two literals.
|
||||
*/
|
||||
export function toTreeNodeSnapshot(page: {
|
||||
id: string;
|
||||
slugId: string;
|
||||
title: string | null;
|
||||
icon: string | null;
|
||||
position: string;
|
||||
spaceId: string;
|
||||
parentPageId: string | null;
|
||||
temporaryExpiresAt?: Date | string | null;
|
||||
}): TreeNodeSnapshot {
|
||||
return {
|
||||
id: page.id,
|
||||
slugId: page.slugId,
|
||||
title: page.title,
|
||||
icon: page.icon,
|
||||
position: page.position,
|
||||
spaceId: page.spaceId,
|
||||
parentPageId: page.parentPageId,
|
||||
temporaryExpiresAt: page.temporaryExpiresAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export class PageEvent {
|
||||
pageIds: string[];
|
||||
workspaceId: string;
|
||||
|
||||
@@ -16,7 +16,10 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { EventName } from '../../../common/events/event.contants';
|
||||
import { TreeUpdateSnapshot } from '../../listeners/page.listener';
|
||||
import {
|
||||
TreeUpdateSnapshot,
|
||||
toTreeNodeSnapshot,
|
||||
} from '../../listeners/page.listener';
|
||||
|
||||
/**
|
||||
* Optional extras for the PAGE_UPDATED event emitted by updatePage(s). Lets the
|
||||
@@ -200,20 +203,10 @@ export class PageRepo {
|
||||
this.eventEmitter.emit(EventName.PAGE_CREATED, {
|
||||
pageIds: [result.id],
|
||||
workspaceId: result.workspaceId,
|
||||
pages: [
|
||||
{
|
||||
id: result.id,
|
||||
slugId: result.slugId,
|
||||
title: result.title,
|
||||
icon: result.icon,
|
||||
position: result.position,
|
||||
spaceId: result.spaceId,
|
||||
parentPageId: result.parentPageId,
|
||||
// Carry the death-timer deadline so a note created as temporary shows
|
||||
// its sidebar clock marker on every client without a reload.
|
||||
temporaryExpiresAt: result.temporaryExpiresAt,
|
||||
},
|
||||
],
|
||||
// Built via the shared snapshot helper so the field copy (and the
|
||||
// death-timer deadline that shows the sidebar clock marker without a
|
||||
// reload) can't drift from the `addTreeNode` broadcast literal.
|
||||
pages: [toTreeNodeSnapshot(result)],
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user