@aglyn/besigner-ui 1.0.0-beta.162 → 1.0.0-beta.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/besigner-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.164",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.
|
|
30
|
-
"@aglyn/aglyn-node-renderer": "1.0.0-beta.
|
|
31
|
-
"@aglyn/besigner": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-svg-icons": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-ui-json-editor": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-dom": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.164",
|
|
29
|
+
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.164",
|
|
30
|
+
"@aglyn/aglyn-node-renderer": "1.0.0-beta.164",
|
|
31
|
+
"@aglyn/besigner": "1.0.0-beta.164",
|
|
32
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.164",
|
|
33
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.164",
|
|
34
|
+
"@aglyn/shared-data-types": "1.0.0-beta.164",
|
|
35
|
+
"@aglyn/shared-svg-icons": "1.0.0-beta.164",
|
|
36
|
+
"@aglyn/shared-ui-json-editor": "1.0.0-beta.164",
|
|
37
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.164",
|
|
38
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.164",
|
|
39
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.164",
|
|
40
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.164",
|
|
41
|
+
"@aglyn/shared-util-dom": "1.0.0-beta.164",
|
|
42
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.164",
|
|
43
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.164",
|
|
44
44
|
"@dnd-kit/core": "^6.3.1",
|
|
45
45
|
"@dnd-kit/utilities": "^3.2.2",
|
|
46
46
|
"@react-aria/utils": "^3.34.1",
|
|
@@ -189,6 +189,23 @@ export interface BesignerDraftAuthor {
|
|
|
189
189
|
export interface UseBesignerDocumentResult {
|
|
190
190
|
/** True when the canvas differs from the last agreed state. */
|
|
191
191
|
saveAvailable: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* True when the canvas is exactly what {@link saveWorkingDraft} last
|
|
194
|
+
* stored — the work is safe, even though {@link saveAvailable} is still
|
|
195
|
+
* true because the VERSION document deliberately does not have it
|
|
196
|
+
* (AGL-3271).
|
|
197
|
+
*
|
|
198
|
+
* On the version a site is serving, Save draft writes the draft beside the
|
|
199
|
+
* version and never the version itself, so `saveAvailable` can never go
|
|
200
|
+
* false there and a toolbar reading it alone offers `Save draft` forever —
|
|
201
|
+
* over work that has already saved, with the publish it is waiting for
|
|
202
|
+
* reachable only from a menu. Read the two together: unsaved, saved but
|
|
203
|
+
* not live, live.
|
|
204
|
+
*
|
|
205
|
+
* Goes false again on the author's next edit, and on a document that has
|
|
206
|
+
* no working draft it is never true.
|
|
207
|
+
*/
|
|
208
|
+
workingDraftSaved: boolean;
|
|
192
209
|
/** True when someone else wrote this document since we loaded it. */
|
|
193
210
|
remoteChanged: boolean;
|
|
194
211
|
handleSave: () => Promise<void> | void;
|
|
@@ -448,11 +448,24 @@ const noopNotify = ()=>undefined;
|
|
|
448
448
|
// unconfirmed baseline always reads dirty (AGL-1262), so "clean" here
|
|
449
449
|
// means the store has confirmed exactly this tree.
|
|
450
450
|
if (!saveAvailable) return 'unchanged';
|
|
451
|
+
// What is SENT is what the draft will hold. An author who keeps typing
|
|
452
|
+
// through the round trip has work this write does not carry, and
|
|
453
|
+
// recording the canvas as it stands when the promise resolves would
|
|
454
|
+
// call that work stored (AGL-3271).
|
|
455
|
+
const written = canvas.toJSON().nodes;
|
|
451
456
|
return writeServerDraft(firestore, draftIds, {
|
|
452
|
-
nodes:
|
|
457
|
+
nodes: written,
|
|
453
458
|
baseStamp: versionStamp(updatedAt),
|
|
454
459
|
updatedByUid: (_ref = author == null ? void 0 : author.uid) != null ? _ref : null,
|
|
455
460
|
updatedByEmail: (_ref1 = author == null ? void 0 : author.email) != null ? _ref1 : null
|
|
461
|
+
}).then((result)=>{
|
|
462
|
+
// ONLY on a write, so this moves in lockstep with the editor's own
|
|
463
|
+
// `draftPending` — the two are read together to decide the button's
|
|
464
|
+
// label, and a checkpoint recorded on a branch that leaves
|
|
465
|
+
// `draftPending` false would let a document with an unpublished
|
|
466
|
+
// draft read as "Up to date".
|
|
467
|
+
if (result === 'written') canvas.updateDraftedNodes(written);
|
|
468
|
+
return result;
|
|
456
469
|
}).catch(()=>'failed');
|
|
457
470
|
}, [
|
|
458
471
|
options.firestore,
|
|
@@ -480,6 +493,7 @@ const noopNotify = ()=>undefined;
|
|
|
480
493
|
}, []);
|
|
481
494
|
return {
|
|
482
495
|
saveAvailable,
|
|
496
|
+
workingDraftSaved: canvas.isDraftedSame,
|
|
483
497
|
remoteChanged,
|
|
484
498
|
handleSave,
|
|
485
499
|
saveWorkingDraft,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/hooks/use-besigner-document.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Firestore } from 'firebase/firestore'\nimport type * as Aglyn from '@aglyn/aglyn'\nimport {\n canvas,\n ConcurrentEditError,\n ensureCanvasRoot,\n formatBytes,\n hasConcurrentWrite,\n HostViewType,\n incorporatesStoredNodes,\n measureNodeMap,\n versionStamp,\n} from '@aglyn/aglyn'\nimport isEqual from 'lodash-es/isEqual.js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport * as Besigner from '@aglyn/besigner'\nimport {\n type BesignerDraftIds,\n clearBesignerDraft,\n} from '../drafts/besigner-draft-store'\nimport {\n type ServerDraftWrite,\n clearServerDraft,\n writeServerDraft,\n} from '../drafts/besigner-server-draft'\nimport {\n type BesignerDraftState,\n useBesignerDraft,\n} from './use-besigner-draft'\n\n/**\n * How the host application surfaces a message. Deliberately a plain\n * callback rather than a snackbar dependency: the console passes its\n * `enqueueSnackbar`, and an embedder outside the console passes whatever it\n * has (a toast, a console.log, nothing at all).\n */\nexport type BesignerNotify = (\n message: string,\n options?: {\n variant?: 'info' | 'success' | 'warning' | 'error'\n /**\n * Presentation hints, passed through rather than derived from `variant`\n * because the console's four editors deliberately mix them — a\n * near-limit warning is transient, a concurrent-edit warning is not.\n * An embedder without these concepts can ignore them.\n */\n persist?: boolean\n allowDuplicate?: boolean\n },\n) => void\n\n/**\n * Begins a loading indication and returns the function that ends it. Same\n * contract as the console's `queueLoading`; defaults to a no-op.\n */\nexport type BesignerQueueLoading = () => () => void\n\n/**\n * What this editor believes the stored document looked like when it last\n * agreed with it, handed to `save` so a store can make the write\n * conditional (AGL-1301) — a Firestore provider runs the same comparison\n * inside a transaction, closing the window where a save lands between\n * another writer's commit and the local snapshot's delivery.\n */\nexport interface BesignerSaveBaseline {\n /** `versionStamp(...)` of the stored `updatedAt` at load/last agreement. */\n baseStamp: string | null\n /** The stored nodes as of that agreement, in STORED shape. */\n baseNodes: Aglyn.ProcessableNodes | undefined\n}\n\nexport interface BesignerDocumentSource<TData = unknown> {\n /** The stored node map, or undefined while loading. */\n nodes: Aglyn.ProcessableNodes | undefined\n /** Whatever the store stamps on write; used to detect concurrent edits. */\n updatedAt?: unknown\n /**\n * The snapshot `nodes` came from still carries writes this client made\n * that the store has NOT acknowledged (Firestore's\n * `snapshot.metadata.hasPendingWrites`).\n *\n * It decides whether the loaded document may be adopted as the *saved*\n * state. It may not: a queued write is replayed into the first snapshot\n * after a reload, so believing it would mean recording work the server\n * never took as \"already saved\" — after which the editor is clean, Save is\n * dead, and the canvas and the document disagree in silence (AGL-1262).\n * Omitted (undefined) reads as confirmed, which is right for every store\n * that has no such queue.\n */\n pendingWrites?: boolean\n status?: 'idle' | 'loading' | 'success' | 'error'\n error?: { message?: string } | null\n /**\n * Persists the node map. Rejecting surfaces as an error notification —\n * except `ConcurrentEditError`, which surfaces through the same refusal\n * UX as the listener-side guard (AGL-1301). `baseline` is what this\n * editor last agreed the stored document looked like; a store that can\n * make the write conditional should (see `saveNodesGuarded`), and one\n * that cannot may ignore it.\n */\n save: (\n nodes: Record<string, unknown>,\n baseline?: BesignerSaveBaseline,\n ) => Promise<void>\n data?: TData\n}\n\nexport interface UseBesignerDocumentOptions<TData = unknown>\n extends BesignerDocumentSource<TData> {\n /**\n * The word used in user-facing copy — 'screen', 'layout', 'component',\n * 'template', 'email'. Keeps each editor's messages accurate without\n * forking the logic.\n */\n noun: string\n /** Canvas view type; reset to SCREEN on unmount. */\n viewType?: Aglyn.HostViewType\n /**\n * Identity of the open document. Changing it re-arms the canvas reset,\n * which is what stops one document's nodes leaking into the next.\n */\n documentKey?: string\n /**\n * Identity for the local crash-recovery draft (AGL-1256). Omit and drafts\n * are simply off for this editor.\n *\n * Deliberately separate from `documentKey`, which is not unique enough to\n * key storage on: both email editors build it as\n * `${templateKey}:${versionId}`, so a platform template and every host's\n * override of the same key produce the same string.\n */\n draft?: BesignerDraftIds\n /**\n * Enables the SHARED working draft — the one `Save draft` writes and\n * `Save & publish` clears. Omit and the editor keeps the local crash net\n * alone, which is what a document with no versions to hang a draft off\n * should do.\n */\n firestore?: Firestore\n /**\n * Other live editing sessions in this document's room, or null while\n * presence has not settled — see `recoverableRoomSessions` (AGL-2486).\n *\n * Only the crash-recovery prompt reads it. The conflict guard deliberately\n * does NOT: a room is a live-collaboration fact and saving must be\n * protected whether or not presence is working at all.\n */\n roomSessions?: number | null\n notify?: BesignerNotify\n queueLoading?: BesignerQueueLoading\n /** Called after a successful save, for activity logging. */\n onSaved?: () => void\n /**\n * Called when a save was REFUSED or FAILED — the document does not hold the\n * canvas.\n *\n * `onSaved` not firing is not enough to tell a caller that, because it also\n * stays silent for \"nothing to save\", where the document already holds the\n * canvas and every follow-on step is safe. The two need opposite handling by\n * anything that acts on a save: `handleSaveAndPublish` promotes the version\n * pointer afterwards, and promoting past a refusal publishes a version whose\n * document never received the edit.\n *\n * Fired for every path that leaves the stored tree unchanged against the\n * author's intent: a stale-baseline conflict, an unpreparable canvas, the\n * size guard, and a thrown write. Each has already explained itself in a\n * toast, so a caller's job is only to stop.\n */\n onSaveRefused?: () => void\n /**\n * Overrides the success message. Defaults to `\"<Noun> saved successfully\"`;\n * templates say simply \"Template saved\" and that copy is preserved rather\n * than normalised by the extraction.\n */\n savedMessage?: string\n /**\n * Maps the stored node map into what the canvas should render, for\n * documents whose storage shape is not a canvas tree.\n *\n * Reusable components and component-kind templates store a *definition*,\n * rooted at the promoted node rather than the canvas root — rendering one\n * unwrapped gives a canvas with no root and a blank editor (AGL-680).\n * Defaults to identity; putting a document back into its stored shape is\n * the caller's `save`, which already owns that direction (AGL-681).\n */\n toCanvasNodes?: (\n nodes: Aglyn.ProcessableNodes,\n ) => Aglyn.ProcessableNodes\n /**\n * The inverse of `toCanvasNodes`: maps the canvas tree back into the\n * stored shape, and may refuse the save with a message.\n *\n * Runs *before* the size guard, so the guard measures what will actually\n * be written rather than the canvas representation of it. A component-kind\n * template that no longer has a single top-level element is rejected here\n * (AGL-681).\n */\n fromCanvasNodes?: (\n canvasNodes: Record<string, unknown>,\n ) => { nodes: Record<string, unknown> } | { error: string }\n}\n\n/** Who a shared working draft records as its last writer. */\nexport interface BesignerDraftAuthor {\n uid?: string | null\n email?: string | null\n}\n\nexport interface UseBesignerDocumentResult {\n /** True when the canvas differs from the last agreed state. */\n saveAvailable: boolean\n /** True when someone else wrote this document since we loaded it. */\n remoteChanged: boolean\n handleSave: () => Promise<void> | void\n /**\n * Writes the SHARED working draft — what `Save draft` stores and\n * `Save & publish` clears.\n *\n * Here rather than in each editor because of the one field an editor\n * cannot get right on its own: `baseStamp`. A recovered draft is measured\n * against `storedStamp`, which is this document's `updatedAt` — the\n * VERSION document — and every console editor was stamping its draft from\n * the PARENT document instead (`screens/{id}`, `components/{id}`, …). Two\n * documents, two `updatedAt` fields, so the two values could only agree by\n * coincidence: every working draft read back as \"someone else saved this\n * while you were editing\", Restore was never offered, and the draft could\n * never be recovered by anyone. Taking the stamp where the comparison is\n * made is what stops the two drifting apart again.\n *\n * Resolves `'failed'` when this document cannot hold a shared draft — no\n * `firestore`, or no draft ids — which is the same answer a rejected write\n * gives, and the caller's handling of both is identical.\n *\n * Resolves `'unchanged'` WITHOUT writing when the canvas is clean\n * (AGL-2874). A clean canvas is the stored document, so there is no draft\n * to take — and writing one anyway would replace whatever saved draft the\n * slot holds with a copy of the document it was drafted against.\n */\n saveWorkingDraft: (author?: BesignerDraftAuthor) => Promise<ServerDraftWrite>\n /**\n * Refuses, and says why, an action that would leave behind a saved draft\n * this author has been offered and has not opened (AGL-2874). Returns true\n * when the caller must stop.\n *\n * `'publish'` for Save & publish and a Publish click: publishing stores and\n * promotes the canvas, and a canvas that never took the draft in would go\n * live without it — after which the draft is cleared as published. `'save'`\n * for a draft save, which would overwrite it, and for a save of the\n * document, which moves the stamp the draft was taken against and leaves\n * it unopenable.\n *\n * Only while the offer is ON SCREEN and can be opened: the refusal points\n * at the banner's Open draft and Discard, and must never point at buttons\n * that are not there.\n */\n refuseOverUnopenedDraft: (action: 'save' | 'publish') => boolean\n /**\n * Announce a write this editor is about to make to the SAME document\n * outside `handleSave` — component properties are the first (AGL-1247).\n *\n * Without it the write's own echo is indistinguishable from a colleague's\n * and trips the conflict guard, which pauses saving until reload: the\n * editor accuses you of being someone else.\n */\n markOwnWrite: () => void\n /**\n * Unsaved work recovered from local storage after a crash or reload, and\n * the two things the author can do about it (AGL-1256). Always present;\n * `available` is false when there is nothing to offer.\n */\n draft: BesignerDraftState\n /** JSON editor plumbing, identical in every editor. */\n jsonOpen: boolean\n openJsonEditor: () => void\n closeJsonEditor: () => void\n handleJsonSave: (event: unknown, value: unknown) => void\n /**\n * The document could not be read AND there is nothing to show for it\n * (AGL-1066).\n *\n * Deliberately NOT \"the read errored\". Under `persistentLocalCache` a\n * refused listen still serves the document from IndexedDB, and a refused\n * listen can now reach `status: 'error'` — so an editor with a canvas full\n * of the author's work would otherwise be replaced by \"Not found\" about two\n * seconds into a stale session, mid-edit. Whether the console should keep\n * SERVING that cached document is the question AGL-1066 settled as \"keep\n * serving, but stop presenting it as live\"; the presentation half is the\n * shell's re-auth banner and the refusal on save, not blanking the canvas.\n *\n * When the read failed and nothing ever arrived there is genuinely nothing\n * to render, and this is true — which is the state the \"Not found\" branch\n * was written for.\n */\n hasError: boolean\n notFound: boolean\n /**\n * The document is on screen but the server has stopped answering for it\n * (AGL-1066) — cached content, no refresh coming. Saving is already refused\n * by the seed guards; this is what an editor would surface to say why.\n */\n staleContent: boolean\n}\n\nconst noopQueueLoading: BesignerQueueLoading = () => () => undefined\nconst noopNotify: BesignerNotify = () => undefined\n\n/**\n * Pushes a stored node map into the shared canvas singleton.\n *\n * The canvas root is guaranteed here rather than per editor (AGL-931). A map\n * without one leaves the hierarchy showing 'Invalid node' with Add Element\n * disabled — an editor the document can never be repaired from — and only\n * the two definition-shaped editors passed a `toCanvasNodes` that repaired\n * it. Doing it at the one point every editor loads through means no route\n * can be added that forgets.\n */\nexport function setLocalNodes(value: Aglyn.ProcessableNodes) {\n const parsed = canvas.processNodesToDenormalized(value)\n return canvas.setNodes(\n ensureCanvasRoot(parsed) as typeof parsed,\n )\n}\n\n/**\n * The document-agnostic half of a besigner editor.\n *\n * Everything here was copy-pasted across the console's four besigner routes\n * (screens, layouts, components, templates) — canvas lifecycle, first load,\n * concurrent-write detection (AGL-674), the size-guarded save (AGL-678) and\n * the JSON editor. None of it is about hosts, orgs, themes or publishing,\n * so none of it belongs in a console route.\n *\n * It has **no console and no host dependency** by design: the caller\n * supplies the document source and the notify/loading adapters. That is\n * what lets besigner drive a document that has no host — the platform email\n * templates — and, longer term, run outside the console entirely.\n */\nexport function useBesignerDocument<TData = unknown>(\n options: UseBesignerDocumentOptions<TData>,\n): UseBesignerDocumentResult {\n const {\n nodes,\n updatedAt,\n pendingWrites,\n status,\n error,\n save,\n noun,\n viewType,\n documentKey,\n draft: draftIds,\n roomSessions,\n notify = noopNotify,\n queueLoading = noopQueueLoading,\n onSaved,\n onSaveRefused,\n savedMessage,\n toCanvasNodes,\n fromCanvasNodes,\n } = options\n\n const saveAvailable = !canvas.isInitialSame\n const errored = Boolean(error) || status === 'error'\n // See `UseBesignerDocumentResult.hasError` — an errored read that still has\n // a document to show is stale, not missing (AGL-1066).\n const hasError = errored && !nodes\n const staleContent = errored && Boolean(nodes)\n /**\n * `!== 'loading'` rather than `=== 'success'` (AGL-1066).\n *\n * A refused read with nothing cached used to sit on `'loading'` forever and\n * now reaches `'error'`, so gating on `'success'` would have quietly stopped\n * recognising a missing document at the moment it started being reachable.\n * `hasError` covers the errored half either way; this keeps the two verdicts\n * from both going false and rendering an editor over nothing.\n */\n const notFound = status !== 'loading' && !nodes\n\n // The canvas is a singleton shared by every editing session; without a\n // reset on leave, client-side navigation to another document keeps (and\n // could save) this one's nodes.\n useEffect(() => {\n return () => {\n canvas.reset()\n Besigner.focus.clearFocusStatus()\n }\n }, [documentKey])\n\n // The view type decides which components the drawer offers (an email\n // document must not be able to insert a nav bar). Reset on leave so the\n // next session on the singleton canvas is unaffected.\n useEffect(() => {\n if (!viewType) return undefined\n if (!Besigner.doesBesignerAppExist()) return undefined\n const app = Besigner.getBesignerApp()\n Besigner.setBesignerFlag(app, { flag: 'viewType', value: () => viewType })\n return () => {\n Besigner.setBesignerFlag(app, {\n flag: 'viewType',\n value: () => HostViewType.SCREEN,\n })\n }\n }, [viewType])\n\n useEffect(() => {\n if (status === 'loading') return queueLoading()\n return undefined\n }, [status])\n\n // Conflict detection (AGL-674). Two people editing one version both write\n // the WHOLE node map, so without this the later save silently replaces the\n // earlier one and neither is told. `baseStamp` is what the document looked\n // like when this editor last agreed with it.\n const baseStampRef = useRef<string | null>(null)\n /**\n * The stored nodes as of the last agreement, in STORED shape. Kept\n * alongside the stamp because the stamp is an app-level FIELD a writer can\n * forget: admin scripts have updated `nodes` without touching `updatedAt`,\n * and such a write was invisible to the stamp guard and got clobbered\n * (AGL-1301). The web SDK exposes no server-side `updateTime` on a\n * snapshot, so a change in the content itself is the strongest signal a\n * client can key on.\n */\n const baseNodesRef = useRef<Aglyn.ProcessableNodes | undefined>(undefined)\n /**\n * What THIS editing session last wrote, held until the snapshot carrying\n * it arrives — so \"is this my own echo?\" is answered by evidence rather\n * than by a flag (AGL-2486).\n *\n * ## The hole this closes\n *\n * It used to be a boolean: save, set it, and adopt the NEXT moving\n * snapshot as ours whatever it contained. That is a bet on delivery order,\n * and the console runs `persistentMultipleTabManager`, so the tabs of one\n * browser share a cache and their snapshots coalesce. Two sessions saving\n * within a beat of each other therefore reach this:\n *\n * 1. this tab saves and arms the flag;\n * 2. another session commits before our echo is delivered;\n * 3. one snapshot arrives, carrying THEIR nodes;\n * 4. the flag consumes it — `remoteChanged` stays false, and both\n * baselines advance to their write.\n *\n * The second half is the damage. With the baseline advanced, the next\n * Save passes the client guard AND the AGL-1301 transaction — the stamp\n * and content it presents are the ones actually stored — so their work is\n * overwritten with no warning at either layer: the second save silently\n * replaces the first. Nothing about that needs the two writers to be\n * different people — it needs them to be different SESSIONS, which two tabs\n * of one account are.\n *\n * Comparing the content closes it without a schema change. A snapshot is\n * our echo only if it carries what we wrote; anything else is somebody\n * else's write and is flagged. Two sessions that saved IDENTICAL content\n * match and are adopted, which is correct — there is no work to lose\n * between two writes that agree.\n *\n * Note what stays untouched: the guard still reads no uid anywhere, so it\n * has never distinguished a colleague from your own other window, and now\n * refuses both alike. The server-side half is unchanged and is what makes\n * this safe — because the baseline no longer advances, the stale save is\n * refused inside the transaction as well.\n */\n const expectOwnWriteRef = useRef<{ nodes: unknown } | null>(null)\n const [remoteChanged, setRemoteChanged] = useState(false)\n\n useEffect(() => {\n if (nodes && !canvas.didSetInitial) {\n setLocalNodes(toCanvasNodes ? toCanvasNodes(nodes) : nodes)\n // The loaded document becomes the \"saved\" baseline ONLY when the store\n // has acknowledged it. A snapshot still carrying our own queued write\n // shows the work (never hide it) but records nothing as saved, so the\n // editor stays dirty and the work can still be written for real\n // (AGL-1262). Without this the editor adopts an unacknowledged edit as\n // the saved state and can never save again — not by re-editing, not by\n // restoring the draft, whose content is the same.\n canvas.updateInitialNodes(undefined, {\n confirmed: !pendingWrites,\n })\n baseStampRef.current = versionStamp(updatedAt)\n baseNodesRef.current = nodes\n return\n }\n // The baseline was recorded from a snapshot carrying our own queued\n // write, and that write has now been acknowledged (AGL-2486). Nothing\n // else ever re-confirms: the guard above runs once, so without this the\n // editor stays dirty for the rest of the session over a document nobody\n // has edited. `confirmInitialNodes` refuses if the canvas has moved on\n // since, so an author who edited in that window keeps their Save\n // (AGL-1262) — the store vouched for the earlier write, not for theirs.\n if (nodes && !pendingWrites && !canvas.isInitialConfirmed) {\n canvas.confirmInitialNodes()\n }\n }, [nodes, pendingWrites])\n\n /**\n * The canvas as the STORE would hold it — the same conversion `handleSave`\n * performs, so \"does my document contain theirs\" is asked in the shape the\n * answer has to be true in (AGL-2486).\n *\n * Null when the canvas cannot be expressed in that shape at all: a\n * component-kind document mid-edit that `fromCanvasNodes` refuses\n * (AGL-681) has no stored form to compare, and \"no answer\" must read as a\n * conflict rather than as agreement.\n */\n const storedShapeOfCanvas = useCallback((): Record<string, unknown> | null => {\n if (!canvas.didSetInitial) return null\n const canvasNodes = canvas.toJSON().nodes as Record<string, unknown>\n if (!fromCanvasNodes) return canvasNodes\n const prepared = fromCanvasNodes(canvasNodes)\n return 'error' in prepared ? null : prepared.nodes\n }, [fromCanvasNodes])\n\n useEffect(() => {\n const stored = versionStamp(updatedAt)\n const stampMoved = hasConcurrentWrite(baseStampRef.current, stored)\n // A writer that forgot the stamp still cannot hide: the content moved.\n const nodesMoved =\n nodes != null &&\n baseNodesRef.current != null &&\n !isEqual(nodes, baseNodesRef.current)\n if (!stampMoved && !nodesMoved) return\n const ownWrite = expectOwnWriteRef.current\n if (ownWrite && isEqual(nodes, ownWrite.nodes)) {\n // This is the echo of our own write landing — it carries what we sent.\n expectOwnWriteRef.current = null\n baseStampRef.current = stored\n baseNodesRef.current = nodes\n return\n }\n // Somebody else's write. Whether it is a CONFLICT is a different\n // question from whether the document moved (AGL-2486).\n //\n // Everyone collaborating should be able to save as they go, and the\n // mirror is what makes that safe: their changes reached this canvas node\n // by node before they pressed Save, so this session usually already holds\n // what they stored and its own write is a superset of it.\n // `incorporatesStoredNodes` is the\n // evidence for that — never an assumption that co-editing is healthy —\n // and a session that has fallen behind cannot satisfy it, which is the\n // case the guard exists for and still refuses.\n //\n // Advancing the baseline here is what lets the save through, and it is\n // safe because the store re-runs the AGL-1301 precondition against what\n // is ACTUALLY stored at commit time: anything that lands between this\n // decision and that commit moves `nodes` away from the baseline just\n // adopted, and the transaction refuses it. The promise that \"even a save\n // racing the conflict by milliseconds is refused\" is untouched — the\n // client simply stops refusing saves the server would have accepted.\n const ourNodes = storedShapeOfCanvas()\n if (incorporatesStoredNodes(baseNodesRef.current, nodes, ourNodes)) {\n expectOwnWriteRef.current = null\n baseStampRef.current = stored\n baseNodesRef.current = nodes\n setRemoteChanged(false)\n // …and when the canvas holds NOTHING beyond what was just stored,\n // there is nothing left to save. Without this the editor keeps\n // offering SAVE over a document it agrees with completely, which is\n // the second half of the same report: the mirror made the canvas\n // dirty against a baseline recorded at load, and their save is what\n // made that baseline stale rather than the canvas wrong. No argument,\n // so the canvas as it stands becomes the saved state.\n if (isEqual(ourNodes, nodes)) canvas.updateInitialNodes()\n return\n }\n // Our own expectation is void too: whatever it was waiting for either\n // lost the race or is behind this, and a later matching snapshot must\n // not be allowed to walk the baseline forward over a reported conflict.\n expectOwnWriteRef.current = null\n setRemoteChanged(true)\n }, [updatedAt, nodes, storedShapeOfCanvas])\n\n // Crash net (AGL-1256). Shares the conflict guard's stamp so the restore\n // prompt can tell \"your unsaved work\" from \"your unsaved work, and someone\n // else has saved since\".\n const draft = useBesignerDraft({\n ids: draftIds,\n // Given one, the shared working draft joins the local crash net and wins\n // when both exist (AGL-1152).\n firestore: options.firestore,\n loaded: canvas.didSetInitial,\n dirty: saveAvailable,\n storedStamp: versionStamp(updatedAt),\n storedNodes: nodes,\n roomSessions,\n })\n\n const unopenedDraftOnOffer =\n draft.available &&\n draft.origin === 'shared' &&\n draft.restoreBlockedBy === null\n\n // See `UseBesignerDocumentResult.refuseOverUnopenedDraft`.\n const refuseOverUnopenedDraft = useCallback(\n (action: 'save' | 'publish'): boolean => {\n if (!unopenedDraftOnOffer) return false\n notify(\n action === 'publish'\n ? `This ${noun} has a saved draft you have not opened. Open it to ` +\n 'publish it, or discard it to publish what is on the canvas.'\n : `This ${noun} has a saved draft you have not opened. Open it to ` +\n 'keep working on it, or discard it, then save.',\n { variant: 'warning', allowDuplicate: true },\n )\n return true\n },\n [unopenedDraftOnOffer, noun, notify],\n )\n\n const handleSave = useCallback(async () => {\n // Before anything else, including \"Already saved\": a click on a clean\n // canvas with a saved draft on offer is most likely a click meant for the\n // draft, and the answer it needs is where the draft is.\n if (refuseOverUnopenedDraft('save')) {\n onSaveRefused?.()\n return undefined\n }\n const canvasNodes = canvas.toJSON().nodes as Record<string, unknown>\n const prepared = fromCanvasNodes\n ? fromCanvasNodes(canvasNodes)\n : { nodes: canvasNodes }\n\n // \"Nothing to save\" is a claim about the STORED document, so check it\n // against the stored document — not against a baseline that can be\n // wrong. When the two disagree the baseline lied, and this click is the\n // author's only way out: a Save that quietly does nothing while the\n // canvas and the document differ is how work is lost in silence\n // (AGL-1262). `nodes` absent means the document has not loaded, and\n // saving the empty canvas over it would be the worse bug.\n if (!saveAvailable) {\n const agrees =\n !nodes || 'error' in prepared || isEqual(prepared.nodes, nodes)\n if (agrees) {\n return notify('Already saved', { variant: 'info', persist: false })\n }\n }\n // Refuse rather than merge. A wrong automatic merge of a whole node map\n // is worse than a refusal the user can act on — and their work is still\n // in the canvas either way.\n if (remoteChanged) {\n onSaveRefused?.()\n return notify(new ConcurrentEditError().message, {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n const dequeueLoading = queueLoading()\n\n if ('error' in prepared) {\n dequeueLoading()\n onSaveRefused?.()\n return notify(prepared.error, {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n const nextNodes = prepared.nodes\n // Size guard (AGL-678): the node map is stored as one msgpack blob and\n // Firestore rejects documents over 1 MiB. Nothing checked this before,\n // so an oversized document simply stopped saving with a generic error\n // and no way to tell which content was to blame.\n const size = measureNodeMap(nextNodes)\n if (size.tooLarge) {\n dequeueLoading()\n onSaveRefused?.()\n const worst = size.largest[0]\n return notify(\n `This ${noun} is ${formatBytes(size.bytes)} and too large to ` +\n 'save. Move repeated sections into reusable components, or replace ' +\n 'inlined images with uploads from the media library' +\n (worst\n ? ` — the largest element is ${formatBytes(worst.bytes)}.`\n : '.'),\n { variant: 'error', allowDuplicate: true },\n )\n }\n if (size.nearLimit) {\n notify(\n `Heads up: this ${noun} is ${formatBytes(size.bytes)}. Past ` +\n 'about 900 KB it stops saving — moving repeated sections into ' +\n 'reusable components is the usual fix.',\n { variant: 'warning', persist: false },\n )\n }\n\n try {\n // The baseline rides along so the store can refuse a stale save\n // SERVER-side (AGL-1301) — the listener guard above only knows about\n // writes whose snapshot has already arrived.\n await save(nextNodes, {\n baseStamp: baseStampRef.current,\n baseNodes: baseNodesRef.current,\n })\n canvas.updateInitialNodes(nextNodes as never)\n // The draft dies with the save that made it redundant (AGL-1256). This\n // is the rule that keeps a crash net from quietly becoming free version\n // history: a draft can only ever hold work that was never saved, so it\n // can never be used to roll a document back to an earlier saved state.\n // That is what the `versioning` entitlement sells.\n if (draftIds) clearBesignerDraft(draftIds)\n // The SHARED draft goes with it only when the saved canvas is one that\n // took the draft in — opened here, or written from here (AGL-2874). A\n // draft this session never opened is somebody's saved work that the\n // canvas just stored does not contain. Best effort: a save that landed\n // must not report failure because the tidy-up behind it did.\n if (draftIds && options.firestore && !draft.sharedDraftUnopened) {\n void clearServerDraft(options.firestore, draftIds)\n }\n // Fire-and-forget attribution (AGL-676) — an audit miss must not break\n // the edit that triggered it.\n onSaved?.()\n // Our own write moves the stamp; the new value arrives on a later\n // snapshot, which we will recognise by the nodes we just sent rather\n // than by it merely being the next one to arrive (AGL-2486).\n expectOwnWriteRef.current = { nodes: nextNodes }\n setRemoteChanged(false)\n notify(\n savedMessage ??\n `${noun.charAt(0).toUpperCase()}${noun.slice(1)} saved successfully`,\n { variant: 'success', persist: false },\n )\n } catch (saveError) {\n // Whatever the reason, the stored tree is not what the canvas holds, so\n // anything waiting on this save has to stop.\n onSaveRefused?.()\n // A store-side refusal of a stale baseline (AGL-1301) is the SAME\n // conflict the guard above refuses, caught later — surface it the same\n // way, and pause saving so the next click does not retry blind. Name\n // check rather than instanceof: the error may cross a lib boundary.\n if ((saveError as Error | undefined)?.name === 'ConcurrentEditError') {\n setRemoteChanged(true)\n notify((saveError as Error).message, {\n variant: 'warning',\n allowDuplicate: true,\n })\n } else {\n notify(`Error: ${JSON.stringify(saveError)}`, {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n } finally {\n dequeueLoading()\n }\n return undefined\n }, [\n saveAvailable,\n remoteChanged,\n nodes,\n save,\n notify,\n queueLoading,\n noun,\n onSaved,\n onSaveRefused,\n savedMessage,\n fromCanvasNodes,\n draftIds,\n refuseOverUnopenedDraft,\n options.firestore,\n draft.sharedDraftUnopened,\n ])\n\n // See `UseBesignerDocumentResult.saveWorkingDraft` for why the stamp is\n // taken here and not by the editor that owns the button.\n const saveWorkingDraft = useCallback(\n async (author?: BesignerDraftAuthor): Promise<ServerDraftWrite> => {\n const firestore = options.firestore\n if (!firestore || !draftIds) return 'failed'\n // Trusting the canvas's own verdict is safe in this direction: an\n // unconfirmed baseline always reads dirty (AGL-1262), so \"clean\" here\n // means the store has confirmed exactly this tree.\n if (!saveAvailable) return 'unchanged'\n return writeServerDraft(firestore, draftIds, {\n nodes: canvas.toJSON().nodes as Aglyn.ProcessableNodes,\n baseStamp: versionStamp(updatedAt),\n updatedByUid: author?.uid ?? null,\n updatedByEmail: author?.email ?? null,\n }).catch(() => 'failed' as const)\n },\n [options.firestore, draftIds, updatedAt, saveAvailable],\n )\n\n // Same expectation `handleSave` records, for writes that do not go through\n // it. Those write OTHER fields — component properties are the first\n // (AGL-1247) — so the echo they produce moves the stamp and leaves `nodes`\n // exactly as the baseline has them. Expecting that is what makes the\n // announcement precise: if a colleague's node save lands first, it does\n // not match, and declaring a property no longer swallows their write.\n const markOwnWrite = useCallback(() => {\n expectOwnWriteRef.current = { nodes: baseNodesRef.current }\n }, [])\n\n const [jsonOpen, setJsonOpen] = useState(false)\n const openJsonEditor = useCallback(() => setJsonOpen(true), [])\n const closeJsonEditor = useCallback(() => setJsonOpen(false), [])\n const handleJsonSave = useCallback((_event: unknown, value: unknown) => {\n canvas.applyNodes(value as never)\n setJsonOpen(false)\n }, [])\n\n return {\n saveAvailable,\n remoteChanged,\n handleSave,\n saveWorkingDraft,\n refuseOverUnopenedDraft,\n markOwnWrite,\n draft,\n jsonOpen,\n openJsonEditor,\n closeJsonEditor,\n handleJsonSave,\n hasError,\n notFound,\n staleContent,\n }\n}\n\nexport default useBesignerDocument\n"],"names":["canvas","ConcurrentEditError","ensureCanvasRoot","formatBytes","hasConcurrentWrite","HostViewType","incorporatesStoredNodes","measureNodeMap","versionStamp","isEqual","useCallback","useEffect","useRef","useState","Besigner","clearBesignerDraft","clearServerDraft","writeServerDraft","useBesignerDraft","noopQueueLoading","undefined","noopNotify","setLocalNodes","value","parsed","processNodesToDenormalized","setNodes","useBesignerDocument","options","nodes","updatedAt","pendingWrites","status","error","save","noun","viewType","documentKey","draft","draftIds","roomSessions","notify","queueLoading","onSaved","onSaveRefused","savedMessage","toCanvasNodes","fromCanvasNodes","saveAvailable","isInitialSame","errored","Boolean","hasError","staleContent","notFound","reset","focus","clearFocusStatus","doesBesignerAppExist","app","getBesignerApp","setBesignerFlag","flag","SCREEN","baseStampRef","baseNodesRef","expectOwnWriteRef","remoteChanged","setRemoteChanged","didSetInitial","updateInitialNodes","confirmed","current","isInitialConfirmed","confirmInitialNodes","storedShapeOfCanvas","canvasNodes","toJSON","prepared","stored","stampMoved","nodesMoved","ownWrite","ourNodes","ids","firestore","loaded","dirty","storedStamp","storedNodes","unopenedDraftOnOffer","available","origin","restoreBlockedBy","refuseOverUnopenedDraft","action","variant","allowDuplicate","handleSave","agrees","persist","message","dequeueLoading","nextNodes","size","tooLarge","worst","largest","bytes","nearLimit","baseStamp","baseNodes","sharedDraftUnopened","charAt","toUpperCase","slice","saveError","name","JSON","stringify","saveWorkingDraft","author","updatedByUid","uid","updatedByEmail","email","catch","markOwnWrite","jsonOpen","setJsonOpen","openJsonEditor","closeJsonEditor","handleJsonSave","_event","applyNodes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/besigner/feature/designer/src/lib/hooks/use-besigner-document.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Firestore } from 'firebase/firestore'\nimport type * as Aglyn from '@aglyn/aglyn'\nimport {\n canvas,\n ConcurrentEditError,\n ensureCanvasRoot,\n formatBytes,\n hasConcurrentWrite,\n HostViewType,\n incorporatesStoredNodes,\n measureNodeMap,\n versionStamp,\n} from '@aglyn/aglyn'\nimport isEqual from 'lodash-es/isEqual.js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport * as Besigner from '@aglyn/besigner'\nimport {\n type BesignerDraftIds,\n clearBesignerDraft,\n} from '../drafts/besigner-draft-store'\nimport {\n type ServerDraftWrite,\n clearServerDraft,\n writeServerDraft,\n} from '../drafts/besigner-server-draft'\nimport {\n type BesignerDraftState,\n useBesignerDraft,\n} from './use-besigner-draft'\n\n/**\n * How the host application surfaces a message. Deliberately a plain\n * callback rather than a snackbar dependency: the console passes its\n * `enqueueSnackbar`, and an embedder outside the console passes whatever it\n * has (a toast, a console.log, nothing at all).\n */\nexport type BesignerNotify = (\n message: string,\n options?: {\n variant?: 'info' | 'success' | 'warning' | 'error'\n /**\n * Presentation hints, passed through rather than derived from `variant`\n * because the console's four editors deliberately mix them — a\n * near-limit warning is transient, a concurrent-edit warning is not.\n * An embedder without these concepts can ignore them.\n */\n persist?: boolean\n allowDuplicate?: boolean\n },\n) => void\n\n/**\n * Begins a loading indication and returns the function that ends it. Same\n * contract as the console's `queueLoading`; defaults to a no-op.\n */\nexport type BesignerQueueLoading = () => () => void\n\n/**\n * What this editor believes the stored document looked like when it last\n * agreed with it, handed to `save` so a store can make the write\n * conditional (AGL-1301) — a Firestore provider runs the same comparison\n * inside a transaction, closing the window where a save lands between\n * another writer's commit and the local snapshot's delivery.\n */\nexport interface BesignerSaveBaseline {\n /** `versionStamp(...)` of the stored `updatedAt` at load/last agreement. */\n baseStamp: string | null\n /** The stored nodes as of that agreement, in STORED shape. */\n baseNodes: Aglyn.ProcessableNodes | undefined\n}\n\nexport interface BesignerDocumentSource<TData = unknown> {\n /** The stored node map, or undefined while loading. */\n nodes: Aglyn.ProcessableNodes | undefined\n /** Whatever the store stamps on write; used to detect concurrent edits. */\n updatedAt?: unknown\n /**\n * The snapshot `nodes` came from still carries writes this client made\n * that the store has NOT acknowledged (Firestore's\n * `snapshot.metadata.hasPendingWrites`).\n *\n * It decides whether the loaded document may be adopted as the *saved*\n * state. It may not: a queued write is replayed into the first snapshot\n * after a reload, so believing it would mean recording work the server\n * never took as \"already saved\" — after which the editor is clean, Save is\n * dead, and the canvas and the document disagree in silence (AGL-1262).\n * Omitted (undefined) reads as confirmed, which is right for every store\n * that has no such queue.\n */\n pendingWrites?: boolean\n status?: 'idle' | 'loading' | 'success' | 'error'\n error?: { message?: string } | null\n /**\n * Persists the node map. Rejecting surfaces as an error notification —\n * except `ConcurrentEditError`, which surfaces through the same refusal\n * UX as the listener-side guard (AGL-1301). `baseline` is what this\n * editor last agreed the stored document looked like; a store that can\n * make the write conditional should (see `saveNodesGuarded`), and one\n * that cannot may ignore it.\n */\n save: (\n nodes: Record<string, unknown>,\n baseline?: BesignerSaveBaseline,\n ) => Promise<void>\n data?: TData\n}\n\nexport interface UseBesignerDocumentOptions<TData = unknown>\n extends BesignerDocumentSource<TData> {\n /**\n * The word used in user-facing copy — 'screen', 'layout', 'component',\n * 'template', 'email'. Keeps each editor's messages accurate without\n * forking the logic.\n */\n noun: string\n /** Canvas view type; reset to SCREEN on unmount. */\n viewType?: Aglyn.HostViewType\n /**\n * Identity of the open document. Changing it re-arms the canvas reset,\n * which is what stops one document's nodes leaking into the next.\n */\n documentKey?: string\n /**\n * Identity for the local crash-recovery draft (AGL-1256). Omit and drafts\n * are simply off for this editor.\n *\n * Deliberately separate from `documentKey`, which is not unique enough to\n * key storage on: both email editors build it as\n * `${templateKey}:${versionId}`, so a platform template and every host's\n * override of the same key produce the same string.\n */\n draft?: BesignerDraftIds\n /**\n * Enables the SHARED working draft — the one `Save draft` writes and\n * `Save & publish` clears. Omit and the editor keeps the local crash net\n * alone, which is what a document with no versions to hang a draft off\n * should do.\n */\n firestore?: Firestore\n /**\n * Other live editing sessions in this document's room, or null while\n * presence has not settled — see `recoverableRoomSessions` (AGL-2486).\n *\n * Only the crash-recovery prompt reads it. The conflict guard deliberately\n * does NOT: a room is a live-collaboration fact and saving must be\n * protected whether or not presence is working at all.\n */\n roomSessions?: number | null\n notify?: BesignerNotify\n queueLoading?: BesignerQueueLoading\n /** Called after a successful save, for activity logging. */\n onSaved?: () => void\n /**\n * Called when a save was REFUSED or FAILED — the document does not hold the\n * canvas.\n *\n * `onSaved` not firing is not enough to tell a caller that, because it also\n * stays silent for \"nothing to save\", where the document already holds the\n * canvas and every follow-on step is safe. The two need opposite handling by\n * anything that acts on a save: `handleSaveAndPublish` promotes the version\n * pointer afterwards, and promoting past a refusal publishes a version whose\n * document never received the edit.\n *\n * Fired for every path that leaves the stored tree unchanged against the\n * author's intent: a stale-baseline conflict, an unpreparable canvas, the\n * size guard, and a thrown write. Each has already explained itself in a\n * toast, so a caller's job is only to stop.\n */\n onSaveRefused?: () => void\n /**\n * Overrides the success message. Defaults to `\"<Noun> saved successfully\"`;\n * templates say simply \"Template saved\" and that copy is preserved rather\n * than normalised by the extraction.\n */\n savedMessage?: string\n /**\n * Maps the stored node map into what the canvas should render, for\n * documents whose storage shape is not a canvas tree.\n *\n * Reusable components and component-kind templates store a *definition*,\n * rooted at the promoted node rather than the canvas root — rendering one\n * unwrapped gives a canvas with no root and a blank editor (AGL-680).\n * Defaults to identity; putting a document back into its stored shape is\n * the caller's `save`, which already owns that direction (AGL-681).\n */\n toCanvasNodes?: (\n nodes: Aglyn.ProcessableNodes,\n ) => Aglyn.ProcessableNodes\n /**\n * The inverse of `toCanvasNodes`: maps the canvas tree back into the\n * stored shape, and may refuse the save with a message.\n *\n * Runs *before* the size guard, so the guard measures what will actually\n * be written rather than the canvas representation of it. A component-kind\n * template that no longer has a single top-level element is rejected here\n * (AGL-681).\n */\n fromCanvasNodes?: (\n canvasNodes: Record<string, unknown>,\n ) => { nodes: Record<string, unknown> } | { error: string }\n}\n\n/** Who a shared working draft records as its last writer. */\nexport interface BesignerDraftAuthor {\n uid?: string | null\n email?: string | null\n}\n\nexport interface UseBesignerDocumentResult {\n /** True when the canvas differs from the last agreed state. */\n saveAvailable: boolean\n /**\n * True when the canvas is exactly what {@link saveWorkingDraft} last\n * stored — the work is safe, even though {@link saveAvailable} is still\n * true because the VERSION document deliberately does not have it\n * (AGL-3271).\n *\n * On the version a site is serving, Save draft writes the draft beside the\n * version and never the version itself, so `saveAvailable` can never go\n * false there and a toolbar reading it alone offers `Save draft` forever —\n * over work that has already saved, with the publish it is waiting for\n * reachable only from a menu. Read the two together: unsaved, saved but\n * not live, live.\n *\n * Goes false again on the author's next edit, and on a document that has\n * no working draft it is never true.\n */\n workingDraftSaved: boolean\n /** True when someone else wrote this document since we loaded it. */\n remoteChanged: boolean\n handleSave: () => Promise<void> | void\n /**\n * Writes the SHARED working draft — what `Save draft` stores and\n * `Save & publish` clears.\n *\n * Here rather than in each editor because of the one field an editor\n * cannot get right on its own: `baseStamp`. A recovered draft is measured\n * against `storedStamp`, which is this document's `updatedAt` — the\n * VERSION document — and every console editor was stamping its draft from\n * the PARENT document instead (`screens/{id}`, `components/{id}`, …). Two\n * documents, two `updatedAt` fields, so the two values could only agree by\n * coincidence: every working draft read back as \"someone else saved this\n * while you were editing\", Restore was never offered, and the draft could\n * never be recovered by anyone. Taking the stamp where the comparison is\n * made is what stops the two drifting apart again.\n *\n * Resolves `'failed'` when this document cannot hold a shared draft — no\n * `firestore`, or no draft ids — which is the same answer a rejected write\n * gives, and the caller's handling of both is identical.\n *\n * Resolves `'unchanged'` WITHOUT writing when the canvas is clean\n * (AGL-2874). A clean canvas is the stored document, so there is no draft\n * to take — and writing one anyway would replace whatever saved draft the\n * slot holds with a copy of the document it was drafted against.\n */\n saveWorkingDraft: (author?: BesignerDraftAuthor) => Promise<ServerDraftWrite>\n /**\n * Refuses, and says why, an action that would leave behind a saved draft\n * this author has been offered and has not opened (AGL-2874). Returns true\n * when the caller must stop.\n *\n * `'publish'` for Save & publish and a Publish click: publishing stores and\n * promotes the canvas, and a canvas that never took the draft in would go\n * live without it — after which the draft is cleared as published. `'save'`\n * for a draft save, which would overwrite it, and for a save of the\n * document, which moves the stamp the draft was taken against and leaves\n * it unopenable.\n *\n * Only while the offer is ON SCREEN and can be opened: the refusal points\n * at the banner's Open draft and Discard, and must never point at buttons\n * that are not there.\n */\n refuseOverUnopenedDraft: (action: 'save' | 'publish') => boolean\n /**\n * Announce a write this editor is about to make to the SAME document\n * outside `handleSave` — component properties are the first (AGL-1247).\n *\n * Without it the write's own echo is indistinguishable from a colleague's\n * and trips the conflict guard, which pauses saving until reload: the\n * editor accuses you of being someone else.\n */\n markOwnWrite: () => void\n /**\n * Unsaved work recovered from local storage after a crash or reload, and\n * the two things the author can do about it (AGL-1256). Always present;\n * `available` is false when there is nothing to offer.\n */\n draft: BesignerDraftState\n /** JSON editor plumbing, identical in every editor. */\n jsonOpen: boolean\n openJsonEditor: () => void\n closeJsonEditor: () => void\n handleJsonSave: (event: unknown, value: unknown) => void\n /**\n * The document could not be read AND there is nothing to show for it\n * (AGL-1066).\n *\n * Deliberately NOT \"the read errored\". Under `persistentLocalCache` a\n * refused listen still serves the document from IndexedDB, and a refused\n * listen can now reach `status: 'error'` — so an editor with a canvas full\n * of the author's work would otherwise be replaced by \"Not found\" about two\n * seconds into a stale session, mid-edit. Whether the console should keep\n * SERVING that cached document is the question AGL-1066 settled as \"keep\n * serving, but stop presenting it as live\"; the presentation half is the\n * shell's re-auth banner and the refusal on save, not blanking the canvas.\n *\n * When the read failed and nothing ever arrived there is genuinely nothing\n * to render, and this is true — which is the state the \"Not found\" branch\n * was written for.\n */\n hasError: boolean\n notFound: boolean\n /**\n * The document is on screen but the server has stopped answering for it\n * (AGL-1066) — cached content, no refresh coming. Saving is already refused\n * by the seed guards; this is what an editor would surface to say why.\n */\n staleContent: boolean\n}\n\nconst noopQueueLoading: BesignerQueueLoading = () => () => undefined\nconst noopNotify: BesignerNotify = () => undefined\n\n/**\n * Pushes a stored node map into the shared canvas singleton.\n *\n * The canvas root is guaranteed here rather than per editor (AGL-931). A map\n * without one leaves the hierarchy showing 'Invalid node' with Add Element\n * disabled — an editor the document can never be repaired from — and only\n * the two definition-shaped editors passed a `toCanvasNodes` that repaired\n * it. Doing it at the one point every editor loads through means no route\n * can be added that forgets.\n */\nexport function setLocalNodes(value: Aglyn.ProcessableNodes) {\n const parsed = canvas.processNodesToDenormalized(value)\n return canvas.setNodes(\n ensureCanvasRoot(parsed) as typeof parsed,\n )\n}\n\n/**\n * The document-agnostic half of a besigner editor.\n *\n * Everything here was copy-pasted across the console's four besigner routes\n * (screens, layouts, components, templates) — canvas lifecycle, first load,\n * concurrent-write detection (AGL-674), the size-guarded save (AGL-678) and\n * the JSON editor. None of it is about hosts, orgs, themes or publishing,\n * so none of it belongs in a console route.\n *\n * It has **no console and no host dependency** by design: the caller\n * supplies the document source and the notify/loading adapters. That is\n * what lets besigner drive a document that has no host — the platform email\n * templates — and, longer term, run outside the console entirely.\n */\nexport function useBesignerDocument<TData = unknown>(\n options: UseBesignerDocumentOptions<TData>,\n): UseBesignerDocumentResult {\n const {\n nodes,\n updatedAt,\n pendingWrites,\n status,\n error,\n save,\n noun,\n viewType,\n documentKey,\n draft: draftIds,\n roomSessions,\n notify = noopNotify,\n queueLoading = noopQueueLoading,\n onSaved,\n onSaveRefused,\n savedMessage,\n toCanvasNodes,\n fromCanvasNodes,\n } = options\n\n const saveAvailable = !canvas.isInitialSame\n const errored = Boolean(error) || status === 'error'\n // See `UseBesignerDocumentResult.hasError` — an errored read that still has\n // a document to show is stale, not missing (AGL-1066).\n const hasError = errored && !nodes\n const staleContent = errored && Boolean(nodes)\n /**\n * `!== 'loading'` rather than `=== 'success'` (AGL-1066).\n *\n * A refused read with nothing cached used to sit on `'loading'` forever and\n * now reaches `'error'`, so gating on `'success'` would have quietly stopped\n * recognising a missing document at the moment it started being reachable.\n * `hasError` covers the errored half either way; this keeps the two verdicts\n * from both going false and rendering an editor over nothing.\n */\n const notFound = status !== 'loading' && !nodes\n\n // The canvas is a singleton shared by every editing session; without a\n // reset on leave, client-side navigation to another document keeps (and\n // could save) this one's nodes.\n useEffect(() => {\n return () => {\n canvas.reset()\n Besigner.focus.clearFocusStatus()\n }\n }, [documentKey])\n\n // The view type decides which components the drawer offers (an email\n // document must not be able to insert a nav bar). Reset on leave so the\n // next session on the singleton canvas is unaffected.\n useEffect(() => {\n if (!viewType) return undefined\n if (!Besigner.doesBesignerAppExist()) return undefined\n const app = Besigner.getBesignerApp()\n Besigner.setBesignerFlag(app, { flag: 'viewType', value: () => viewType })\n return () => {\n Besigner.setBesignerFlag(app, {\n flag: 'viewType',\n value: () => HostViewType.SCREEN,\n })\n }\n }, [viewType])\n\n useEffect(() => {\n if (status === 'loading') return queueLoading()\n return undefined\n }, [status])\n\n // Conflict detection (AGL-674). Two people editing one version both write\n // the WHOLE node map, so without this the later save silently replaces the\n // earlier one and neither is told. `baseStamp` is what the document looked\n // like when this editor last agreed with it.\n const baseStampRef = useRef<string | null>(null)\n /**\n * The stored nodes as of the last agreement, in STORED shape. Kept\n * alongside the stamp because the stamp is an app-level FIELD a writer can\n * forget: admin scripts have updated `nodes` without touching `updatedAt`,\n * and such a write was invisible to the stamp guard and got clobbered\n * (AGL-1301). The web SDK exposes no server-side `updateTime` on a\n * snapshot, so a change in the content itself is the strongest signal a\n * client can key on.\n */\n const baseNodesRef = useRef<Aglyn.ProcessableNodes | undefined>(undefined)\n /**\n * What THIS editing session last wrote, held until the snapshot carrying\n * it arrives — so \"is this my own echo?\" is answered by evidence rather\n * than by a flag (AGL-2486).\n *\n * ## The hole this closes\n *\n * It used to be a boolean: save, set it, and adopt the NEXT moving\n * snapshot as ours whatever it contained. That is a bet on delivery order,\n * and the console runs `persistentMultipleTabManager`, so the tabs of one\n * browser share a cache and their snapshots coalesce. Two sessions saving\n * within a beat of each other therefore reach this:\n *\n * 1. this tab saves and arms the flag;\n * 2. another session commits before our echo is delivered;\n * 3. one snapshot arrives, carrying THEIR nodes;\n * 4. the flag consumes it — `remoteChanged` stays false, and both\n * baselines advance to their write.\n *\n * The second half is the damage. With the baseline advanced, the next\n * Save passes the client guard AND the AGL-1301 transaction — the stamp\n * and content it presents are the ones actually stored — so their work is\n * overwritten with no warning at either layer: the second save silently\n * replaces the first. Nothing about that needs the two writers to be\n * different people — it needs them to be different SESSIONS, which two tabs\n * of one account are.\n *\n * Comparing the content closes it without a schema change. A snapshot is\n * our echo only if it carries what we wrote; anything else is somebody\n * else's write and is flagged. Two sessions that saved IDENTICAL content\n * match and are adopted, which is correct — there is no work to lose\n * between two writes that agree.\n *\n * Note what stays untouched: the guard still reads no uid anywhere, so it\n * has never distinguished a colleague from your own other window, and now\n * refuses both alike. The server-side half is unchanged and is what makes\n * this safe — because the baseline no longer advances, the stale save is\n * refused inside the transaction as well.\n */\n const expectOwnWriteRef = useRef<{ nodes: unknown } | null>(null)\n const [remoteChanged, setRemoteChanged] = useState(false)\n\n useEffect(() => {\n if (nodes && !canvas.didSetInitial) {\n setLocalNodes(toCanvasNodes ? toCanvasNodes(nodes) : nodes)\n // The loaded document becomes the \"saved\" baseline ONLY when the store\n // has acknowledged it. A snapshot still carrying our own queued write\n // shows the work (never hide it) but records nothing as saved, so the\n // editor stays dirty and the work can still be written for real\n // (AGL-1262). Without this the editor adopts an unacknowledged edit as\n // the saved state and can never save again — not by re-editing, not by\n // restoring the draft, whose content is the same.\n canvas.updateInitialNodes(undefined, {\n confirmed: !pendingWrites,\n })\n baseStampRef.current = versionStamp(updatedAt)\n baseNodesRef.current = nodes\n return\n }\n // The baseline was recorded from a snapshot carrying our own queued\n // write, and that write has now been acknowledged (AGL-2486). Nothing\n // else ever re-confirms: the guard above runs once, so without this the\n // editor stays dirty for the rest of the session over a document nobody\n // has edited. `confirmInitialNodes` refuses if the canvas has moved on\n // since, so an author who edited in that window keeps their Save\n // (AGL-1262) — the store vouched for the earlier write, not for theirs.\n if (nodes && !pendingWrites && !canvas.isInitialConfirmed) {\n canvas.confirmInitialNodes()\n }\n }, [nodes, pendingWrites])\n\n /**\n * The canvas as the STORE would hold it — the same conversion `handleSave`\n * performs, so \"does my document contain theirs\" is asked in the shape the\n * answer has to be true in (AGL-2486).\n *\n * Null when the canvas cannot be expressed in that shape at all: a\n * component-kind document mid-edit that `fromCanvasNodes` refuses\n * (AGL-681) has no stored form to compare, and \"no answer\" must read as a\n * conflict rather than as agreement.\n */\n const storedShapeOfCanvas = useCallback((): Record<string, unknown> | null => {\n if (!canvas.didSetInitial) return null\n const canvasNodes = canvas.toJSON().nodes as Record<string, unknown>\n if (!fromCanvasNodes) return canvasNodes\n const prepared = fromCanvasNodes(canvasNodes)\n return 'error' in prepared ? null : prepared.nodes\n }, [fromCanvasNodes])\n\n useEffect(() => {\n const stored = versionStamp(updatedAt)\n const stampMoved = hasConcurrentWrite(baseStampRef.current, stored)\n // A writer that forgot the stamp still cannot hide: the content moved.\n const nodesMoved =\n nodes != null &&\n baseNodesRef.current != null &&\n !isEqual(nodes, baseNodesRef.current)\n if (!stampMoved && !nodesMoved) return\n const ownWrite = expectOwnWriteRef.current\n if (ownWrite && isEqual(nodes, ownWrite.nodes)) {\n // This is the echo of our own write landing — it carries what we sent.\n expectOwnWriteRef.current = null\n baseStampRef.current = stored\n baseNodesRef.current = nodes\n return\n }\n // Somebody else's write. Whether it is a CONFLICT is a different\n // question from whether the document moved (AGL-2486).\n //\n // Everyone collaborating should be able to save as they go, and the\n // mirror is what makes that safe: their changes reached this canvas node\n // by node before they pressed Save, so this session usually already holds\n // what they stored and its own write is a superset of it.\n // `incorporatesStoredNodes` is the\n // evidence for that — never an assumption that co-editing is healthy —\n // and a session that has fallen behind cannot satisfy it, which is the\n // case the guard exists for and still refuses.\n //\n // Advancing the baseline here is what lets the save through, and it is\n // safe because the store re-runs the AGL-1301 precondition against what\n // is ACTUALLY stored at commit time: anything that lands between this\n // decision and that commit moves `nodes` away from the baseline just\n // adopted, and the transaction refuses it. The promise that \"even a save\n // racing the conflict by milliseconds is refused\" is untouched — the\n // client simply stops refusing saves the server would have accepted.\n const ourNodes = storedShapeOfCanvas()\n if (incorporatesStoredNodes(baseNodesRef.current, nodes, ourNodes)) {\n expectOwnWriteRef.current = null\n baseStampRef.current = stored\n baseNodesRef.current = nodes\n setRemoteChanged(false)\n // …and when the canvas holds NOTHING beyond what was just stored,\n // there is nothing left to save. Without this the editor keeps\n // offering SAVE over a document it agrees with completely, which is\n // the second half of the same report: the mirror made the canvas\n // dirty against a baseline recorded at load, and their save is what\n // made that baseline stale rather than the canvas wrong. No argument,\n // so the canvas as it stands becomes the saved state.\n if (isEqual(ourNodes, nodes)) canvas.updateInitialNodes()\n return\n }\n // Our own expectation is void too: whatever it was waiting for either\n // lost the race or is behind this, and a later matching snapshot must\n // not be allowed to walk the baseline forward over a reported conflict.\n expectOwnWriteRef.current = null\n setRemoteChanged(true)\n }, [updatedAt, nodes, storedShapeOfCanvas])\n\n // Crash net (AGL-1256). Shares the conflict guard's stamp so the restore\n // prompt can tell \"your unsaved work\" from \"your unsaved work, and someone\n // else has saved since\".\n const draft = useBesignerDraft({\n ids: draftIds,\n // Given one, the shared working draft joins the local crash net and wins\n // when both exist (AGL-1152).\n firestore: options.firestore,\n loaded: canvas.didSetInitial,\n dirty: saveAvailable,\n storedStamp: versionStamp(updatedAt),\n storedNodes: nodes,\n roomSessions,\n })\n\n const unopenedDraftOnOffer =\n draft.available &&\n draft.origin === 'shared' &&\n draft.restoreBlockedBy === null\n\n // See `UseBesignerDocumentResult.refuseOverUnopenedDraft`.\n const refuseOverUnopenedDraft = useCallback(\n (action: 'save' | 'publish'): boolean => {\n if (!unopenedDraftOnOffer) return false\n notify(\n action === 'publish'\n ? `This ${noun} has a saved draft you have not opened. Open it to ` +\n 'publish it, or discard it to publish what is on the canvas.'\n : `This ${noun} has a saved draft you have not opened. Open it to ` +\n 'keep working on it, or discard it, then save.',\n { variant: 'warning', allowDuplicate: true },\n )\n return true\n },\n [unopenedDraftOnOffer, noun, notify],\n )\n\n const handleSave = useCallback(async () => {\n // Before anything else, including \"Already saved\": a click on a clean\n // canvas with a saved draft on offer is most likely a click meant for the\n // draft, and the answer it needs is where the draft is.\n if (refuseOverUnopenedDraft('save')) {\n onSaveRefused?.()\n return undefined\n }\n const canvasNodes = canvas.toJSON().nodes as Record<string, unknown>\n const prepared = fromCanvasNodes\n ? fromCanvasNodes(canvasNodes)\n : { nodes: canvasNodes }\n\n // \"Nothing to save\" is a claim about the STORED document, so check it\n // against the stored document — not against a baseline that can be\n // wrong. When the two disagree the baseline lied, and this click is the\n // author's only way out: a Save that quietly does nothing while the\n // canvas and the document differ is how work is lost in silence\n // (AGL-1262). `nodes` absent means the document has not loaded, and\n // saving the empty canvas over it would be the worse bug.\n if (!saveAvailable) {\n const agrees =\n !nodes || 'error' in prepared || isEqual(prepared.nodes, nodes)\n if (agrees) {\n return notify('Already saved', { variant: 'info', persist: false })\n }\n }\n // Refuse rather than merge. A wrong automatic merge of a whole node map\n // is worse than a refusal the user can act on — and their work is still\n // in the canvas either way.\n if (remoteChanged) {\n onSaveRefused?.()\n return notify(new ConcurrentEditError().message, {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n const dequeueLoading = queueLoading()\n\n if ('error' in prepared) {\n dequeueLoading()\n onSaveRefused?.()\n return notify(prepared.error, {\n variant: 'warning',\n allowDuplicate: true,\n })\n }\n const nextNodes = prepared.nodes\n // Size guard (AGL-678): the node map is stored as one msgpack blob and\n // Firestore rejects documents over 1 MiB. Nothing checked this before,\n // so an oversized document simply stopped saving with a generic error\n // and no way to tell which content was to blame.\n const size = measureNodeMap(nextNodes)\n if (size.tooLarge) {\n dequeueLoading()\n onSaveRefused?.()\n const worst = size.largest[0]\n return notify(\n `This ${noun} is ${formatBytes(size.bytes)} and too large to ` +\n 'save. Move repeated sections into reusable components, or replace ' +\n 'inlined images with uploads from the media library' +\n (worst\n ? ` — the largest element is ${formatBytes(worst.bytes)}.`\n : '.'),\n { variant: 'error', allowDuplicate: true },\n )\n }\n if (size.nearLimit) {\n notify(\n `Heads up: this ${noun} is ${formatBytes(size.bytes)}. Past ` +\n 'about 900 KB it stops saving — moving repeated sections into ' +\n 'reusable components is the usual fix.',\n { variant: 'warning', persist: false },\n )\n }\n\n try {\n // The baseline rides along so the store can refuse a stale save\n // SERVER-side (AGL-1301) — the listener guard above only knows about\n // writes whose snapshot has already arrived.\n await save(nextNodes, {\n baseStamp: baseStampRef.current,\n baseNodes: baseNodesRef.current,\n })\n canvas.updateInitialNodes(nextNodes as never)\n // The draft dies with the save that made it redundant (AGL-1256). This\n // is the rule that keeps a crash net from quietly becoming free version\n // history: a draft can only ever hold work that was never saved, so it\n // can never be used to roll a document back to an earlier saved state.\n // That is what the `versioning` entitlement sells.\n if (draftIds) clearBesignerDraft(draftIds)\n // The SHARED draft goes with it only when the saved canvas is one that\n // took the draft in — opened here, or written from here (AGL-2874). A\n // draft this session never opened is somebody's saved work that the\n // canvas just stored does not contain. Best effort: a save that landed\n // must not report failure because the tidy-up behind it did.\n if (draftIds && options.firestore && !draft.sharedDraftUnopened) {\n void clearServerDraft(options.firestore, draftIds)\n }\n // Fire-and-forget attribution (AGL-676) — an audit miss must not break\n // the edit that triggered it.\n onSaved?.()\n // Our own write moves the stamp; the new value arrives on a later\n // snapshot, which we will recognise by the nodes we just sent rather\n // than by it merely being the next one to arrive (AGL-2486).\n expectOwnWriteRef.current = { nodes: nextNodes }\n setRemoteChanged(false)\n notify(\n savedMessage ??\n `${noun.charAt(0).toUpperCase()}${noun.slice(1)} saved successfully`,\n { variant: 'success', persist: false },\n )\n } catch (saveError) {\n // Whatever the reason, the stored tree is not what the canvas holds, so\n // anything waiting on this save has to stop.\n onSaveRefused?.()\n // A store-side refusal of a stale baseline (AGL-1301) is the SAME\n // conflict the guard above refuses, caught later — surface it the same\n // way, and pause saving so the next click does not retry blind. Name\n // check rather than instanceof: the error may cross a lib boundary.\n if ((saveError as Error | undefined)?.name === 'ConcurrentEditError') {\n setRemoteChanged(true)\n notify((saveError as Error).message, {\n variant: 'warning',\n allowDuplicate: true,\n })\n } else {\n notify(`Error: ${JSON.stringify(saveError)}`, {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n } finally {\n dequeueLoading()\n }\n return undefined\n }, [\n saveAvailable,\n remoteChanged,\n nodes,\n save,\n notify,\n queueLoading,\n noun,\n onSaved,\n onSaveRefused,\n savedMessage,\n fromCanvasNodes,\n draftIds,\n refuseOverUnopenedDraft,\n options.firestore,\n draft.sharedDraftUnopened,\n ])\n\n // See `UseBesignerDocumentResult.saveWorkingDraft` for why the stamp is\n // taken here and not by the editor that owns the button.\n const saveWorkingDraft = useCallback(\n async (author?: BesignerDraftAuthor): Promise<ServerDraftWrite> => {\n const firestore = options.firestore\n if (!firestore || !draftIds) return 'failed'\n // Trusting the canvas's own verdict is safe in this direction: an\n // unconfirmed baseline always reads dirty (AGL-1262), so \"clean\" here\n // means the store has confirmed exactly this tree.\n if (!saveAvailable) return 'unchanged'\n // What is SENT is what the draft will hold. An author who keeps typing\n // through the round trip has work this write does not carry, and\n // recording the canvas as it stands when the promise resolves would\n // call that work stored (AGL-3271).\n const written = canvas.toJSON().nodes as Aglyn.ProcessableNodes\n return writeServerDraft(firestore, draftIds, {\n nodes: written,\n baseStamp: versionStamp(updatedAt),\n updatedByUid: author?.uid ?? null,\n updatedByEmail: author?.email ?? null,\n })\n .then((result) => {\n // ONLY on a write, so this moves in lockstep with the editor's own\n // `draftPending` — the two are read together to decide the button's\n // label, and a checkpoint recorded on a branch that leaves\n // `draftPending` false would let a document with an unpublished\n // draft read as \"Up to date\".\n if (result === 'written') canvas.updateDraftedNodes(written as never)\n return result\n })\n .catch(() => 'failed' as const)\n },\n [options.firestore, draftIds, updatedAt, saveAvailable],\n )\n\n // Same expectation `handleSave` records, for writes that do not go through\n // it. Those write OTHER fields — component properties are the first\n // (AGL-1247) — so the echo they produce moves the stamp and leaves `nodes`\n // exactly as the baseline has them. Expecting that is what makes the\n // announcement precise: if a colleague's node save lands first, it does\n // not match, and declaring a property no longer swallows their write.\n const markOwnWrite = useCallback(() => {\n expectOwnWriteRef.current = { nodes: baseNodesRef.current }\n }, [])\n\n const [jsonOpen, setJsonOpen] = useState(false)\n const openJsonEditor = useCallback(() => setJsonOpen(true), [])\n const closeJsonEditor = useCallback(() => setJsonOpen(false), [])\n const handleJsonSave = useCallback((_event: unknown, value: unknown) => {\n canvas.applyNodes(value as never)\n setJsonOpen(false)\n }, [])\n\n return {\n saveAvailable,\n workingDraftSaved: canvas.isDraftedSame,\n remoteChanged,\n handleSave,\n saveWorkingDraft,\n refuseOverUnopenedDraft,\n markOwnWrite,\n draft,\n jsonOpen,\n openJsonEditor,\n closeJsonEditor,\n handleJsonSave,\n hasError,\n notFound,\n staleContent,\n }\n}\n\nexport default useBesignerDocument\n"],"names":["canvas","ConcurrentEditError","ensureCanvasRoot","formatBytes","hasConcurrentWrite","HostViewType","incorporatesStoredNodes","measureNodeMap","versionStamp","isEqual","useCallback","useEffect","useRef","useState","Besigner","clearBesignerDraft","clearServerDraft","writeServerDraft","useBesignerDraft","noopQueueLoading","undefined","noopNotify","setLocalNodes","value","parsed","processNodesToDenormalized","setNodes","useBesignerDocument","options","nodes","updatedAt","pendingWrites","status","error","save","noun","viewType","documentKey","draft","draftIds","roomSessions","notify","queueLoading","onSaved","onSaveRefused","savedMessage","toCanvasNodes","fromCanvasNodes","saveAvailable","isInitialSame","errored","Boolean","hasError","staleContent","notFound","reset","focus","clearFocusStatus","doesBesignerAppExist","app","getBesignerApp","setBesignerFlag","flag","SCREEN","baseStampRef","baseNodesRef","expectOwnWriteRef","remoteChanged","setRemoteChanged","didSetInitial","updateInitialNodes","confirmed","current","isInitialConfirmed","confirmInitialNodes","storedShapeOfCanvas","canvasNodes","toJSON","prepared","stored","stampMoved","nodesMoved","ownWrite","ourNodes","ids","firestore","loaded","dirty","storedStamp","storedNodes","unopenedDraftOnOffer","available","origin","restoreBlockedBy","refuseOverUnopenedDraft","action","variant","allowDuplicate","handleSave","agrees","persist","message","dequeueLoading","nextNodes","size","tooLarge","worst","largest","bytes","nearLimit","baseStamp","baseNodes","sharedDraftUnopened","charAt","toUpperCase","slice","saveError","name","JSON","stringify","saveWorkingDraft","author","written","updatedByUid","uid","updatedByEmail","email","then","result","updateDraftedNodes","catch","markOwnWrite","jsonOpen","setJsonOpen","openJsonEditor","closeJsonEditor","handleJsonSave","_event","applyNodes","workingDraftSaved","isDraftedSame"],"mappings":"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"}
|