Files
pad/web/src/lib/attachments/hostAddress.ts
T
xarmian b253a2be6f test(attachments): cover the wiring the unit suites structurally cannot
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.
2026-08-04 19:50:22 +00:00

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);
}