| 110 | 110 | | } from "./access-store.ts"; |
| 111 | 111 | | import { agentMayFind, agentReach, audienceRule, type AgentReach, type AudienceRule } from "./agents.ts"; |
| 112 | 112 | | import { publishFolioEvent } from "./events.ts"; |
| 113 | | − | import { MAX_DEPTH, cleanCover, cleanIcon, cleanNote, cleanSource, cleanTarget, cleanTitle, decodeCursor, depthOf, encodeCursor, listLimit, pageState, sharedTops, slugOf, subtreeHeight, treeNodes } from "./list.ts"; |
| 113 | + | import { MAX_DEPTH, MAX_PAGE_STATE, cleanCover, cleanIcon, cleanNote, cleanSource, cleanTarget, cleanTitle, decodeCursor, depthOf, encodeCursor, listLimit, pageState, sharedTops, slugOf, subtreeHeight, treeNodes } from "./list.ts"; |
| 114 | 114 | | import { REQUEST_RECIPIENTS, claimAccessRequest } from "./requests.ts"; |
| 115 | 115 | | import type { FolioRoom } from "./room.ts"; |
| 116 | 116 | | import { builtinFolioTemplate, builtinFolioTemplates } from "./templates.ts"; |
| ⋯ |
| 584 | 584 | | ]); |
| 585 | 585 | | const parents = above.map((id) => aboveRows.get(id)).filter((r): r is FolioRow => !!r); |
| 586 | 586 | | const others = [...parents, ...childRows.results, ...linkRows.results.filter((r) => r.id !== row.id)]; |
| 587 | | − | const { roles } = await this.roles(ctx, others); |
| 587 | + | const [{ roles }, state] = await Promise.all([this.roles(ctx, others), this.stateFor(ctx.workspace, row)]); |
| 588 | 588 | | const readable = (list: FolioRow[]) => list.filter((r) => roles.get(r.id)).map((r) => this.ref(ctx.workspace.slug, r)); |
| 589 | 589 | | return ok({ |
| 590 | 590 | | folio: folio!, |
| 591 | 591 | | text: row.text, |
| 592 | | − | state: pageState(row.state), |
| 592 | + | state, |
| 593 | 593 | | breadcrumbs: readable(parents), |
| 594 | 594 | | children: readable(childRows.results), |
| 595 | 595 | | backlinks: readable(linkRows.results.filter((r) => r.id !== row.id)), |
| ⋯ |
| 597 | 597 | | }); |
| 598 | 598 | | } |
| 599 | 599 | | |
| 600 | + | /** |
| 601 | + | * The document the page carries, so the editor opens on it before the |
| 602 | + | * room answers. A folio saved before its state was kept (or whose last |
| 603 | + | * save was too large for a row) has none in D1: its room is asked once, |
| 604 | + | * the state goes out with this page, and it is written back so the next |
| 605 | + | * open reads it from the row like any other. A room that cannot answer |
| 606 | + | * costs the page nothing: the editor waits for the room as before. |
| 607 | + | */ |
| 608 | + | private async stateFor(workspace: Workspace, row: FolioRow): Promise<string | null> { |
| 609 | + | const saved = pageState(row.state); |
| 610 | + | if (saved || row.trashed_at) return saved; |
| 611 | + | try { |
| 612 | + | const room = await this.ready(workspace, row); |
| 613 | + | const state = await room.state(); |
| 614 | + | if (!state.byteLength || state.byteLength > MAX_PAGE_STATE) return null; |
| 615 | + | this.defer(this.db.prepare("UPDATE folios SET state = ? WHERE id = ? AND state IS NULL").bind(state, row.id).run()); |
| 616 | + | return pageState(state); |
| 617 | + | } catch (error) { |
| 618 | + | console.warn("artifacts: a page opens without its state", row.id, error instanceof Error ? error.message : String(error)); |
| 619 | + | return null; |
| 620 | + | } |
| 621 | + | } |
| 622 | + | |
| 600 | 623 | | // ── Making and changing ───────────────────────────────────────────────── |
| 601 | 624 | | |
| 602 | 625 | | /** Where a new folio may go for this person: a parent doc they can edit, a space they can edit, or their Private. */ |