mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
b253a2be6f
Final review round 3, on the test suite as a deliverable. The panel had no producer-to-host test: the strip's tests mock the event bus, the panel host's tests emit on it directly, and between them a broken hostToken thread through ItemDetail would have passed everything. Verified by breaking that thread — the new browser test fails, the whole unit suite stays green. It is also the only place DR-12's "activates exactly once per key press" can be demonstrated at all: jsdom does not synthesise a button's activation click, so the unit test could only ever prove the narrower "no hand-rolled handler races the UA click". That test is renamed to claim exactly that, with a pointer to where the real one lives. Also folds the workspace into the host-address reader. It was captured once in the Tiptap options while the URL builder stayed live, so a mounted chip surviving a pane workspace switch would probe under the PREVIOUS workspace's key — a cross-workspace answer, cached under the wrong key. Same staleness class as the item id, one axis over. This incidentally makes the image extension's `address` option load-bearing rather than the dead plumbing the review flagged: its probes read the live workspace through it now. isAddressable deliberately takes only the two ROUTING fields — the workspace rides along for cache keying and says nothing about whether an event can find its host.
76 lines
3.4 KiB
TypeScript
76 lines
3.4 KiB
TypeScript
/**
|
|
* The address a Tiptap attachment NodeView stamps on the events it emits
|
|
* (PLAN-2392 DR-8), and why it is a FUNCTION rather than two strings.
|
|
*
|
|
* DR-8 needs two facts at emit time: which item the editor is editing, and
|
|
* which `ItemDetail` mount owns it (a master pane and a peeked pane are both
|
|
* mounted, so `itemId` alone would let both hosts consume one NodeView's
|
|
* event). The obvious shape is two string options set at `configure()` time.
|
|
*
|
|
* That shape is a trap here, for two independent reasons:
|
|
*
|
|
* 1. **The comment composer outlives the item.** `CommentEditor` is
|
|
* deliberately reused across a no-`{#key}` item switch — its `itemId` prop
|
|
* just changes — so a value captured when its extensions were configured
|
|
* goes stale, and its chips would emit events addressed to the PREVIOUS
|
|
* item. The host matches on both fields and would correctly ignore them:
|
|
* a tap that silently does nothing.
|
|
*
|
|
* 2. **You cannot fix that by writing to the options.** Tiptap's `options` is
|
|
* a GETTER that returns a fresh spread on every access
|
|
* (`@tiptap/core@3.22.5`, `dist/index.cjs:3452`), so `ext.options.itemId =
|
|
* next` mutates a temporary that is discarded on the next line. The
|
|
* assignment looks like it works and does nothing. (`optionsAreASnapshot`
|
|
* in the sibling test pins this, so a future Tiptap bump that changes it
|
|
* is a visible test failure rather than a silent invitation to go back to
|
|
* mutating.)
|
|
*
|
|
* So the option is a reader the host supplies once and keeps honest: a closure
|
|
* over its own live props. Called at emit time, it is always current, for a
|
|
* remounted host (the body editor, re-keyed per item) and a reused one (the
|
|
* composer) alike — one shape, no per-host special case.
|
|
*/
|
|
|
|
export interface AttachmentHostAddress {
|
|
/**
|
|
* Workspace the editor is currently in. Read through the reader for the
|
|
* same reason as the other two: the pane switches workspace without
|
|
* remounting, and this value keys the attachment metadata CACHE — a stale
|
|
* one makes a mounted chip probe under the previous workspace's key, which
|
|
* is a cross-workspace answer to a question about this one.
|
|
*/
|
|
workspaceSlug: string;
|
|
/** UUID of the item being edited. Empty when there is no item context. */
|
|
itemId: string;
|
|
/** Identity of the `ItemDetail` mount that owns this editor. */
|
|
hostToken: string;
|
|
}
|
|
|
|
/** Reads the CURRENT address. Called at emit time, never cached by callers. */
|
|
export type AttachmentHostAddressReader = () => AttachmentHostAddress;
|
|
|
|
/** The no-context address: an editor with no host cannot address a panel. */
|
|
export const UNADDRESSED: AttachmentHostAddress = {
|
|
workspaceSlug: '',
|
|
itemId: '',
|
|
hostToken: '',
|
|
};
|
|
|
|
/** Default option value — an editor mounted without a host addresses nothing. */
|
|
export const readUnaddressed: AttachmentHostAddressReader = () => UNADDRESSED;
|
|
|
|
/**
|
|
* Whether an address can reach a host at all. Both halves are required: a
|
|
* missing token would make the event ambiguous between concurrently-mounted
|
|
* hosts, which is the exact failure DR-8 exists to prevent.
|
|
*
|
|
* Deliberately takes only the two ROUTING fields, not a whole address: the
|
|
* workspace is carried alongside them for cache keying and says nothing about
|
|
* whether an event can find its host.
|
|
*/
|
|
export function isAddressable(
|
|
address: Pick<AttachmentHostAddress, 'itemId' | 'hostToken'> | null | undefined
|
|
): boolean {
|
|
return Boolean(address?.itemId && address?.hostToken);
|
|
}
|