@milaboratories/pl-middle-layer 1.72.0 → 1.73.0
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/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +14 -9
- package/dist/middle_layer/folders.cjs.map +1 -1
- package/dist/middle_layer/folders.js +14 -10
- package/dist/middle_layer/folders.js.map +1 -1
- package/dist/middle_layer/middle_layer.cjs +17 -17
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +11 -11
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +18 -18
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/package.json +9 -9
- package/src/middle_layer/folders.test.ts +22 -14
- package/src/middle_layer/folders.ts +37 -27
- package/src/middle_layer/middle_layer.ts +18 -18
- package/src/middle_layer/templates.test.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folders.js","names":[],"sources":["../../src/middle_layer/folders.ts"],"sourcesContent":["import type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type {\n FoldersDecoded,\n FoldersDocument,\n FoldersDocumentProblem,\n FoldersEdit,\n FolderId,\n FolderView,\n FoldersItem,\n FoldersLabelRename,\n FoldersLeafItem,\n FoldersMoveOutcome,\n FoldersMovePlan,\n FoldersMovePlanResult,\n FoldersPlacement,\n FoldersProjectInput,\n FoldersRemoval,\n FoldersRemovalOutcome,\n FoldersRemovalPlanResult,\n FoldersTemplateInput,\n FoldersView,\n FoldersViolation,\n ProjectMeta,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport { asProjectId, asTemplateId } from \"@milaboratories/pl-model-common\";\nimport {\n asFolderId,\n commitFoldersMove,\n commitFoldersRemoval,\n decodeStoredFoldersDocument,\n emptyFoldersDocument,\n formatFoldersViolation,\n healFolders,\n planFoldersRemoval,\n planFoldersMove,\n foldersDocumentFromView,\n foldersNameBlank,\n foldersNameTaken,\n foldersSiblingNames,\n foldersSubtree,\n foldersUniqueName,\n normalizeDescription,\n validateFoldersDocument,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport { ProjectLastModifiedTimestamp, ProjectMetaKey } from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { projectListEntries } from \"./project_list\";\nimport type { TemplateId, TemplateListEntry } from \"./template_list\";\nimport { TemplateLabelKey, templateListEntries } from \"./template_list\";\nimport { renameTemplate } from \"../mutator/template\";\nimport type { ListedKind } from \"../mutator/list\";\nimport { listedById, notListedError } from \"../mutator/list\";\n\n/** Field on the user's client root holding the folder singleton. */\nexport const FoldersField = \"folders\";\nexport const FoldersResourceType: ResourceType = { name: \"Folders\", version: \"1\" };\n\n/**\n * The one field of the singleton. It holds the whole tree as a single immutable JSON value\n * resource; a write mints a new value and re-points this field at it, the way a block's stored\n * state is rewritten.\n */\nexport const FoldersDocumentField = \"document\";\n\n/** A project list entry with its place in the folder tree resolved. */\nexport type FoldersProjectEntry = ProjectListEntry & FoldersPlacement;\n\n/** A template list entry with its place in the folder tree resolved. */\nexport type FoldersTemplateEntry = TemplateListEntry & FoldersPlacement;\n\n/**\n * The single value the folder feature puts across the API boundary: the tree and the project list\n * already joined.\n *\n * Two independently refreshed values would let a project show up in a folder the tree has not\n * heard of, with nobody owning the reconciliation, so the join happens here and the desktop never\n * sees the halves.\n */\nexport interface FoldersListing {\n readonly folders: readonly FolderView[];\n /** Every project, most recently modified first. Nothing here is ever dropped. */\n readonly projects: readonly FoldersProjectEntry[];\n /** Every stored template, most recently created first. Nothing here is ever dropped. */\n readonly templates: readonly FoldersTemplateEntry[];\n /** False when every folder write is refused, because the document is not ours to rewrite. */\n readonly writable: boolean;\n /** Present when the stored document could not be used as it stands. */\n readonly problem?: FoldersDocumentProblem;\n /** True when what the listing shows differs from what is stored, because the read healed it. */\n readonly healed: boolean;\n}\n\n/** Every singleton a folder operation touches. */\nexport interface FoldersRids {\n readonly folders: SignedResourceId;\n readonly projects: SignedResourceId;\n readonly templates: SignedResourceId;\n}\n\n/**\n * Resolves the folder singleton on the transaction's client root, lazily creating (and locking)\n * an empty one when {@link FoldersField} is not yet populated. Same shape as the\n * project-list and template-list singletons beside it.\n */\nexport async function ensureFoldersRid(tx: PlTransaction): Promise<SignedResourceId> {\n const f = field(tx.clientRoot, FoldersField);\n tx.createField(f, \"Dynamic\");\n const fData = await tx.getField(f);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(FoldersResourceType);\n tx.lock(ref);\n tx.setField(f, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const FoldersTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, FoldersResourceType)) return [];\n return resource.fields;\n};\n\nexport const foldersFieldFilter: Filter = treeFilter.resourceTypeEq(FoldersResourceType.name);\n\n/**\n * The folder tree plus the computable that joins it with the project list.\n *\n * Both halves are read from one computable context, so the listing is always one consistent\n * snapshot. Nothing is published while either half is still syncing: a project would otherwise\n * render at the top level for a poll or two and then jump into its folder.\n */\nexport async function createFolderList(\n pl: PlClient,\n rid: SignedResourceId,\n projectsTree: SynchronizedTreeState,\n templatesTree: SynchronizedTreeState,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<FoldersListing>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: FoldersTreePruningFunction,\n fieldFilter: foldersFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const projectsNode = ctx.accessor(projectsTree.entry()).node();\n const templatesNode = ctx.accessor(templatesTree.entry()).node();\n const foldersNode = ctx.accessor(tree.entry()).node();\n if (projectsNode === undefined || templatesNode === undefined || foldersNode === undefined) {\n ctx.markUnstable(\"folders_not_synced\");\n return undefined;\n }\n\n const entries = projectListEntries(projectsNode, openedProjects.getValue(ctx));\n const templates = templateListEntries(templatesNode);\n const result = foldersListing(foldersNode, entries, templates);\n if (result.status === \"not-synced\") {\n ctx.markUnstable(result.reason);\n return undefined;\n }\n return result.listing;\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node the folder reader needs from the folder singleton. */\nexport interface FoldersNode {\n listDynamicFields(): string[];\n traverse(step: {\n field: string;\n stableIfNotFound: true;\n }): { getDataAsString(): string | undefined } | undefined;\n}\n\n/** Either a listing to publish, or the reason there is nothing worth publishing yet. */\nexport type FoldersListingResult =\n | { readonly status: \"not-synced\"; readonly reason: string }\n | { readonly status: \"ready\"; readonly listing: FoldersListing };\n\n/**\n * Joins the folder singleton with an already-read project list.\n *\n * Split out of the computable so that the join, the first-paint rule and the degrade can be\n * exercised without a backend — the same reason {@link projectListEntries} is a function rather\n * than a closure. The interface it takes is the exact slice of the tree accessor it touches, so\n * the real accessor satisfies it structurally.\n *\n * The listing is built by mapping over the **project list**, never over the folder view, so no\n * folder input can drop a project. Do not refactor that into a map over the view.\n */\nexport function foldersListing(\n foldersNode: FoldersNode,\n entries: readonly ProjectListEntry[],\n templateEntries: readonly TemplateListEntry[] = [],\n): FoldersListingResult {\n // The field is absent on every account that has never made a folder, and that absence is\n // final until a write creates it.\n const documentNode = foldersNode.traverse({\n field: FoldersDocumentField,\n stableIfNotFound: true as const,\n });\n\n // A document field that exists but whose value has not arrived is not \"no folders\" — it is a\n // tree we cannot describe yet, and publishing a flat list here is exactly the first-paint\n // flicker this join exists to avoid.\n if (documentNode === undefined && foldersNode.listDynamicFields().includes(FoldersDocumentField))\n return { status: \"not-synced\", reason: \"folders_document_not_synced\" };\n\n // A node that is in the tree carries whatever data it had when the frame was taken; the tree\n // never fills data in later. So a document resource present with no blob is not a slow sync —\n // it is a document this build cannot read, and the read degrades to the flat list with writes\n // refused rather than waiting for something that will not arrive.\n const decoded = decodeStoredFoldersDocument(\n documentNode === undefined\n ? { present: false }\n : { present: true, raw: documentNode.getDataAsString() },\n );\n\n const view = healFolders(\n decoded,\n entries.map((entry) => ({ id: entry.id, name: entry.meta.label })),\n templateEntries.map((entry) => ({ id: entry.id, name: entry.label })),\n );\n\n return {\n status: \"ready\",\n listing: {\n folders: view.folders,\n projects: withPlacement(entries, view.projects),\n templates: withPlacement(templateEntries, view.templates),\n writable: view.writable,\n ...(view.problem === undefined ? {} : { problem: view.problem }),\n healed: view.healed,\n },\n };\n}\n\n/**\n * The one way the folder document is written by a folder operation.\n *\n * The whole body runs inside a single write transaction: the document and the project list are\n * re-read there, the edit is computed from what that read found, and only then is a new value\n * resource minted and the field re-pointed. The backend validates read sets, so a racing commit\n * aborts this one and the client replays the body against fresh state — which is only true\n * because the reading happens *inside* the body. Nothing may be carried across attempts, and\n * anything with an identity of its own (a folder id) is minted by the caller beforehand, so a\n * retry cannot hand back an id that was never stored.\n *\n * The advisory lock serialises writers inside one process. It is not a substitute for the\n * backend's conflict detection — it just removes the cheap case, one window racing itself.\n */\nexport async function withFolders<T>(\n pl: PlClient,\n name: string,\n rids: FoldersRids,\n body: (view: FoldersView) => FoldersEdit<T>,\n): Promise<T> {\n return await pl.withWriteTx(\n name,\n async (tx) => {\n const read = await readFolders(tx, rids);\n if (!read.decoded.writable) throw new Error(refusalMessage(read.decoded.problem));\n\n const edit = body(read.view);\n if (edit.document === undefined) return edit.result;\n\n rejectNewViolations(read, edit.document, edit.labelRenames ?? [], [\n ...(edit.deletedProjects ?? []),\n ...(edit.deletedTemplates ?? []),\n ]);\n writeFoldersDocument(tx, rids, read, edit.document);\n\n // A project's label is written straight into its metadata rather than through the project\n // mutator, which would migrate projects the user only asked to move. Everything else the\n // metadata carries — the description — is preserved by writing the value read here rather\n // than a fresh one. It is safe to do so because that value was read in this same\n // transaction: a writable transaction's point reads are conflict-tracked, so a metadata\n // write committing in parallel aborts one of the two and the loser is replayed against the\n // winner's state. A template's label is a value of its own and is simply replaced.\n const timestamp = JSON.stringify(Date.now());\n for (const rename of edit.labelRenames ?? []) {\n if (rename.item.kind === \"template\") {\n const rid = read.templateRids.get(rename.item.id);\n if (rid === undefined) throw notListedError(\"Template\", rename.item.id);\n renameTemplate(tx, rid, rename.name);\n continue;\n }\n const rid = read.projectRids.get(rename.item.id);\n const meta = read.projectMetas.get(rename.item.id);\n if (rid === undefined || meta === undefined)\n throw notListedError(\"Project\", rename.item.id);\n tx.setKValue(rid, ProjectMetaKey, JSON.stringify({ ...meta, label: rename.name }));\n tx.setKValue(rid, ProjectLastModifiedTimestamp, timestamp);\n }\n\n // Removing the field from the list is what deletes a project or a template, exactly as\n // MiddleLayer.deleteProject and deleteTemplate do it; here it rides the same transaction as\n // the document rewrite, so a folder never disappears while what it held survives, or the\n // reverse.\n await removeListedFields(tx, rids.projects, edit.deletedProjects ?? [], \"Project\");\n await removeListedFields(tx, rids.templates, edit.deletedTemplates ?? [], \"Template\");\n\n await tx.commit();\n return edit.result;\n },\n { lockId: foldersLockId(rids.folders) },\n );\n}\n\n/**\n * The same read the write path performs, in a read transaction — what a preview is computed from,\n * so that a preview and the write that follows it never disagree about how the tree was read.\n */\nexport async function readFoldersView(\n pl: PlClient,\n name: string,\n rids: FoldersRids,\n): Promise<FoldersView> {\n return await pl.withReadTx(name, async (tx) => {\n return (await readFolders(tx, rids)).view;\n });\n}\n\n/**\n * The folder tree inside a transaction that is not a folder operation of its own: one that\n * creates, renames or copies a project or a template and has to name it or place it.\n *\n * Opening it reads the tree once, and every call answers from that read. A placement updates\n * the tree in memory as well as in the transaction, so a later call on the same handle sees the\n * folders and assignments it wrote. An item created in the transaction after the handle was\n * opened is placed by id only: the handle never learned its name.\n *\n * Unlike {@link withFolders} this takes no advisory lock, because it does not own the\n * transaction. The backend's read-set validation still applies: a folder write committing in\n * parallel aborts one of the two and the loser is replayed.\n *\n * Every placement is best-effort towards a document that is not ours to rewrite — one written by\n * a newer build, or one nothing here can parse: it writes nothing and never throws, and the item\n * stays at the top level, exactly where it landed before folders existed. An item that could not\n * be placed is still an item, and failing its creation over the placement would be the worse\n * trade. A destination that no longer exists is different: it is a folder the caller named and\n * the user chose, so the placement throws and the creation fails with it.\n */\nexport interface FoldersTx {\n /** The tree as this transaction sees it, placements made through this handle included. */\n readonly view: FoldersView;\n /** Resource id of every project the list holds. */\n readonly projectRids: ReadonlyMap<ProjectId, SignedResourceId>;\n /** Resource id of every template the list holds under a label. */\n readonly templateRids: ReadonlyMap<TemplateId, SignedResourceId>;\n /**\n * Names already taken in a folder, or at the top level, for choosing one that is free before\n * anything is created.\n *\n * Empty when the folder document is not ours to read: nothing is then known about who sits\n * where, and inventing collisions out of that would rename copies for no reason.\n */\n namesTakenIn(folder: FolderId | undefined): string[];\n /** Names already taken where `project` sits — where something made from it lands. Empty for\n * the same reason {@link namesTakenIn} can be. */\n namesTakenBeside(project: ProjectId): string[];\n /** The folder holding a project, or undefined at the top level. */\n folderOf(project: ProjectId): FolderId | undefined;\n /**\n * Refuses a name for a project or template rename when one of its siblings already carries it.\n *\n * Renaming a project or a template is not a folder operation and does not go through\n * {@link withFolders} — it writes the item's own metadata. But it shares the namespace the\n * folder rule governs, so without this check a user could rename two things sitting in the\n * same folder to the same name, and the uniqueness the rest of the feature relies on would be\n * false from the day it shipped.\n *\n * The name is checked against the state the rename actually commits against, since the read\n * happened in the rename's own transaction. Four things it deliberately does not do:\n *\n * - It never refuses a blank name: projects and templates may carry an empty label.\n * - It never refuses a rename to what the item is already called, compared without regard to\n * case. An account predating folders may already hold two projects with the same name in\n * one place, because nothing enforced uniqueness before; such a pair must stay renameable,\n * and neither of them is rewritten by anything here.\n * - It stays silent when the item is not in the tree — a project whose metadata could not be\n * read — since there is then no current name to compare against and refusing would block a\n * rename on a transient read.\n * - It stays silent when the folder document is not ours to read. Such a document reads as\n * \"no folders\", which would put every project at the top level and turn this per-parent\n * check into a global one, refusing renames that are perfectly legal in the tree that is\n * actually stored. Folder *writes* are refused in that state; a rename is not a folder write\n * and must keep working.\n */\n assertNameFree(item: FoldersLeafItem, name: string): void;\n /** Puts items into a folder. Items destined for the top level need no assignment at all, so\n * nothing is written for them. */\n place(items: readonly FoldersLeafItem[], folder: FolderId | undefined): void;\n /**\n * Recreates a folder subtree under `destination`, or at the top level, and puts each item in\n * its own folder inside it.\n *\n * What travels is the shape of a subtree, not its identity: the incoming folder keys are the\n * caller's and mean nothing here, so a fresh id is minted for each. Only the root is renamed to\n * be free among the destination's siblings — the folders inside it are only ever compared with\n * each other, and they came in already distinct.\n *\n * @returns `unwritable` when the document is not ours to rewrite and nothing was grafted.\n */\n graft(subtree: FoldersGraft, destination: FolderId | undefined): \"grafted\" | \"unwritable\";\n}\n\n/** A subtree to graft: folders under keys local to the caller, and items naming the folder\n * holding them. */\nexport interface FoldersGraft {\n /** Key of the folder that becomes the grafted root; the one folder with no parent. */\n readonly root: string;\n readonly folders: Readonly<\n Record<\n string,\n { readonly name: string; readonly parent?: string; readonly description?: string }\n >\n >;\n readonly items: readonly (FoldersLeafItem & { readonly folder: string })[];\n}\n\n/** Opens the folder tree inside the caller's transaction; see {@link FoldersTx}. */\nexport async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promise<FoldersTx> {\n const read = await readFolders(tx, rids);\n const projects = read.view.projects.map(({ id, name }) => ({ id, name }));\n const templates = read.view.templates.map(({ id, name }) => ({ id, name }));\n\n let view = read.view;\n let document = foldersDocumentFromView(read.view);\n let documentFieldExists = read.documentFieldExists;\n\n const write = (next: FoldersDocument): void => {\n writeFoldersDocument(tx, rids, { ...read, documentFieldExists }, next);\n documentFieldExists = true;\n document = next;\n view = healFolders({ document: next, writable: true }, projects, templates);\n };\n\n const assertFolderExists = (folder: FolderId): void => {\n if (!view.folders.some((candidate) => candidate.id === folder))\n throw new Error(`Folder ${folder} does not exist.`);\n };\n\n const namesTakenIn = (folder: FolderId | undefined): string[] =>\n read.decoded.writable ? foldersSiblingNames(view, folder) : [];\n\n const folderOf = (project: ProjectId): FolderId | undefined =>\n view.projects.find((candidate) => candidate.id === project)?.folder;\n\n return {\n get view() {\n return view;\n },\n projectRids: read.projectRids,\n templateRids: read.templateRids,\n namesTakenIn,\n namesTakenBeside: (project) => namesTakenIn(folderOf(project)),\n folderOf,\n\n assertNameFree(item, name) {\n if (foldersNameBlank(name) || !read.decoded.writable) return;\n\n const placed =\n item.kind === \"project\"\n ? view.projects.find((candidate) => candidate.id === item.id)\n : view.templates.find((candidate) => candidate.id === item.id);\n if (placed === undefined) return;\n\n // Nothing is in collision with itself, so a rename that only changes the case of its own\n // name goes through even where a pre-existing duplicate sits beside it.\n if (foldersNameTaken(name, [placed.name])) return;\n\n const siblings = foldersSiblingNames(view, placed.folder, [item.id]);\n if (foldersNameTaken(name, siblings)) throw new Error(`\"${name}\" is already used here.`);\n },\n\n place(items, folder) {\n if (folder === undefined || items.length === 0 || !read.decoded.writable) return;\n assertFolderExists(folder);\n\n const assignments = { ...document.assignments };\n const templateAssignments = { ...document.templateAssignments };\n for (const item of items) {\n if (item.kind === \"project\") assignments[item.id] = folder;\n else templateAssignments[item.id] = folder;\n }\n write({ ...document, assignments, templateAssignments });\n },\n\n graft(subtree, destination) {\n if (!read.decoded.writable) return \"unwritable\";\n if (destination !== undefined) assertFolderExists(destination);\n\n const root: FoldersGraft[\"folders\"][string] | undefined = subtree.folders[subtree.root];\n if (root === undefined) throw new Error(`The subtree holds no folder ${subtree.root}.`);\n const rootName = foldersUniqueName(root.name, foldersSiblingNames(view, destination));\n\n const incoming = Object.keys(subtree.folders);\n const minted = new Map<string, FolderId>();\n for (const key of incoming) minted.set(key, asFolderId(randomUUID()));\n\n const folders = [...document.folders];\n for (const key of incoming) {\n const incomingFolder = subtree.folders[key];\n const parent =\n key === subtree.root\n ? destination\n : mustGet(minted, incomingFolder.parent ?? subtree.root);\n const description = normalizeDescription(incomingFolder.description);\n folders.push({\n id: mustGet(minted, key),\n name: key === subtree.root ? rootName : incomingFolder.name,\n ...(parent === undefined ? {} : { parent }),\n ...(description === undefined ? {} : { description }),\n });\n }\n\n const assignments = { ...document.assignments };\n const templateAssignments = { ...document.templateAssignments };\n for (const item of subtree.items) {\n const folder = minted.get(item.folder);\n if (folder === undefined) continue; // an item pointing at a folder that never arrived\n if (item.kind === \"project\") assignments[item.id] = folder;\n else templateAssignments[item.id] = folder;\n }\n\n write({ ...document, folders, assignments, templateAssignments });\n return \"grafted\";\n },\n };\n}\n\n/**\n * Creates a folder and returns its id.\n *\n * The name is typed by a human, so a name already used in the destination is rejected rather than\n * quietly suffixed — a text field that disagrees with what was typed is worse than one that says\n * no.\n */\nexport async function createFolder(\n pl: PlClient,\n rids: FoldersRids,\n name: string,\n parent?: FolderId,\n): Promise<FolderId> {\n if (foldersNameBlank(name)) throw new Error(\"A folder name cannot be empty.\");\n const wanted = name.trim();\n\n // Minted here, outside the transaction: an attempt that conflicts persists nothing, so an id\n // minted inside one could be handed back to the caller after never having been stored.\n const id = asFolderId(randomUUID());\n\n await withFolders(pl, \"MLCreateFolder\", rids, (view) => {\n if (parent !== undefined && !view.folders.some((folder) => folder.id === parent))\n throw new Error(`Folder ${parent} does not exist.`);\n if (foldersNameTaken(wanted, foldersSiblingNames(view, parent)))\n throw new Error(`\"${wanted}\" is already used here.`);\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: [\n ...base.folders,\n parent === undefined ? { id, name: wanted } : { id, name: wanted, parent },\n ],\n },\n };\n });\n\n return id;\n}\n\n/** Renames a folder. A human typed this name too, so a collision is rejected, not suffixed. */\nexport async function renameFolder(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n name: string,\n): Promise<void> {\n if (foldersNameBlank(name)) throw new Error(\"A folder name cannot be empty.\");\n const wanted = name.trim();\n\n await withFolders(pl, \"MLRenameFolder\", rids, (view) => {\n const target = view.folders.find((candidate) => candidate.id === folder);\n if (target === undefined) throw new Error(`Folder ${folder} does not exist.`);\n\n const siblings = foldersSiblingNames(view, target.parent, [folder]);\n if (foldersNameTaken(wanted, siblings)) throw new Error(`\"${wanted}\" is already used here.`);\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: base.folders.map((candidate) =>\n candidate.id === folder ? { ...candidate, name: wanted } : candidate,\n ),\n },\n };\n });\n}\n\n/**\n * Sets what a folder says about itself. Blank clears it.\n *\n * Unlike a name, a description is in no namespace: nothing is checked against the siblings, and\n * two folders may say the same thing about themselves.\n */\nexport async function setFolderDescription(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n description: string,\n): Promise<void> {\n const wanted = normalizeDescription(description);\n\n await withFolders(pl, \"MLSetFolderDescription\", rids, (view) => {\n const target = view.folders.find((candidate) => candidate.id === folder);\n if (target === undefined) throw new Error(`Folder ${folder} does not exist.`);\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: base.folders.map((candidate) => {\n if (candidate.id !== folder) return candidate;\n const { description: _dropped, ...rest } = candidate;\n return wanted === undefined ? rest : { ...rest, description: wanted };\n }),\n },\n };\n });\n}\n\n/**\n * The shape of a folder subtree under ids local to the caller: a fresh id per folder, the root's\n * own parent left behind so the subtree stands alone, and a lookup from a real folder id to its\n * local one.\n *\n * Both ways of copying a folder need this. A share carries the shape into another user's tree and\n * a duplicate rebuilds it beside the source, and neither may carry the folder ids themselves,\n * because the document that receives the subtree mints its own.\n */\nexport function foldersLocalSubtree<Id extends string>(\n view: FoldersView,\n root: FolderId,\n mint: () => Id,\n): {\n readonly inSubtree: ReadonlySet<FolderId>;\n readonly localId: (id: FolderId) => Id;\n readonly folders: Record<Id, { name: string; parent?: Id; description?: string }>;\n} {\n const inSubtree = new Set(foldersSubtree(view, root));\n\n const localIds = new Map<FolderId, Id>();\n for (const id of inSubtree) localIds.set(id, mint());\n const localId = (id: FolderId): Id => {\n const local = localIds.get(id);\n if (local === undefined) throw new Error(`Folder ${id} is not under ${root}.`);\n return local;\n };\n\n const folders = {} as Record<Id, { name: string; parent?: Id; description?: string }>;\n for (const candidate of view.folders) {\n if (!inSubtree.has(candidate.id)) continue;\n // The copied folder is the subtree's root: its own parent stays behind. Every other folder of\n // the subtree has its parent in the subtree too.\n const parent = candidate.id === root ? undefined : localId(candidate.parent ?? root);\n folders[localId(candidate.id)] = {\n name: candidate.name,\n ...(parent === undefined ? {} : { parent }),\n ...(candidate.description === undefined ? {} : { description: candidate.description }),\n };\n }\n\n return { inSubtree, localId, folders };\n}\n\n/**\n * The plan a move would produce, computed from a fresh read — what the confirmation dialog shows.\n * The same planner runs again inside the write, over the same shape of read.\n */\nexport async function previewFoldersMove(\n pl: PlClient,\n rids: FoldersRids,\n items: readonly FoldersItem[],\n destination?: FolderId,\n): Promise<FoldersMovePlanResult> {\n const view = await readFoldersView(pl, \"MLPreviewFoldersMove\", rids);\n return planFoldersMove(view, items, destination);\n}\n\n/**\n * Moves any mix of folders, projects and templates into one destination, committing only when\n * the plan recomputed inside the transaction is identical to `confirmedPlan`.\n */\nexport async function moveFolderItems(\n pl: PlClient,\n rids: FoldersRids,\n items: readonly FoldersItem[],\n destination: FolderId | undefined,\n confirmedPlan: FoldersMovePlan,\n): Promise<FoldersMoveOutcome> {\n return await withFolders(pl, \"MLMoveFolderItems\", rids, (view) =>\n commitFoldersMove(view, planFoldersMove(view, items, destination), confirmedPlan),\n );\n}\n\n/** What deleting a folder would destroy: the subtree of folders, and every project and template\n * in it. */\nexport async function previewFolderDeletion(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n): Promise<FoldersRemovalPlanResult> {\n const view = await readFoldersView(pl, \"MLPreviewFolderDeletion\", rids);\n return planFoldersRemoval(view, folder);\n}\n\n/**\n * Deletes a folder, every folder inside it, and every project and template held anywhere in that\n * subtree.\n *\n * Deletion cannot be undone, so `confirmedRemoval` is required: the removal is recomputed here and\n * the write commits only if it names exactly the same things the caller confirmed. A project\n * dropped into the folder after the dialog opened therefore aborts the deletion rather than being\n * destroyed unseen.\n */\nexport async function deleteFolder(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n confirmedRemoval?: FoldersRemoval,\n): Promise<FoldersRemovalOutcome> {\n return await withFolders(pl, \"MLDeleteFolder\", rids, (view) =>\n commitFoldersRemoval(view, planFoldersRemoval(view, folder), confirmedRemoval),\n );\n}\n\n/**\n * Replaces a folder document this build cannot read with an empty one: no folders and no\n * assignments, so every project and template shows at the top level. Nothing but the document is\n * touched, and what the unreadable tree held is lost for good.\n *\n * It is refused while the document reads fine, so a working tree can never be wiped through it.\n * \"Reads fine\" is decided by the same decode the listing uses, in the transaction that writes.\n */\nexport async function resetFolders(pl: PlClient, rids: FoldersRids): Promise<void> {\n await pl.withWriteTx(\n \"MLResetFolders\",\n async (tx) => {\n const stored = await readStoredFolders(tx, rids);\n if (stored.decoded.writable)\n throw new Error(\n \"Folders can only be reset when they cannot be read, and these can be read. Nothing was changed.\",\n );\n\n writeFoldersDocument(\n tx,\n rids,\n { carriedAssignments: {}, documentFieldExists: stored.documentFieldExists },\n emptyFoldersDocument(),\n );\n await tx.commit();\n },\n { lockId: foldersLockId(rids.folders) },\n );\n}\n\n//\n// Internals\n//\n\n/** A key looked up in a map that was built from those very keys; absent means the code above\n * changed and the two no longer agree. */\nfunction mustGet<K, V>(map: Map<K, V>, key: K): V {\n const value = map.get(key);\n if (value === undefined) throw new Error(`No entry for ${String(key)}.`);\n return value;\n}\n\n/** Each entry of a list with its place in the tree, looked up by id; an entry the tree does not\n * place is at the top level. */\nfunction withPlacement<E extends { readonly id: string }>(\n entries: readonly E[],\n placed: readonly ({ readonly id: string } & FoldersPlacement)[],\n): (E & FoldersPlacement)[] {\n const byId = new Map(placed.map((item) => [item.id, item]));\n return entries.map((entry) => {\n const placement = byId.get(entry.id);\n return {\n ...entry,\n ...(placement?.folder === undefined ? {} : { folder: placement.folder }),\n ancestors: placement?.ancestors ?? [],\n path: placement?.path ?? [],\n };\n });\n}\n\ninterface FoldersRead {\n readonly view: FoldersView;\n readonly decoded: FoldersDecoded;\n readonly projectRids: ReadonlyMap<ProjectId, SignedResourceId>;\n readonly projectMetas: ReadonlyMap<ProjectId, ProjectMeta>;\n readonly templateRids: ReadonlyMap<TemplateId, SignedResourceId>;\n readonly documentFieldExists: boolean;\n /**\n * Assignments of projects that are in the list but whose metadata could not be read. They are\n * missing from the view, so a document rebuilt from the view would drop them — and a project\n * whose folder is forgotten because its metadata was briefly unreadable is a loss, not healing.\n */\n readonly carriedAssignments: Readonly<Record<string, FolderId>>;\n}\n\n/**\n * The folder document and both item lists, read inside a transaction.\n *\n * An absent document field is a user with no folders, and is writable. A field that points at\n * something carrying no readable blob is a document that exists and cannot be read here, and must\n * refuse writes, or the first folder edit replaces that tree with an empty one.\n *\n * A list entry counts as a project when it carries project metadata, and as a template when it\n * carries a template label. Nothing else is checked. The listing readers are stricter:\n * {@link projectListEntries} also requires the project resource type by name and both the created\n * and the last-modified timestamps, and {@link templateListEntries} also requires the template\n * resource type by name, the data blob and the created timestamp. So every entry the listing shows\n * is kept here, and so is an entry the listing skips because it has only partly synced or is a\n * foreign resource carrying one of those keys: it still holds its name and its folder assignment,\n * which is the safe side for a write. Telling a foreign resource apart by its type instead would\n * read the state of every resource in the list, and a read of a project's state conflicts with\n * every write that project's own session makes, so a folder edit would keep losing to any project\n * that is open.\n */\nasync function readFolders(tx: PlTransaction, rids: FoldersRids): Promise<FoldersRead> {\n const { decoded, documentFieldExists } = await readStoredFolders(tx, rids);\n\n const [listedProjects, listedTemplates] = await Promise.all([\n listedById(tx, rids.projects),\n listedById(tx, rids.templates),\n ]);\n const [metas, labels] = await Promise.all([\n Promise.all(\n [...listedProjects.values()].map(({ rid }) =>\n tx.getKValueJsonIfExists<ProjectMeta>(rid, ProjectMetaKey),\n ),\n ),\n Promise.all(\n [...listedTemplates.values()].map(({ rid }) =>\n tx.getKValueJsonIfExists<string>(rid, TemplateLabelKey),\n ),\n ),\n ]);\n\n const projectRids = new Map<ProjectId, SignedResourceId>();\n const projectMetas = new Map<ProjectId, ProjectMeta>();\n const projects: FoldersProjectInput[] = [];\n const carriedAssignments: Record<string, FolderId> = {};\n [...listedProjects.values()].forEach(({ rid }, index) => {\n const id = asProjectId(resourceIdToString(rid));\n projectRids.set(id, rid);\n const meta = metas[index];\n if (meta === undefined) {\n const folder = decoded.document.assignments[id];\n if (folder !== undefined) carriedAssignments[id] = folder;\n return;\n }\n projectMetas.set(id, meta);\n projects.push({ id, name: meta.label });\n });\n\n // Labels as well as ids: a template is named in the same namespace as folders and projects.\n const templateRids = new Map<TemplateId, SignedResourceId>();\n const templates: FoldersTemplateInput[] = [];\n [...listedTemplates.values()].forEach(({ rid }, index) => {\n const label = labels[index];\n if (label === undefined) return;\n const id = asTemplateId(resourceIdToString(rid));\n templateRids.set(id, rid);\n templates.push({ id, name: label });\n });\n\n return {\n view: healFolders(decoded, projects, templates),\n decoded,\n projectRids,\n projectMetas,\n templateRids,\n documentFieldExists,\n carriedAssignments,\n };\n}\n\n/** The folder document alone, decoded, and whether its field exists at all. */\nasync function readStoredFolders(\n tx: PlTransaction,\n rids: FoldersRids,\n): Promise<Pick<FoldersRead, \"decoded\" | \"documentFieldExists\">> {\n const documentField = await tx.getFieldIfExists(field(rids.folders, FoldersDocumentField));\n const decoded = decodeStoredFoldersDocument(\n documentField === undefined || isNullSignedResourceId(documentField.value)\n ? { present: false }\n : { present: true, raw: await readDocumentText(tx, documentField.value) },\n );\n return { decoded, documentFieldExists: documentField !== undefined };\n}\n\n/** The stored JSON of a document value, or undefined when the value carries no blob. */\nasync function readDocumentText(\n tx: PlTransaction,\n rid: SignedResourceId,\n): Promise<string | undefined> {\n const data = await tx.getResourceData(rid, false);\n return data.data === undefined ? undefined : Buffer.from(data.data).toString(\"utf-8\");\n}\n\n/**\n * Persists a folder document: the assignments the read had to carry are added back, a new value\n * resource is minted, and the one document field is created or re-pointed at it.\n */\nfunction writeFoldersDocument(\n tx: PlTransaction,\n rids: FoldersRids,\n read: Pick<FoldersRead, \"carriedAssignments\" | \"documentFieldExists\">,\n document: FoldersDocument,\n): void {\n const ref = tx.createJsonValue(withCarriedAssignments(document, read.carriedAssignments));\n const documentField = field(rids.folders, FoldersDocumentField);\n if (read.documentFieldExists) tx.setField(documentField, ref);\n else tx.createField(documentField, \"Dynamic\", ref);\n}\n\n/**\n * A document written by a newer build, or one nothing here can parse, reads as no folders so that\n * the project list still renders — and every write is refused, because degrading the read while\n * still writing would silently replace that tree with this build's truncated view of it.\n */\nfunction refusalMessage(problem: FoldersDocumentProblem): string {\n return (\n `Folders cannot be changed: ${problem.message}. ` +\n `Update the application to a version that understands this folder document.`\n );\n}\n\nfunction withCarriedAssignments(\n document: FoldersDocument,\n carried: Readonly<Record<string, FolderId>>,\n): FoldersDocument {\n const entries = Object.entries(carried).filter(([, folder]) =>\n document.folders.some((candidate) => candidate.id === folder),\n );\n if (entries.length === 0) return document;\n return { ...document, assignments: { ...document.assignments, ...Object.fromEntries(entries) } };\n}\n\n/**\n * Refuses a write that would put a *new* rule violation into the document.\n *\n * Only new ones: accounts predating folders may already hold two projects with the same name in\n * one place, because nothing enforced uniqueness before, and a write that merely carries such a\n * collision along must not be blocked by it. What the folder machinery must never do is create\n * one.\n *\n * What the write deletes is left out of the document it leaves behind. A deleted project or\n * template has no folder any more, and counted anyway it would stand at the top level under its\n * old name — where it clashes with whatever is really there, and refuses the deletion.\n */\nfunction rejectNewViolations(\n read: FoldersRead,\n document: FoldersDocument,\n renames: readonly FoldersLabelRename[],\n deleted: readonly string[],\n): void {\n const renamed = new Map<string, string>(renames.map((rename) => [rename.item.id, rename.name]));\n const gone = new Set<string>(deleted);\n const templates = read.view.templates.map(({ id, name }) => ({ id, name }));\n const before = validateFoldersDocument(\n foldersDocumentFromView(read.view),\n [...read.projectMetas].map(([id, meta]) => ({ id, name: meta.label })),\n templates,\n );\n const after = validateFoldersDocument(\n document,\n [...read.projectMetas]\n .filter(([id]) => !gone.has(id))\n .map(([id, meta]) => ({ id, name: renamed.get(id) ?? meta.label })),\n templates\n .filter(({ id }) => !gone.has(id))\n .map(({ id, name }) => ({ id, name: renamed.get(id) ?? name })),\n );\n\n const known = new Set(before.map(violationKey));\n const introduced = after.filter((violation) => !known.has(violationKey(violation)));\n if (introduced.length === 0) return;\n\n throw new Error(`Folder change refused: ${introduced.map(formatFoldersViolation).join(\"; \")}.`);\n}\n\n/**\n * Deletes the named items from one of the root's lists. Every id must be there: an id the list\n * does not hold means the write is deleting something other than what was planned, and the\n * transaction is abandoned rather than allowed to destroy a guess.\n */\nasync function removeListedFields(\n tx: PlTransaction,\n listRid: SignedResourceId,\n ids: readonly string[],\n kind: ListedKind,\n): Promise<void> {\n if (ids.length === 0) return;\n\n const listed = await listedById(tx, listRid);\n for (const id of new Set(ids)) {\n const entry = listed.get(id);\n if (entry === undefined) throw notListedError(kind, id);\n tx.removeField(field(listRid, entry.fieldName));\n }\n}\n\nfunction violationKey(violation: FoldersViolation): string {\n return JSON.stringify(violation);\n}\n\n/**\n * Scoped to the singleton rather than to the process, so that two middle layers over two users'\n * roots — an impersonating admin session and the admin's own — do not queue behind each other.\n */\nfunction foldersLockId(foldersRid: SignedResourceId): string {\n return `folders:${foldersRid}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"folders.js","names":[],"sources":["../../src/middle_layer/folders.ts"],"sourcesContent":["import type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { WatchableValue } from \"@milaboratories/computable\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type {\n FoldersDecoded,\n FoldersDocument,\n FoldersDocumentProblem,\n FoldersEdit,\n FolderId,\n FolderView,\n FoldersItem,\n FoldersLabelRename,\n FoldersLeafItem,\n FoldersMoveOutcome,\n FoldersMovePlan,\n FoldersMovePlanResult,\n FoldersPlacement,\n FoldersProjectInput,\n FoldersRemoval,\n FoldersRemovalOutcome,\n FoldersRemovalPlanResult,\n FoldersTemplateInput,\n FoldersView,\n FoldersViolation,\n ProjectMeta,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport { asProjectId, asTemplateId } from \"@milaboratories/pl-model-common\";\nimport {\n asFolderId,\n commitFoldersMove,\n commitFoldersRemoval,\n decodeStoredFoldersDocument,\n emptyFoldersDocument,\n formatFoldersViolation,\n healFolders,\n planFoldersRemoval,\n planFoldersMove,\n foldersDocumentFromView,\n foldersNameBlank,\n foldersNameTaken,\n foldersSiblingNames,\n foldersSubtree,\n foldersUniqueName,\n normalizeDescription,\n validateFoldersDocument,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectId, ProjectListEntry } from \"../model/project_model\";\nimport { ProjectLastModifiedTimestamp, ProjectMetaKey } from \"../model/project_model\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { projectListEntries } from \"./project_list\";\nimport type { TemplateId, TemplateListEntry } from \"./template_list\";\nimport { TemplateLabelKey, templateListEntries } from \"./template_list\";\nimport { renameTemplate } from \"../mutator/template\";\nimport type { ListedKind } from \"../mutator/list\";\nimport { listedById, notListedError } from \"../mutator/list\";\n\n/** Field on the user's client root holding the folder singleton. */\nexport const FoldersField = \"folders\";\nexport const FoldersResourceType: ResourceType = { name: \"Folders\", version: \"1\" };\n\n/**\n * The one field of the singleton. It holds the whole tree as a single immutable JSON value\n * resource; a write mints a new value and re-points this field at it, the way a block's stored\n * state is rewritten.\n */\nexport const FoldersDocumentField = \"document\";\n\n/** A project list entry with its place in the folder tree resolved. */\nexport type FoldersProjectEntry = ProjectListEntry & FoldersPlacement;\n\n/** A template list entry with its place in the folder tree resolved. */\nexport type FoldersTemplateEntry = TemplateListEntry & FoldersPlacement;\n\n/**\n * The single value the folder feature puts across the API boundary: the tree and the project list\n * already joined.\n *\n * Two independently refreshed values would let a project show up in a folder the tree has not\n * heard of, with nobody owning the reconciliation, so the join happens here and the desktop never\n * sees the halves.\n */\nexport interface FoldersListing {\n readonly folders: readonly FolderView[];\n /** Every project, most recently modified first. Nothing here is ever dropped. */\n readonly projects: readonly FoldersProjectEntry[];\n /** Every stored template, most recently created first. Nothing here is ever dropped. */\n readonly templates: readonly FoldersTemplateEntry[];\n /** False when every folder write is refused, because the document is not ours to rewrite. */\n readonly writable: boolean;\n /** Present when the stored document could not be used as it stands. */\n readonly problem?: FoldersDocumentProblem;\n /** True when what the listing shows differs from what is stored, because the read healed it. */\n readonly healed: boolean;\n}\n\n/** Every singleton a folder operation touches. */\nexport interface FoldersRids {\n readonly folders: SignedResourceId;\n readonly projects: SignedResourceId;\n readonly templates: SignedResourceId;\n}\n\n/**\n * Resolves the folder singleton on the transaction's client root, lazily creating (and locking)\n * an empty one when {@link FoldersField} is not yet populated. Same shape as the\n * project-list and template-list singletons beside it.\n */\nexport async function ensureFoldersRid(tx: PlTransaction): Promise<SignedResourceId> {\n const f = field(tx.clientRoot, FoldersField);\n tx.createField(f, \"Dynamic\");\n const fData = await tx.getField(f);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(FoldersResourceType);\n tx.lock(ref);\n tx.setField(f, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const FoldersTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, FoldersResourceType)) return [];\n return resource.fields;\n};\n\nexport const foldersFieldFilter: Filter = treeFilter.resourceTypeEq(FoldersResourceType.name);\n\n/**\n * The folder tree plus the computable that joins it with the project list.\n *\n * Both halves are read from one computable context, so the listing is always one consistent\n * snapshot. Nothing is published while either half is still syncing: a project would otherwise\n * render at the top level for a poll or two and then jump into its folder.\n */\nexport async function createFolderList(\n pl: PlClient,\n rid: SignedResourceId,\n projectsTree: SynchronizedTreeState,\n templatesTree: SynchronizedTreeState,\n openedProjects: WatchableValue<ProjectId[]>,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<FoldersListing>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: FoldersTreePruningFunction,\n fieldFilter: foldersFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const projectsNode = ctx.accessor(projectsTree.entry()).node();\n const templatesNode = ctx.accessor(templatesTree.entry()).node();\n const foldersNode = ctx.accessor(tree.entry()).node();\n if (projectsNode === undefined || templatesNode === undefined || foldersNode === undefined) {\n ctx.markUnstable(\"folders_not_synced\");\n return undefined;\n }\n\n const entries = projectListEntries(projectsNode, openedProjects.getValue(ctx));\n const templates = templateListEntries(templatesNode);\n const result = foldersListing(foldersNode, entries, templates);\n if (result.status === \"not-synced\") {\n ctx.markUnstable(result.reason);\n return undefined;\n }\n return result.listing;\n }).withStableType();\n\n return { computable: c, tree };\n}\n\n/** The part of a tree node the folder reader needs from the folder singleton. */\nexport interface FoldersNode {\n listDynamicFields(): string[];\n traverse(step: {\n field: string;\n stableIfNotFound: true;\n }): { getDataAsString(): string | undefined } | undefined;\n}\n\n/** Either a listing to publish, or the reason there is nothing worth publishing yet. */\nexport type FoldersListingResult =\n | { readonly status: \"not-synced\"; readonly reason: string }\n | { readonly status: \"ready\"; readonly listing: FoldersListing };\n\n/**\n * Joins the folder singleton with an already-read project list.\n *\n * Split out of the computable so that the join, the first-paint rule and the degrade can be\n * exercised without a backend — the same reason {@link projectListEntries} is a function rather\n * than a closure. The interface it takes is the exact slice of the tree accessor it touches, so\n * the real accessor satisfies it structurally.\n *\n * The listing is built by mapping over the **project list**, never over the folder view, so no\n * folder input can drop a project. Do not refactor that into a map over the view.\n */\nexport function foldersListing(\n foldersNode: FoldersNode,\n entries: readonly ProjectListEntry[],\n templateEntries: readonly TemplateListEntry[] = [],\n): FoldersListingResult {\n // The field is absent on every account that has never made a folder, and that absence is\n // final until a write creates it.\n const documentNode = foldersNode.traverse({\n field: FoldersDocumentField,\n stableIfNotFound: true as const,\n });\n\n // A document field that exists but whose value has not arrived is not \"no folders\" — it is a\n // tree we cannot describe yet, and publishing a flat list here is exactly the first-paint\n // flicker this join exists to avoid.\n if (documentNode === undefined && foldersNode.listDynamicFields().includes(FoldersDocumentField))\n return { status: \"not-synced\", reason: \"folders_document_not_synced\" };\n\n // A node that is in the tree carries whatever data it had when the frame was taken; the tree\n // never fills data in later. So a document resource present with no blob is not a slow sync —\n // it is a document this build cannot read, and the read degrades to the flat list with writes\n // refused rather than waiting for something that will not arrive.\n const decoded = decodeStoredFoldersDocument(\n documentNode === undefined\n ? { present: false }\n : { present: true, raw: documentNode.getDataAsString() },\n );\n\n const view = healFolders(\n decoded,\n entries.map((entry) => ({ id: entry.id, name: entry.meta.label })),\n templateEntries.map((entry) => ({ id: entry.id, name: entry.label })),\n );\n\n return {\n status: \"ready\",\n listing: {\n folders: view.folders,\n projects: withPlacement(entries, view.projects),\n templates: withPlacement(templateEntries, view.templates),\n writable: view.writable,\n ...(view.problem === undefined ? {} : { problem: view.problem }),\n healed: view.healed,\n },\n };\n}\n\n/**\n * The one way the folder document is written by a folder operation.\n *\n * The whole body runs inside a single write transaction: the document and the project list are\n * re-read there, the edit is computed from what that read found, and only then is a new value\n * resource minted and the field re-pointed. The backend validates read sets, so a racing commit\n * aborts this one and the client replays the body against fresh state — which is only true\n * because the reading happens *inside* the body. Nothing may be carried across attempts, and\n * anything with an identity of its own (a folder id) is minted by the caller beforehand, so a\n * retry cannot hand back an id that was never stored.\n *\n * The advisory lock serialises writers inside one process. It is not a substitute for the\n * backend's conflict detection — it just removes the cheap case, one window racing itself.\n */\nexport async function withFolders<T>(\n pl: PlClient,\n name: string,\n rids: FoldersRids,\n body: (view: FoldersView) => FoldersEdit<T>,\n): Promise<T> {\n return await pl.withWriteTx(\n name,\n async (tx) => {\n const read = await readFolders(tx, rids);\n if (!read.decoded.writable) throw new Error(refusalMessage(read.decoded.problem));\n\n const edit = body(read.view);\n if (edit.document === undefined) return edit.result;\n\n rejectNewViolations(read, edit.document, edit.labelRenames ?? [], [\n ...(edit.deletedProjects ?? []),\n ...(edit.deletedTemplates ?? []),\n ]);\n writeFoldersDocument(tx, rids, read, edit.document);\n\n // A project's label is written straight into its metadata rather than through the project\n // mutator, which would migrate projects the user only asked to move. Everything else the\n // metadata carries — the description — is preserved by writing the value read here rather\n // than a fresh one. It is safe to do so because that value was read in this same\n // transaction: a writable transaction's point reads are conflict-tracked, so a metadata\n // write committing in parallel aborts one of the two and the loser is replayed against the\n // winner's state. A template's label is a value of its own and is simply replaced.\n const timestamp = JSON.stringify(Date.now());\n for (const rename of edit.labelRenames ?? []) {\n if (rename.item.kind === \"template\") {\n const rid = read.templateRids.get(rename.item.id);\n if (rid === undefined) throw notListedError(\"Template\", rename.item.id);\n renameTemplate(tx, rid, rename.name);\n continue;\n }\n const rid = read.projectRids.get(rename.item.id);\n const meta = read.projectMetas.get(rename.item.id);\n if (rid === undefined || meta === undefined)\n throw notListedError(\"Project\", rename.item.id);\n tx.setKValue(rid, ProjectMetaKey, JSON.stringify({ ...meta, label: rename.name }));\n tx.setKValue(rid, ProjectLastModifiedTimestamp, timestamp);\n }\n\n // Removing the field from the list is what deletes a project or a template, exactly as\n // MiddleLayer.deleteProject and deleteTemplate do it; here it rides the same transaction as\n // the document rewrite, so a folder never disappears while what it held survives, or the\n // reverse.\n await removeListedFields(tx, rids.projects, edit.deletedProjects ?? [], \"Project\");\n await removeListedFields(tx, rids.templates, edit.deletedTemplates ?? [], \"Template\");\n\n await tx.commit();\n return edit.result;\n },\n { lockId: foldersLockId(rids.folders) },\n );\n}\n\n/**\n * The same read the write path performs, in a read transaction — what a preview is computed from,\n * so that a preview and the write that follows it never disagree about how the tree was read.\n */\nexport async function readFoldersView(\n pl: PlClient,\n name: string,\n rids: FoldersRids,\n): Promise<FoldersView> {\n return await pl.withReadTx(name, async (tx) => {\n return (await readFolders(tx, rids)).view;\n });\n}\n\n/**\n * The folder tree inside a transaction that is not a folder operation of its own: one that\n * creates, renames or copies a project or a template and has to name it or place it.\n *\n * Opening it reads the tree once, and every call answers from that read. A placement updates\n * the tree in memory as well as in the transaction, so a later call on the same handle sees the\n * folders and assignments it wrote. An item created in the transaction after the handle was\n * opened is placed by id only: the handle never learned its name.\n *\n * Unlike {@link withFolders} this takes no advisory lock, because it does not own the\n * transaction. The backend's read-set validation still applies: a folder write committing in\n * parallel aborts one of the two and the loser is replayed.\n *\n * Every placement is best-effort towards a document that is not ours to rewrite — one written by\n * a newer build, or one nothing here can parse: it writes nothing and never throws, and the item\n * stays at the top level, exactly where it landed before folders existed. An item that could not\n * be placed is still an item, and failing its creation over the placement would be the worse\n * trade. A destination that no longer exists is different: it is a folder the caller named and\n * the user chose, so the placement throws and the creation fails with it.\n */\nexport interface FoldersTx {\n /** The tree as this transaction sees it, placements made through this handle included. */\n readonly view: FoldersView;\n /** Resource id of every project the list holds. */\n readonly projectRids: ReadonlyMap<ProjectId, SignedResourceId>;\n /** Resource id of every template the list holds under a label. */\n readonly templateRids: ReadonlyMap<TemplateId, SignedResourceId>;\n /**\n * Names already taken by items of one kind in a folder, or at the top level, for choosing one\n * that is free before anything of that kind is created. Each kind has a namespace of its own.\n *\n * Empty when the folder document is not ours to read: nothing is then known about who sits\n * where, and inventing collisions out of that would rename copies for no reason.\n */\n namesTakenIn(folder: FolderId | undefined, kind: FoldersItem[\"kind\"]): string[];\n /** Names already taken by items of one kind where `project` sits — where something made from it\n * lands. Empty for the same reason {@link namesTakenIn} can be. */\n namesTakenBeside(project: ProjectId, kind: FoldersItem[\"kind\"]): string[];\n /** The folder holding a project, or undefined at the top level. */\n folderOf(project: ProjectId): FolderId | undefined;\n /**\n * Refuses a name for a project or template rename when a sibling of the same kind already\n * carries it. A folder, a project and a template beside each other may share a name; two\n * projects, or two templates, may not.\n *\n * Renaming a project or a template is not a folder operation and does not go through\n * {@link withFolders} — it writes the item's own metadata. But the folder rule governs its\n * name, so without this check a user could rename two projects sitting in the same folder to\n * the same name, and the uniqueness the rest of the feature relies on would be false from the\n * day it shipped.\n *\n * The name is checked against the state the rename actually commits against, since the read\n * happened in the rename's own transaction. Four things it deliberately does not do:\n *\n * - It never refuses a blank name: projects and templates may carry an empty label.\n * - It never refuses a rename to what the item is already called, compared without regard to\n * case. An account predating folders may already hold two projects with the same name in\n * one place, because nothing enforced uniqueness before; such a pair must stay renameable,\n * and neither of them is rewritten by anything here.\n * - It stays silent when the item is not in the tree — a project whose metadata could not be\n * read — since there is then no current name to compare against and refusing would block a\n * rename on a transient read.\n * - It stays silent when the folder document is not ours to read. Such a document reads as\n * \"no folders\", which would put every project at the top level and turn this per-parent\n * check into a global one, refusing renames that are perfectly legal in the tree that is\n * actually stored. Folder *writes* are refused in that state; a rename is not a folder write\n * and must keep working.\n */\n assertNameFree(item: FoldersLeafItem, name: string): void;\n /** Puts items into a folder. Items destined for the top level need no assignment at all, so\n * nothing is written for them. */\n place(items: readonly FoldersLeafItem[], folder: FolderId | undefined): void;\n /**\n * Recreates a folder subtree under `destination`, or at the top level, and puts each item in\n * its own folder inside it.\n *\n * What travels is the shape of a subtree, not its identity: the incoming folder keys are the\n * caller's and mean nothing here, so a fresh id is minted for each. Only the root is renamed to\n * be free among the folders in the destination — the folders inside it are only ever compared\n * with each other, and they came in already distinct.\n *\n * @returns `unwritable` when the document is not ours to rewrite and nothing was grafted.\n */\n graft(subtree: FoldersGraft, destination: FolderId | undefined): \"grafted\" | \"unwritable\";\n}\n\n/** A subtree to graft: folders under keys local to the caller, and items naming the folder\n * holding them. */\nexport interface FoldersGraft {\n /** Key of the folder that becomes the grafted root; the one folder with no parent. */\n readonly root: string;\n readonly folders: Readonly<\n Record<\n string,\n { readonly name: string; readonly parent?: string; readonly description?: string }\n >\n >;\n readonly items: readonly (FoldersLeafItem & { readonly folder: string })[];\n}\n\n/** Opens the folder tree inside the caller's transaction; see {@link FoldersTx}. */\nexport async function openFoldersTx(tx: PlTransaction, rids: FoldersRids): Promise<FoldersTx> {\n const read = await readFolders(tx, rids);\n const projects = read.view.projects.map(({ id, name }) => ({ id, name }));\n const templates = read.view.templates.map(({ id, name }) => ({ id, name }));\n\n let view = read.view;\n let document = foldersDocumentFromView(read.view);\n let documentFieldExists = read.documentFieldExists;\n\n const write = (next: FoldersDocument): void => {\n writeFoldersDocument(tx, rids, { ...read, documentFieldExists }, next);\n documentFieldExists = true;\n document = next;\n view = healFolders({ document: next, writable: true }, projects, templates);\n };\n\n const assertFolderExists = (folder: FolderId): void => {\n if (!view.folders.some((candidate) => candidate.id === folder))\n throw new Error(`Folder ${folder} does not exist.`);\n };\n\n const namesTakenIn = (folder: FolderId | undefined, kind: FoldersItem[\"kind\"]): string[] =>\n read.decoded.writable ? foldersSiblingNames(view, folder, [], { kind }) : [];\n\n const folderOf = (project: ProjectId): FolderId | undefined =>\n view.projects.find((candidate) => candidate.id === project)?.folder;\n\n return {\n get view() {\n return view;\n },\n projectRids: read.projectRids,\n templateRids: read.templateRids,\n namesTakenIn,\n namesTakenBeside: (project, kind) => namesTakenIn(folderOf(project), kind),\n folderOf,\n\n assertNameFree(item, name) {\n if (foldersNameBlank(name) || !read.decoded.writable) return;\n\n const placed =\n item.kind === \"project\"\n ? view.projects.find((candidate) => candidate.id === item.id)\n : view.templates.find((candidate) => candidate.id === item.id);\n if (placed === undefined) return;\n\n // Nothing is in collision with itself, so a rename that only changes the case of its own\n // name goes through even where a pre-existing duplicate sits beside it.\n if (foldersNameTaken(name, [placed.name])) return;\n\n const siblings = foldersSiblingNames(view, placed.folder, [item.id], { kind: item.kind });\n if (foldersNameTaken(name, siblings)) throw new Error(nameTakenMessage(item.kind, name));\n },\n\n place(items, folder) {\n if (folder === undefined || items.length === 0 || !read.decoded.writable) return;\n assertFolderExists(folder);\n\n const assignments = { ...document.assignments };\n const templateAssignments = { ...document.templateAssignments };\n for (const item of items) {\n if (item.kind === \"project\") assignments[item.id] = folder;\n else templateAssignments[item.id] = folder;\n }\n write({ ...document, assignments, templateAssignments });\n },\n\n graft(subtree, destination) {\n if (!read.decoded.writable) return \"unwritable\";\n if (destination !== undefined) assertFolderExists(destination);\n\n const root: FoldersGraft[\"folders\"][string] | undefined = subtree.folders[subtree.root];\n if (root === undefined) throw new Error(`The subtree holds no folder ${subtree.root}.`);\n const rootName = foldersUniqueName(\n root.name,\n foldersSiblingNames(view, destination, [], { kind: \"folder\" }),\n );\n\n const incoming = Object.keys(subtree.folders);\n const minted = new Map<string, FolderId>();\n for (const key of incoming) minted.set(key, asFolderId(randomUUID()));\n\n const folders = [...document.folders];\n for (const key of incoming) {\n const incomingFolder = subtree.folders[key];\n const parent =\n key === subtree.root\n ? destination\n : mustGet(minted, incomingFolder.parent ?? subtree.root);\n const description = normalizeDescription(incomingFolder.description);\n folders.push({\n id: mustGet(minted, key),\n name: key === subtree.root ? rootName : incomingFolder.name,\n ...(parent === undefined ? {} : { parent }),\n ...(description === undefined ? {} : { description }),\n });\n }\n\n const assignments = { ...document.assignments };\n const templateAssignments = { ...document.templateAssignments };\n for (const item of subtree.items) {\n const folder = minted.get(item.folder);\n if (folder === undefined) continue; // an item pointing at a folder that never arrived\n if (item.kind === \"project\") assignments[item.id] = folder;\n else templateAssignments[item.id] = folder;\n }\n\n write({ ...document, folders, assignments, templateAssignments });\n return \"grafted\";\n },\n };\n}\n\n/**\n * Creates a folder and returns its id.\n *\n * The name is typed by a human, so a name another folder in the destination already carries is\n * rejected rather than quietly suffixed — a text field that disagrees with what was typed is worse\n * than one that says no. A project or a template of that name beside it is no obstacle.\n */\nexport async function createFolder(\n pl: PlClient,\n rids: FoldersRids,\n name: string,\n parent?: FolderId,\n): Promise<FolderId> {\n if (foldersNameBlank(name)) throw new Error(\"A folder name cannot be empty.\");\n const wanted = name.trim();\n\n // Minted here, outside the transaction: an attempt that conflicts persists nothing, so an id\n // minted inside one could be handed back to the caller after never having been stored.\n const id = asFolderId(randomUUID());\n\n await withFolders(pl, \"MLCreateFolder\", rids, (view) => {\n if (parent !== undefined && !view.folders.some((folder) => folder.id === parent))\n throw new Error(`Folder ${parent} does not exist.`);\n if (foldersNameTaken(wanted, foldersSiblingNames(view, parent, [], { kind: \"folder\" })))\n throw new Error(nameTakenMessage(\"folder\", wanted));\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: [\n ...base.folders,\n parent === undefined ? { id, name: wanted } : { id, name: wanted, parent },\n ],\n },\n };\n });\n\n return id;\n}\n\n/** Renames a folder. A human typed this name too, so a collision is rejected, not suffixed. */\nexport async function renameFolder(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n name: string,\n): Promise<void> {\n if (foldersNameBlank(name)) throw new Error(\"A folder name cannot be empty.\");\n const wanted = name.trim();\n\n await withFolders(pl, \"MLRenameFolder\", rids, (view) => {\n const target = view.folders.find((candidate) => candidate.id === folder);\n if (target === undefined) throw new Error(`Folder ${folder} does not exist.`);\n\n const siblings = foldersSiblingNames(view, target.parent, [folder], { kind: \"folder\" });\n if (foldersNameTaken(wanted, siblings)) throw new Error(nameTakenMessage(\"folder\", wanted));\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: base.folders.map((candidate) =>\n candidate.id === folder ? { ...candidate, name: wanted } : candidate,\n ),\n },\n };\n });\n}\n\n/**\n * Sets what a folder says about itself. Blank clears it.\n *\n * Unlike a name, a description is in no namespace: nothing is checked against the siblings, and\n * two folders may say the same thing about themselves.\n */\nexport async function setFolderDescription(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n description: string,\n): Promise<void> {\n const wanted = normalizeDescription(description);\n\n await withFolders(pl, \"MLSetFolderDescription\", rids, (view) => {\n const target = view.folders.find((candidate) => candidate.id === folder);\n if (target === undefined) throw new Error(`Folder ${folder} does not exist.`);\n\n const base = foldersDocumentFromView(view);\n return {\n result: undefined,\n document: {\n ...base,\n folders: base.folders.map((candidate) => {\n if (candidate.id !== folder) return candidate;\n const { description: _dropped, ...rest } = candidate;\n return wanted === undefined ? rest : { ...rest, description: wanted };\n }),\n },\n };\n });\n}\n\n/**\n * The shape of a folder subtree under ids local to the caller: a fresh id per folder, the root's\n * own parent left behind so the subtree stands alone, and a lookup from a real folder id to its\n * local one.\n *\n * Both ways of copying a folder need this. A share carries the shape into another user's tree and\n * a duplicate rebuilds it beside the source, and neither may carry the folder ids themselves,\n * because the document that receives the subtree mints its own.\n */\nexport function foldersLocalSubtree<Id extends string>(\n view: FoldersView,\n root: FolderId,\n mint: () => Id,\n): {\n readonly inSubtree: ReadonlySet<FolderId>;\n readonly localId: (id: FolderId) => Id;\n readonly folders: Record<Id, { name: string; parent?: Id; description?: string }>;\n} {\n const inSubtree = new Set(foldersSubtree(view, root));\n\n const localIds = new Map<FolderId, Id>();\n for (const id of inSubtree) localIds.set(id, mint());\n const localId = (id: FolderId): Id => {\n const local = localIds.get(id);\n if (local === undefined) throw new Error(`Folder ${id} is not under ${root}.`);\n return local;\n };\n\n const folders = {} as Record<Id, { name: string; parent?: Id; description?: string }>;\n for (const candidate of view.folders) {\n if (!inSubtree.has(candidate.id)) continue;\n // The copied folder is the subtree's root: its own parent stays behind. Every other folder of\n // the subtree has its parent in the subtree too.\n const parent = candidate.id === root ? undefined : localId(candidate.parent ?? root);\n folders[localId(candidate.id)] = {\n name: candidate.name,\n ...(parent === undefined ? {} : { parent }),\n ...(candidate.description === undefined ? {} : { description: candidate.description }),\n };\n }\n\n return { inSubtree, localId, folders };\n}\n\n/**\n * The plan a move would produce, computed from a fresh read — what the confirmation dialog shows.\n * The same planner runs again inside the write, over the same shape of read.\n */\nexport async function previewFoldersMove(\n pl: PlClient,\n rids: FoldersRids,\n items: readonly FoldersItem[],\n destination?: FolderId,\n): Promise<FoldersMovePlanResult> {\n const view = await readFoldersView(pl, \"MLPreviewFoldersMove\", rids);\n return planFoldersMove(view, items, destination);\n}\n\n/**\n * Moves any mix of folders, projects and templates into one destination, committing only when\n * the plan recomputed inside the transaction is identical to `confirmedPlan`.\n */\nexport async function moveFolderItems(\n pl: PlClient,\n rids: FoldersRids,\n items: readonly FoldersItem[],\n destination: FolderId | undefined,\n confirmedPlan: FoldersMovePlan,\n): Promise<FoldersMoveOutcome> {\n return await withFolders(pl, \"MLMoveFolderItems\", rids, (view) =>\n commitFoldersMove(view, planFoldersMove(view, items, destination), confirmedPlan),\n );\n}\n\n/** What deleting a folder would destroy: the subtree of folders, and every project and template\n * in it. */\nexport async function previewFolderDeletion(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n): Promise<FoldersRemovalPlanResult> {\n const view = await readFoldersView(pl, \"MLPreviewFolderDeletion\", rids);\n return planFoldersRemoval(view, folder);\n}\n\n/**\n * Deletes a folder, every folder inside it, and every project and template held anywhere in that\n * subtree.\n *\n * Deletion cannot be undone, so `confirmedRemoval` is required: the removal is recomputed here and\n * the write commits only if it names exactly the same things the caller confirmed. A project\n * dropped into the folder after the dialog opened therefore aborts the deletion rather than being\n * destroyed unseen.\n */\nexport async function deleteFolder(\n pl: PlClient,\n rids: FoldersRids,\n folder: FolderId,\n confirmedRemoval?: FoldersRemoval,\n): Promise<FoldersRemovalOutcome> {\n return await withFolders(pl, \"MLDeleteFolder\", rids, (view) =>\n commitFoldersRemoval(view, planFoldersRemoval(view, folder), confirmedRemoval),\n );\n}\n\n/**\n * Replaces a folder document this build cannot read with an empty one: no folders and no\n * assignments, so every project and template shows at the top level. Nothing but the document is\n * touched, and what the unreadable tree held is lost for good.\n *\n * It is refused while the document reads fine, so a working tree can never be wiped through it.\n * \"Reads fine\" is decided by the same decode the listing uses, in the transaction that writes.\n */\nexport async function resetFolders(pl: PlClient, rids: FoldersRids): Promise<void> {\n await pl.withWriteTx(\n \"MLResetFolders\",\n async (tx) => {\n const stored = await readStoredFolders(tx, rids);\n if (stored.decoded.writable)\n throw new Error(\n \"Folders can only be reset when they cannot be read, and these can be read. Nothing was changed.\",\n );\n\n writeFoldersDocument(\n tx,\n rids,\n { carriedAssignments: {}, documentFieldExists: stored.documentFieldExists },\n emptyFoldersDocument(),\n );\n await tx.commit();\n },\n { lockId: foldersLockId(rids.folders) },\n );\n}\n\n/** Why a typed name is refused: another item of the same kind beside it already carries it. */\nexport function nameTakenMessage(kind: FoldersItem[\"kind\"], name: string): string {\n return `A ${kind} named \"${name}\" is already here.`;\n}\n\n//\n// Internals\n//\n\n/** A key looked up in a map that was built from those very keys; absent means the code above\n * changed and the two no longer agree. */\nfunction mustGet<K, V>(map: Map<K, V>, key: K): V {\n const value = map.get(key);\n if (value === undefined) throw new Error(`No entry for ${String(key)}.`);\n return value;\n}\n\n/** Each entry of a list with its place in the tree, looked up by id; an entry the tree does not\n * place is at the top level. */\nfunction withPlacement<E extends { readonly id: string }>(\n entries: readonly E[],\n placed: readonly ({ readonly id: string } & FoldersPlacement)[],\n): (E & FoldersPlacement)[] {\n const byId = new Map(placed.map((item) => [item.id, item]));\n return entries.map((entry) => {\n const placement = byId.get(entry.id);\n return {\n ...entry,\n ...(placement?.folder === undefined ? {} : { folder: placement.folder }),\n ancestors: placement?.ancestors ?? [],\n path: placement?.path ?? [],\n };\n });\n}\n\ninterface FoldersRead {\n readonly view: FoldersView;\n readonly decoded: FoldersDecoded;\n readonly projectRids: ReadonlyMap<ProjectId, SignedResourceId>;\n readonly projectMetas: ReadonlyMap<ProjectId, ProjectMeta>;\n readonly templateRids: ReadonlyMap<TemplateId, SignedResourceId>;\n readonly documentFieldExists: boolean;\n /**\n * Assignments of projects that are in the list but whose metadata could not be read. They are\n * missing from the view, so a document rebuilt from the view would drop them — and a project\n * whose folder is forgotten because its metadata was briefly unreadable is a loss, not healing.\n */\n readonly carriedAssignments: Readonly<Record<string, FolderId>>;\n}\n\n/**\n * The folder document and both item lists, read inside a transaction.\n *\n * An absent document field is a user with no folders, and is writable. A field that points at\n * something carrying no readable blob is a document that exists and cannot be read here, and must\n * refuse writes, or the first folder edit replaces that tree with an empty one.\n *\n * A list entry counts as a project when it carries project metadata, and as a template when it\n * carries a template label. Nothing else is checked. The listing readers are stricter:\n * {@link projectListEntries} also requires the project resource type by name and both the created\n * and the last-modified timestamps, and {@link templateListEntries} also requires the template\n * resource type by name, the data blob and the created timestamp. So every entry the listing shows\n * is kept here, and so is an entry the listing skips because it has only partly synced or is a\n * foreign resource carrying one of those keys: it still holds its name and its folder assignment,\n * which is the safe side for a write. Telling a foreign resource apart by its type instead would\n * read the state of every resource in the list, and a read of a project's state conflicts with\n * every write that project's own session makes, so a folder edit would keep losing to any project\n * that is open.\n */\nasync function readFolders(tx: PlTransaction, rids: FoldersRids): Promise<FoldersRead> {\n const { decoded, documentFieldExists } = await readStoredFolders(tx, rids);\n\n const [listedProjects, listedTemplates] = await Promise.all([\n listedById(tx, rids.projects),\n listedById(tx, rids.templates),\n ]);\n const [metas, labels] = await Promise.all([\n Promise.all(\n [...listedProjects.values()].map(({ rid }) =>\n tx.getKValueJsonIfExists<ProjectMeta>(rid, ProjectMetaKey),\n ),\n ),\n Promise.all(\n [...listedTemplates.values()].map(({ rid }) =>\n tx.getKValueJsonIfExists<string>(rid, TemplateLabelKey),\n ),\n ),\n ]);\n\n const projectRids = new Map<ProjectId, SignedResourceId>();\n const projectMetas = new Map<ProjectId, ProjectMeta>();\n const projects: FoldersProjectInput[] = [];\n const carriedAssignments: Record<string, FolderId> = {};\n [...listedProjects.values()].forEach(({ rid }, index) => {\n const id = asProjectId(resourceIdToString(rid));\n projectRids.set(id, rid);\n const meta = metas[index];\n if (meta === undefined) {\n const folder = decoded.document.assignments[id];\n if (folder !== undefined) carriedAssignments[id] = folder;\n return;\n }\n projectMetas.set(id, meta);\n projects.push({ id, name: meta.label });\n });\n\n // Labels as well as ids: a template's name is held to the folder rule, among templates.\n const templateRids = new Map<TemplateId, SignedResourceId>();\n const templates: FoldersTemplateInput[] = [];\n [...listedTemplates.values()].forEach(({ rid }, index) => {\n const label = labels[index];\n if (label === undefined) return;\n const id = asTemplateId(resourceIdToString(rid));\n templateRids.set(id, rid);\n templates.push({ id, name: label });\n });\n\n return {\n view: healFolders(decoded, projects, templates),\n decoded,\n projectRids,\n projectMetas,\n templateRids,\n documentFieldExists,\n carriedAssignments,\n };\n}\n\n/** The folder document alone, decoded, and whether its field exists at all. */\nasync function readStoredFolders(\n tx: PlTransaction,\n rids: FoldersRids,\n): Promise<Pick<FoldersRead, \"decoded\" | \"documentFieldExists\">> {\n const documentField = await tx.getFieldIfExists(field(rids.folders, FoldersDocumentField));\n const decoded = decodeStoredFoldersDocument(\n documentField === undefined || isNullSignedResourceId(documentField.value)\n ? { present: false }\n : { present: true, raw: await readDocumentText(tx, documentField.value) },\n );\n return { decoded, documentFieldExists: documentField !== undefined };\n}\n\n/** The stored JSON of a document value, or undefined when the value carries no blob. */\nasync function readDocumentText(\n tx: PlTransaction,\n rid: SignedResourceId,\n): Promise<string | undefined> {\n const data = await tx.getResourceData(rid, false);\n return data.data === undefined ? undefined : Buffer.from(data.data).toString(\"utf-8\");\n}\n\n/**\n * Persists a folder document: the assignments the read had to carry are added back, a new value\n * resource is minted, and the one document field is created or re-pointed at it.\n */\nfunction writeFoldersDocument(\n tx: PlTransaction,\n rids: FoldersRids,\n read: Pick<FoldersRead, \"carriedAssignments\" | \"documentFieldExists\">,\n document: FoldersDocument,\n): void {\n const ref = tx.createJsonValue(withCarriedAssignments(document, read.carriedAssignments));\n const documentField = field(rids.folders, FoldersDocumentField);\n if (read.documentFieldExists) tx.setField(documentField, ref);\n else tx.createField(documentField, \"Dynamic\", ref);\n}\n\n/**\n * A document written by a newer build, or one nothing here can parse, reads as no folders so that\n * the project list still renders — and every write is refused, because degrading the read while\n * still writing would silently replace that tree with this build's truncated view of it.\n */\nfunction refusalMessage(problem: FoldersDocumentProblem): string {\n return (\n `Folders cannot be changed: ${problem.message}. ` +\n `Update the application to a version that understands this folder document.`\n );\n}\n\nfunction withCarriedAssignments(\n document: FoldersDocument,\n carried: Readonly<Record<string, FolderId>>,\n): FoldersDocument {\n const entries = Object.entries(carried).filter(([, folder]) =>\n document.folders.some((candidate) => candidate.id === folder),\n );\n if (entries.length === 0) return document;\n return { ...document, assignments: { ...document.assignments, ...Object.fromEntries(entries) } };\n}\n\n/**\n * Refuses a write that would put a *new* rule violation into the document.\n *\n * Only new ones: accounts predating folders may already hold two projects with the same name in\n * one place, because nothing enforced uniqueness before, and a write that merely carries such a\n * collision along must not be blocked by it. What the folder machinery must never do is create\n * one.\n *\n * What the write deletes is left out of the document it leaves behind. A deleted project or\n * template has no folder any more, and counted anyway it would stand at the top level under its\n * old name — where it clashes with whatever is really there, and refuses the deletion.\n */\nfunction rejectNewViolations(\n read: FoldersRead,\n document: FoldersDocument,\n renames: readonly FoldersLabelRename[],\n deleted: readonly string[],\n): void {\n const renamed = new Map<string, string>(renames.map((rename) => [rename.item.id, rename.name]));\n const gone = new Set<string>(deleted);\n const templates = read.view.templates.map(({ id, name }) => ({ id, name }));\n const before = validateFoldersDocument(\n foldersDocumentFromView(read.view),\n [...read.projectMetas].map(([id, meta]) => ({ id, name: meta.label })),\n templates,\n );\n const after = validateFoldersDocument(\n document,\n [...read.projectMetas]\n .filter(([id]) => !gone.has(id))\n .map(([id, meta]) => ({ id, name: renamed.get(id) ?? meta.label })),\n templates\n .filter(({ id }) => !gone.has(id))\n .map(({ id, name }) => ({ id, name: renamed.get(id) ?? name })),\n );\n\n const known = new Set(before.map(violationKey));\n const introduced = after.filter((violation) => !known.has(violationKey(violation)));\n if (introduced.length === 0) return;\n\n throw new Error(`Folder change refused: ${introduced.map(formatFoldersViolation).join(\"; \")}.`);\n}\n\n/**\n * Deletes the named items from one of the root's lists. Every id must be there: an id the list\n * does not hold means the write is deleting something other than what was planned, and the\n * transaction is abandoned rather than allowed to destroy a guess.\n */\nasync function removeListedFields(\n tx: PlTransaction,\n listRid: SignedResourceId,\n ids: readonly string[],\n kind: ListedKind,\n): Promise<void> {\n if (ids.length === 0) return;\n\n const listed = await listedById(tx, listRid);\n for (const id of new Set(ids)) {\n const entry = listed.get(id);\n if (entry === undefined) throw notListedError(kind, id);\n tx.removeField(field(listRid, entry.fieldName));\n }\n}\n\nfunction violationKey(violation: FoldersViolation): string {\n return JSON.stringify(violation);\n}\n\n/**\n * Scoped to the singleton rather than to the process, so that two middle layers over two users'\n * roots — an impersonating admin session and the admin's own — do not queue behind each other.\n */\nfunction foldersLockId(foldersRid: SignedResourceId): string {\n return `folders:${foldersRid}`;\n}\n"],"mappings":"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"}
|
|
@@ -224,9 +224,9 @@ var MiddleLayer = class MiddleLayer {
|
|
|
224
224
|
* different places: a rename that carried a stale description alongside the new name would
|
|
225
225
|
* undo a description edit that happened in between, and the reverse.
|
|
226
226
|
*
|
|
227
|
-
* The label is stored trimmed, and
|
|
228
|
-
*
|
|
229
|
-
* human typed it, and a name that silently becomes a different name is worse than one that is
|
|
227
|
+
* The label is stored trimmed, and the folder rule governs it, so a label another project
|
|
228
|
+
* inside the same folder already carries is refused rather than written — a folder or a
|
|
229
|
+
* template of that name beside it is no obstacle. A human typed it, and a name that silently becomes a different name is worse than one that is
|
|
230
230
|
* turned down. The check and the write share one transaction, so two renames racing for the
|
|
231
231
|
* same name cannot both win. Duplicates an account already holds are tolerated and never
|
|
232
232
|
* rewritten; see the name check of {@link openFoldersTx}.
|
|
@@ -264,7 +264,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
264
264
|
await this.foldersTree.refreshState();
|
|
265
265
|
return id;
|
|
266
266
|
}
|
|
267
|
-
/** Renames a folder. A name
|
|
267
|
+
/** Renames a folder. A name another folder beside it already carries is rejected. */
|
|
268
268
|
async renameFolder(folder, name) {
|
|
269
269
|
await require_folders.renameFolder(this.pl, this.foldersRids, folder, name);
|
|
270
270
|
await this.foldersTree.refreshState();
|
|
@@ -331,7 +331,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
331
331
|
* in them, and a copy of every template.
|
|
332
332
|
*
|
|
333
333
|
* The copy lands beside the source, so only its root needs a name of its own — `X (Copy)`.
|
|
334
|
-
* Nothing inside is renamed: names are compared among siblings, and a copied folder's children
|
|
334
|
+
* Nothing inside is renamed: names are compared among siblings of one kind, and a copied folder's children
|
|
335
335
|
* are only ever compared with each other, where they came in distinct already.
|
|
336
336
|
*
|
|
337
337
|
* The subtree is read first and rebuilt in one write transaction, so the folders and everything
|
|
@@ -609,10 +609,10 @@ var MiddleLayer = class MiddleLayer {
|
|
|
609
609
|
* A block that cannot be expressed as a template entry stores nothing at all, and every
|
|
610
610
|
* such block is reported — fixing an unexportable project takes one pass, not one per block.
|
|
611
611
|
*
|
|
612
|
-
* The template lands beside its project, and is named by the rule
|
|
613
|
-
* by: a label the caller chose is stored trimmed and refused if
|
|
614
|
-
* without one the project's own label is taken — suffixed
|
|
615
|
-
* answers to it.
|
|
612
|
+
* The template lands beside its project, and is named by the rule templates there are named
|
|
613
|
+
* by: a label the caller chose is stored trimmed and refused if another template there already
|
|
614
|
+
* carries it, and without one the project's own label is taken — suffixed only when a template
|
|
615
|
+
* there already answers to it. The project is not a template, so its own name is no obstacle.
|
|
616
616
|
*
|
|
617
617
|
* The template's description is the one the caller gives, stored trimmed; blank means none.
|
|
618
618
|
* Without one the template is stored undescribed, whatever the project's own description says.
|
|
@@ -632,9 +632,9 @@ var MiddleLayer = class MiddleLayer {
|
|
|
632
632
|
const signedRid = await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
|
|
633
633
|
const meta = await tx.getKValueJson(rid, require_project_model.ProjectMetaKey);
|
|
634
634
|
const tree = await require_folders.openFoldersTx(tx, this.foldersRids);
|
|
635
|
-
const taken = tree.namesTakenBeside(projectId);
|
|
636
|
-
if (wanted !== void 0 && (0, _milaboratories_pl_model_middle_layer.foldersNameTaken)(wanted, taken)) throw new Error(
|
|
637
|
-
const name = wanted ?? (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(meta.label,
|
|
635
|
+
const taken = tree.namesTakenBeside(projectId, "template");
|
|
636
|
+
if (wanted !== void 0 && (0, _milaboratories_pl_model_middle_layer.foldersNameTaken)(wanted, taken)) throw new Error(require_folders.nameTakenMessage("template", wanted));
|
|
637
|
+
const name = wanted ?? (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(meta.label, taken);
|
|
638
638
|
const created = await require_template.createTemplate(tx, this.templateListResourceId, {
|
|
639
639
|
label: name,
|
|
640
640
|
description
|
|
@@ -662,8 +662,8 @@ var MiddleLayer = class MiddleLayer {
|
|
|
662
662
|
* Changes a template's label. The stored document is immutable and stays untouched —
|
|
663
663
|
* improving a template means saving a new one.
|
|
664
664
|
*
|
|
665
|
-
* The label is stored trimmed.
|
|
666
|
-
*
|
|
665
|
+
* The label is stored trimmed. One another template beside it already carries is refused, in
|
|
666
|
+
* the transaction that writes it; a folder or a project of that name beside it is no obstacle.
|
|
667
667
|
*/
|
|
668
668
|
async renameTemplate(id, label) {
|
|
669
669
|
const rid = await this.resolveTemplateId(id);
|
|
@@ -793,7 +793,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
793
793
|
const newPrj = await this.pl.withWriteTx("MLDuplicateProject", async (tx) => {
|
|
794
794
|
const sourceMeta = await tx.getKValueJson(sourceRid, require_project_model.ProjectMetaKey);
|
|
795
795
|
const tree = await require_folders.openFoldersTx(tx, this.foldersRids);
|
|
796
|
-
const label = rename === void 0 ? (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(sourceMeta.label, [sourceMeta.label, ...tree.namesTakenBeside(srcProjectId)]) : rename(sourceMeta.label, await existingProjectLabels(tx, this.projectListResourceId));
|
|
796
|
+
const label = rename === void 0 ? (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(sourceMeta.label, [sourceMeta.label, ...tree.namesTakenBeside(srcProjectId, "project")]) : rename(sourceMeta.label, await existingProjectLabels(tx, this.projectListResourceId));
|
|
797
797
|
const newPrj = await require_project.duplicateProject(tx, sourceRid, {
|
|
798
798
|
...sourceMeta,
|
|
799
799
|
label
|
|
@@ -1058,7 +1058,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1058
1058
|
const rid = await this.pl.withWriteTx("MLCopyTemplateShare", async (tx) => {
|
|
1059
1059
|
const tree = await require_folders.openFoldersTx(tx, this.foldersRids);
|
|
1060
1060
|
const created = await require_template.createTemplate(tx, this.templateListResourceId, {
|
|
1061
|
-
label: (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(payload.label, tree.namesTakenIn(destination)),
|
|
1061
|
+
label: (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(payload.label, tree.namesTakenIn(destination, "template")),
|
|
1062
1062
|
description: payload.description
|
|
1063
1063
|
}, {
|
|
1064
1064
|
schemaVersion: 1,
|
|
@@ -1130,7 +1130,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1130
1130
|
}
|
|
1131
1131
|
const createdRids = await this.pl.withWriteTx("MLCopyShare", async (tx) => {
|
|
1132
1132
|
const tree = await require_folders.openFoldersTx(tx, this.foldersRids);
|
|
1133
|
-
const taken = [...tree.namesTakenIn(destination)];
|
|
1133
|
+
const taken = [...tree.namesTakenIn(destination, "project")];
|
|
1134
1134
|
const created = await require_sharing.copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId, (sourceLabel) => {
|
|
1135
1135
|
const name = (0, _milaboratories_pl_model_middle_layer.foldersUniqueName)(sourceLabel, taken);
|
|
1136
1136
|
taken.push(name);
|