@milaboratories/pl-middle-layer 1.71.16 → 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/index.cjs +10 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +636 -0
- package/dist/middle_layer/folders.cjs.map +1 -0
- package/dist/middle_layer/folders.d.ts +39 -0
- package/dist/middle_layer/folders.d.ts.map +1 -0
- package/dist/middle_layer/folders.js +617 -0
- package/dist/middle_layer/folders.js.map +1 -0
- package/dist/middle_layer/index.cjs +3 -0
- package/dist/middle_layer/index.d.ts +4 -3
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +585 -370
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +215 -122
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +593 -378
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.cjs +36 -19
- package/dist/middle_layer/project_list.cjs.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/project_list.d.ts.map +1 -1
- package/dist/middle_layer/project_list.js +37 -21
- package/dist/middle_layer/project_list.js.map +1 -1
- package/dist/middle_layer/sharing_list.cjs +123 -93
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +43 -19
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +123 -93
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +45 -20
- package/dist/middle_layer/template_list.cjs.map +1 -1
- package/dist/middle_layer/template_list.d.ts +7 -15
- package/dist/middle_layer/template_list.d.ts.map +1 -1
- package/dist/middle_layer/template_list.js +44 -21
- package/dist/middle_layer/template_list.js.map +1 -1
- package/dist/model/index.cjs +7 -5
- package/dist/model/index.d.ts +2 -2
- package/dist/model/index.js +2 -2
- package/dist/model/sharing_model.cjs +46 -20
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +125 -55
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +40 -16
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/mutator/list.cjs +30 -0
- package/dist/mutator/list.cjs.map +1 -0
- package/dist/mutator/list.js +29 -0
- package/dist/mutator/list.js.map +1 -0
- package/dist/mutator/project.cjs +14 -1
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +15 -2
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/sharing.cjs +130 -76
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +130 -76
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +24 -17
- package/dist/mutator/template.cjs.map +1 -1
- package/dist/mutator/template.js +26 -20
- package/dist/mutator/template.js.map +1 -1
- package/package.json +17 -17
- package/src/middle_layer/folders.test.ts +1076 -0
- package/src/middle_layer/folders.ts +1069 -0
- package/src/middle_layer/folders_read.test.ts +359 -0
- package/src/middle_layer/index.ts +3 -2
- package/src/middle_layer/middle_layer.ts +813 -547
- package/src/middle_layer/project_list.test.ts +183 -0
- package/src/middle_layer/project_list.ts +58 -21
- package/src/middle_layer/sharing.test.ts +183 -0
- package/src/middle_layer/sharing_list.ts +208 -152
- package/src/middle_layer/template_list.test.ts +148 -0
- package/src/middle_layer/template_list.ts +72 -32
- package/src/middle_layer/templates.test.ts +76 -45
- package/src/model/sharing_model.test.ts +69 -2
- package/src/model/sharing_model.ts +161 -73
- package/src/mutator/list.ts +36 -0
- package/src/mutator/project-v3.test.ts +3 -1
- package/src/mutator/project.ts +13 -2
- package/src/mutator/sharing.ts +193 -120
- package/src/mutator/template.ts +37 -29
- package/src/test/with_ml.ts +73 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middle_layer.js","names":[],"sources":["../../src/middle_layer/middle_layer.ts"],"sourcesContent":["import type {\n PlClient,\n PlTransaction,\n SignedResourceId,\n ResourceRef,\n Role,\n} from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n field,\n GrantType,\n isNotNullSignedResourceId,\n isNullSignedResourceId,\n resourceIdToString,\n} from \"@milaboratories/pl-client\";\nimport { LRUCache } from \"lru-cache\";\nimport {\n createProjectList,\n ensureProjectListRid,\n ProjectsField,\n ProjectsResourceType,\n} from \"./project_list\";\nimport type {\n CreateProjectFromTemplateOutcome,\n SaveProjectAsTemplateOutcome,\n ShareTemplateOutcome,\n StoredTemplateData,\n TemplateId,\n TemplateListEntry,\n} from \"./template_list\";\nimport {\n createTemplateList,\n decodeStoredTemplateData,\n TemplateLabelKey,\n TemplatesField,\n TemplatesResourceType,\n} from \"./template_list\";\nimport { createTemplate, deleteTemplate, renameTemplate } from \"../mutator/template\";\nimport {\n createProject,\n duplicateProject,\n withProject,\n withProjectAuthored,\n} from \"../mutator/project\";\nimport type { ProjectTemplateExportOutcome } from \"../model/template_serializer\";\nimport type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { extractConfig, ensureError } from \"@platforma-sdk/model\";\nimport type { TemplateApplyProblem, TemplateApplyReport } from \"../model/template_apply\";\nimport { TemplateEntryRejected, kindMismatch } from \"../model/template_apply\";\nimport type { BlockPackProvider, TemplateResolveOutcome } from \"../model/template_resolve\";\nimport { resolveTemplateEntries } from \"../model/template_resolve\";\nimport type { PreparedTemplateEntry } from \"../mutator/template_construct\";\nimport { applyTemplateEntries } from \"../mutator/template_construct\";\nimport { throwIfMissingServerCapabilities } from \"./project\";\nimport { cacheBlockPackTemplate } from \"../mutator/template/template_cache\";\nimport { ProjectMetaKey } from \"../model/project_model\";\nimport type { ProjectId } from \"../model/project_model\";\nimport type { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport {\n acceptanceFieldLogin,\n canGrantToEveryone,\n canImpersonate,\n decodeEnvelopeData,\n envelopeProjectMap,\n isAcceptanceField,\n SharingOutboxField,\n SharingOutboxResourceType,\n SharingStateField,\n SharingStateResourceType,\n type EnvelopeAcceptance,\n type EnvelopeData,\n type ProjectChangeAction,\n type ProjectFieldUuid,\n type ShareId,\n type ShareProjectsOptions,\n type ShareTemplateOptions,\n} from \"../model/sharing_model\";\nimport {\n buildShareEnvelope,\n buildTemplateShareEnvelope,\n copyEnvelopeProjectsIntoList,\n envelopeProjectFieldUuid,\n isEnvelopeProjectField,\n resourceIdsToStrings,\n writeEnvelopeAcceptance,\n writeSharingDecision,\n type EnvelopeProjectSource,\n} from \"../mutator/sharing\";\nimport type { LiveEnvelope, OutgoingShare, PendingShare } from \"./sharing_list\";\nimport {\n createLiveEnvelopesComputable,\n createOutgoingShares,\n createPendingSharesComputable,\n createPendingSharesTree,\n createSharingStateTree,\n} from \"./sharing_list\";\nimport { BlockPackPreparer } from \"../mutator/block-pack/block_pack\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { BlockEventDispatcher, cachedDeserialize } from \"@milaboratories/ts-helpers\";\nimport { HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport type { Computable, ComputableStableDefined } from \"@milaboratories/computable\";\nimport { WatchableValue } from \"@milaboratories/computable\";\nimport { Project } from \"./project\";\nimport type { MiddleLayerOps, MiddleLayerOpsConstructor } from \"./ops\";\nimport { DefaultMiddleLayerOpsPaths, DefaultMiddleLayerOpsSettings } from \"./ops\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectListEntry } from \"../model\";\nimport type {\n AuthorMarker,\n ProjectMeta,\n BlockPlatform,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport type { AppliedEntry } from \"../model/template_apply\";\nimport { BlockUpdateWatcher } from \"../block_registry/watcher\";\nimport type { QuickJSWASMModule } from \"quickjs-emscripten\";\nimport { getQuickJS } from \"quickjs-emscripten\";\nimport type { MiddleLayerDriverKit } from \"./driver_kit\";\nimport { initDriverKit } from \"./driver_kit\";\nimport type { BlockCodeFeatureFlags, DriverKit, SupportedRequirement } from \"@platforma-sdk/model\";\nimport { RuntimeCapabilities } from \"@platforma-sdk/model\";\nimport {\n type ModelServiceRegistry,\n registerServiceCapabilities,\n REQUIRES_PFRAMES_VERSION,\n} from \"@milaboratories/pl-model-common\";\nimport { createModelServiceRegistry } from \"../service_factories\";\nimport type { DownloadUrlDriver } from \"@milaboratories/pl-drivers\";\nimport { V2RegistryProvider } from \"../block_registry\";\nimport type { Dispatcher } from \"undici\";\nimport { RetryAgent } from \"undici\";\nimport { getDebugFlags } from \"../debug\";\nimport { ProjectHelper } from \"../model/project_helper\";\nimport type { TreeSnapshotStat } from \"./tree_snapshot_store\";\nimport { TreeSnapshotStore } from \"./tree_snapshot_store\";\n\n/** How long shutdown waits for close-boundary snapshot writes that are already running. Long\n * enough for a ten-megabyte encode and write on ordinary storage, short enough that a wedged\n * filesystem does not hold the quit open. */\nconst SNAPSHOT_DRAIN_TIMEOUT_MS = 5_000;\n\nexport interface MiddleLayerEnvironment {\n dispose(): Promise<void>;\n readonly pl: PlClient;\n readonly runtimeCapabilities: RuntimeCapabilities;\n readonly logger: MiLogger;\n readonly blockEventDispatcher: BlockEventDispatcher;\n readonly httpDispatcher: Dispatcher;\n readonly retryHttpDispatcher: Dispatcher;\n readonly signer: Signer;\n readonly ops: MiddleLayerOps;\n readonly bpPreparer: BlockPackPreparer;\n readonly frontendDownloadDriver: DownloadUrlDriver;\n readonly blockUpdateWatcher: BlockUpdateWatcher;\n readonly quickJs: QuickJSWASMModule;\n readonly driverKit: MiddleLayerDriverKit;\n readonly serviceRegistry: ModelServiceRegistry;\n readonly projectHelper: ProjectHelper;\n /** Persisted project tree mirrors. Undefined when snapshots are switched off, or when the\n * client is impersonating another user, in which case nothing is read or written. */\n readonly treeSnapshots?: TreeSnapshotStore;\n}\n\n/**\n * Main access object to work with pl from UI.\n *\n * It implements an abstraction layer of projects and blocks.\n *\n * As a main entry point inside the pl, this object uses a resource attached\n * via the {@link ProjectsField} to the pl client's root, this resource\n * contains project list.\n *\n * Read about alternative roots, if isolated project lists (working environments)\n * are required.\n * */\nexport class MiddleLayer {\n public readonly pl: PlClient;\n\n private constructor(\n private readonly env: MiddleLayerEnvironment,\n public readonly driverKit: DriverKit,\n public readonly signer: Signer,\n private readonly projectListResourceId: SignedResourceId,\n private readonly templateListResourceId: SignedResourceId,\n private readonly sharingOutboxResourceId: SignedResourceId,\n private readonly sharingStateResourceId: SignedResourceId,\n private readonly openedProjectsList: WatchableValue<ProjectId[]>,\n private readonly projectListTree: SynchronizedTreeState,\n private readonly templateListTree: SynchronizedTreeState,\n private readonly sharingOutboxTree: SynchronizedTreeState,\n private readonly sharingStateTree: SynchronizedTreeState,\n private readonly pendingSharesTree: SynchronizedTreeState,\n public readonly blockRegistryProvider: V2RegistryProvider,\n /** Contains a reactive list of projects along with their meta information. */\n public readonly projectList: ComputableStableDefined<ProjectListEntry[]>,\n /** Contains a reactive list of stored templates along with their labels and provenance. */\n public readonly templateList: ComputableStableDefined<TemplateListEntry[]>,\n /** Reactive view of the donor's outbox — the shares this user has created.\n * v1: API only, no UI. */\n public outgoingShares: Computable<OutgoingShare[] | undefined>,\n /** Envelopes granted to this user, not yet accepted or rejected. Fed by the\n * shared-resource discovery tree. */\n public pendingShares: Computable<PendingShare[] | undefined>,\n /** Internal: the acceptor's currently-live envelopes, read from the same shared-resource\n * discovery tree as {@link pendingShares}. The single source the accept/reject flow resolves\n * live envelopes from — no second discovery path. */\n private readonly liveEnvelopes: Computable<LiveEnvelope[] | undefined>,\n ) {\n this.pl = this.env.pl;\n this.startEnvelopeCleanup();\n }\n\n /**\n * Get the OS where backend is running.\n * For old backend versions returns undefined.\n */\n public get serverPlatform(): BlockPlatform | undefined {\n return this.pl.serverInfo.platform as BlockPlatform | undefined;\n }\n\n /**\n * Runtime capabilities advertised by the connected backend (tokens of\n * the form `<feature>:<version>`, e.g. \"wasm:v1\"). Empty list if the\n * backend predates the capability mechanism — that's the desired\n * fail-closed behaviour for blocks declaring any `requiredCapabilities`.\n */\n public get serverCapabilities(): string[] {\n return this.pl.serverInfo.capabilities ?? [];\n }\n\n /**\n * Login of the authenticated user, for the \"Signed in as\" UI. `null` when the\n * backend has no auth (local/dev mode) — the UI hides the element.\n */\n public get currentUserLogin(): string | null {\n return this.pl.userResources.authUser;\n }\n\n /**\n * Whether the connected backend supports project sharing. Synthetic — computed\n * in the middle layer from the backend capabilities the share flow needs (the\n * cross-color field-reference relaxation the accept flow rests on). It can absorb\n * additional required capabilities later without a UI change.\n */\n public get sharingSupported(): boolean {\n // Sharing does not compose with impersonation (discovery is scoped to the admin's session,\n // decisions attribute to the admin), so it is disabled while impersonating. Admins move\n // projects across roots with duplicateProjectToUser instead.\n return this.serverCapabilities.includes(\"crossTreeRefs:v1\") && !this.impersonating;\n }\n\n /** True when this session is impersonating another user (an admin opened another user's root). */\n public get impersonating(): boolean {\n return this.pl.conf.asUser !== undefined;\n }\n\n /**\n * Role of the authenticated user, from the `GetSessionInfo` RPC surfaced through the\n * pl-client. `null` in no-auth mode (when {@link currentUserLogin} is null).\n */\n public get currentUserRole(): Role | null {\n return this.pl.currentUserRole;\n }\n\n /**\n * Whether the UI offers share-with-everybody — no role policy lives in the UI:\n * serverCapabilities.has(\"publicGrants:v1\") && canGrantToEveryone(currentUserRole)\n * Not a security boundary: a crafted call still hits the backend's role +\n * permission-ceiling gate.\n */\n public get canShareWithEveryone(): boolean {\n return (\n !this.impersonating &&\n this.serverCapabilities.includes(\"publicGrants:v1\") &&\n canGrantToEveryone(this.currentUserRole)\n );\n }\n\n /**\n * Whether the authenticated user may impersonate others (open another user's root). Mirrors\n * the backend's `CanImpersonate` role gate — admin/controller only, never a regular user.\n * Derived from the session role, which stays the authenticated admin's even while\n * impersonating, so this stays true across a switch: the \"return to my root\" affordance must\n * not vanish mid-impersonation. Not a security boundary; the backend re-checks on every call.\n */\n public get currentUserCanImpersonate(): boolean {\n return canImpersonate(this.currentUserRole);\n }\n\n /** Adds a runtime capability to the middle layer. */\n public addRuntimeCapability(\n requirement: SupportedRequirement,\n value: number | boolean = true,\n ): void {\n this.env.runtimeCapabilities.addSupportedRequirement(requirement, value);\n }\n\n /** Checks if the given block feature flags are compatible with the runtime capabilities. */\n public checkBlockCompatibility(featureFlags: BlockCodeFeatureFlags | undefined): boolean {\n return this.env.runtimeCapabilities.checkCompatibility(featureFlags);\n }\n\n /** Returns extended API driver kit used internally by middle layer. */\n public get internalDriverKit(): MiddleLayerDriverKit {\n return this.env.driverKit;\n }\n\n /** Returns the service registry for service introspection. */\n public get serviceRegistry(): ModelServiceRegistry {\n return this.env.serviceRegistry;\n }\n\n //\n // ProjectId ↔ SignedResourceId resolution\n //\n\n private readonly projectIdCache = new LRUCache<ProjectId, SignedResourceId>({ max: 1024 });\n\n /** Resolves a ProjectId to a signed SignedResourceId.\n * Uses LRU cache with TX-scan fallback. */\n private async resolveProjectId(projectId: ProjectId): Promise<SignedResourceId> {\n const cached = this.projectIdCache.get(projectId);\n if (cached !== undefined) return cached;\n\n // Cache miss — scan project list fields to find the matching resource\n const rid = await this.pl.withReadTx(\"ResolveProjectId\", async (tx) => {\n const data = await tx.getResourceData(this.projectListResourceId, true);\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (resourceIdToString(f.value) === (projectId as string)) return f.value;\n }\n throw new Error(`Project ${projectId} not found in project list.`);\n });\n\n this.projectIdCache.set(projectId, rid);\n return rid;\n }\n\n //\n // Project List Manipulation\n //\n\n /** Creates a project with initial state and adds it to project list. */\n public async createProject(meta: ProjectMeta): Promise<ProjectId> {\n let prj: ResourceRef;\n await this.pl.withWriteTx(\"MLCreateProject\", async (tx) => {\n prj = await createProject(tx, meta);\n tx.createField(field(this.projectListResourceId, randomUUID()), \"Dynamic\", prj);\n await tx.commit();\n });\n await this.projectListTree.refreshState();\n\n const signedRid = await prj!.globalId;\n const projectId = resourceIdToString(signedRid) as ProjectId;\n this.projectIdCache.set(projectId, signedRid);\n return projectId;\n }\n\n /** Updates project metadata */\n public async setProjectMeta(\n id: ProjectId,\n meta: ProjectMeta,\n author?: AuthorMarker,\n ): Promise<void> {\n const rid = await this.resolveProjectId(id);\n await withProjectAuthored(\n this.env.projectHelper,\n this.pl,\n rid,\n author,\n (prj) => {\n prj.setMeta(meta);\n },\n { name: \"setProjectMeta\" },\n );\n await this.projectListTree.refreshState();\n }\n\n /**\n * Renders a project as a `template-v1` YAML document, or reports every reason it\n * cannot be — the backing call for an \"Export Project as Template…\" command.\n *\n * Takes a project id rather than an open {@link Project} because exporting is a\n * property of the stored project, not of a session with it: the command belongs on\n * a project card, where the project is usually closed. Opening one to read it would\n * spin up trees and watchers for a one-shot read, and then have to decide whether to\n * close them again.\n *\n * Read-only — the underlying mutator touches no field, so the transaction is never\n * committed and the project list needs no refresh.\n *\n * @param id - project id of the project to export\n */\n public async exportProjectAsTemplate(id: ProjectId): Promise<ProjectTemplateExportOutcome> {\n const rid = await this.resolveProjectId(id);\n return await withProject(\n this.env.projectHelper,\n this.pl,\n rid,\n (prj) => prj.exportAsTemplateV1(),\n { name: \"exportProjectAsTemplate\" },\n );\n }\n\n /**\n * Creates the blocks a `template-v1` document describes in an existing project, in the\n * order the document lists them — the backing call for a \"Create Project from Template\n * file…\" command, which is `createProject` followed by this.\n *\n * Takes a project id rather than an open {@link Project}, like\n * {@link exportProjectAsTemplate} and for the same reason: applying a template is a\n * property of the stored project, not of a session with it, and the flow that needs it\n * has just created the project and has no session yet. An already-open session picks the\n * new blocks up through its own refresh loop.\n *\n * Three stages, and their order is the design:\n *\n * 1. **Resolve every entry** to a concrete block pack, through `provider`.\n * 2. **Prepare every block**: fetch it, check it can run against this backend, cache its\n * workflow template, and offer the entry's params to the block's kind for a shape\n * check.\n * 3. **Create the blocks**, in one transaction — each one's params first pointed at this\n * project by the block's own model, since which values in there are references is\n * knowledge only the block has.\n *\n * The first two create nothing, so either of them failing leaves the project exactly as it\n * was. They are also what leaves stage 3 with only in-memory work, and hence able to be a\n * single transaction.\n *\n * **Stage 3 is all or nothing too**, because it is that one transaction: an entry it cannot\n * create throws, the transaction is never committed, and the project keeps none of the\n * blocks the apply had placed. So `problems` non-empty always means `added` is empty, at\n * every stage — a caller never has to reconcile a half-built project, and never has to ask\n * which of the blocks present came from the file.\n *\n * What is NOT checked before the work starts: which entries an entry references. Reading\n * that means reading the params, which only the block can do, and no block exists until\n * stage 2 has fetched one. A file whose entry references one listed below it therefore\n * applies, and the block it creates reports itself as missing references — the same way a\n * reference to a deleted block already behaves.\n *\n * @param id Project to apply into\n * @param document A parsed template document\n * @param provider Where each entry's block comes from\n * @param options `allowUnstable` widens resolution to pre-release implementations, for\n * the whole document\n */\n public async applyTemplateToProject(\n id: ProjectId,\n document: ProjectTemplateV1,\n provider: BlockPackProvider,\n options: { allowUnstable?: boolean; author?: AuthorMarker } = {},\n ): Promise<TemplateApplyReport> {\n const preparation = await this.prepareTemplateEntries(document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n if (preparation.problems.length > 0) return { added: [], problems: preparation.problems };\n return await this.applyPreparedEntries(id, document, preparation.prepared, options.author);\n }\n\n /**\n * Stages 1 and 2 of an apply — resolve every entry to a block pack, then prepare every\n * block — for a document that may not have a project yet.\n *\n * Neither stage creates anything, which is what lets a caller run them before it decides to\n * create a project at all: {@link createProjectFromTemplate} does exactly that, so an\n * unapplicable template leaves no empty project behind.\n */\n private async prepareTemplateEntries(\n document: ProjectTemplateV1,\n provider: BlockPackProvider,\n options: { allowUnstable: boolean },\n ): Promise<{\n prepared: Map<string, PreparedTemplateEntry>;\n problems: TemplateApplyProblem[];\n }> {\n const prepared = new Map<string, PreparedTemplateEntry>();\n\n const resolution = await resolveTemplateEntries(document, provider, options);\n if (resolution.problems.length > 0) return { prepared, problems: [...resolution.problems] };\n\n // One map, not one per field: resolution reports by entry id, so everything this loop\n // needs about an entry is looked up the same way.\n const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));\n const problems: TemplateApplyProblem[] = [];\n\n for (const entry of resolution.resolved) {\n try {\n const documentEntry = byEntryId.get(entry.entryId)!;\n const preparedBp = await this.env.bpPreparer.prepare(entry.spec);\n const blockCfg = extractConfig(preparedBp.config);\n\n // The first question asked of the prepared block: is it the block this entry meant.\n // Everything below is only meaningful once the answer is yes.\n //\n // The locator is appended here rather than inside the check, which names no route:\n // when the file chose the implementation itself, what it chose is the thing to correct.\n const mismatch = kindMismatch(documentEntry.kind, preparedBp.config.kind);\n if (mismatch !== undefined) {\n const locator = documentEntry.location ?? documentEntry.block;\n problems.push({\n entryId: entry.entryId,\n error: locator === undefined ? `${mismatch}.` : `${mismatch} (${locator}).`,\n });\n continue;\n }\n\n // The same two gates `Project.addBlock` applies, for the same reason: a block that\n // cannot run here must not be installed. Here they become per-entry problems\n // rather than throws, so one unusable block reads as one bad entry.\n this.env.runtimeCapabilities.throwIfIncompatible(blockCfg.featureFlags);\n throwIfMissingServerCapabilities(this.pl, preparedBp.requiredCapabilities);\n\n const cachedBp = await cacheBlockPackTemplate(this.pl, preparedBp);\n\n // Offered to the block's kind while nothing has been created yet. Every entry is\n // checked, including one whose file omitted `params` — the parser read that as `{}`,\n // which a kind with required fields rejects, and rightly: it would otherwise apply as\n // a block that looks configured and is not.\n const checked = this.env.projectHelper.validateTemplateParamsInVM(\n blockCfg,\n documentEntry.params,\n );\n if (checked.error !== undefined) {\n problems.push({ entryId: entry.entryId, error: checked.error.message });\n continue;\n }\n\n // The block package's own title, the same thing the add-block UI writes. It is\n // what the user sees for a block whose model derives no title of its own, and it\n // is resolution's to supply — nothing here could reconstruct it.\n prepared.set(entry.entryId, { blockPack: cachedBp, label: entry.title });\n } catch (e) {\n problems.push({\n entryId: entry.entryId,\n error: `This entry's block could not be installed: ${ensureError(e).message}`,\n });\n }\n }\n\n return { prepared, problems };\n }\n\n /**\n * Stage 3 of an apply — create the blocks in one transaction, all or nothing.\n *\n * An entry it cannot create throws, the transaction is never committed, and the project keeps\n * none of the blocks the apply had placed.\n */\n private async applyPreparedEntries(\n id: ProjectId,\n document: ProjectTemplateV1,\n prepared: Map<string, PreparedTemplateEntry>,\n author?: AuthorMarker,\n ): Promise<TemplateApplyReport> {\n const rid = await this.resolveProjectId(id);\n let added: AppliedEntry[] = [];\n try {\n await withProjectAuthored(\n this.env.projectHelper,\n this.pl,\n rid,\n author,\n (mut) => {\n added = applyTemplateEntries({\n document,\n placer: mut,\n entries: prepared,\n projectHelper: this.env.projectHelper,\n });\n },\n // Under the same lock an open session's own mutations take, so an apply and a user\n // editing the project cannot interleave.\n { name: \"applyTemplateToProject\", lockId: `project:${id}` },\n );\n } catch (e: unknown) {\n // A statement about the file: the transaction went with the throw, so the project kept\n // none of the blocks the apply had placed, and `added` is empty by construction.\n // Anything else — a backend that refused the write, say — is not about the file and\n // keeps propagating.\n if (!(e instanceof TemplateEntryRejected)) throw e;\n return { added: [], problems: [{ entryId: e.entryId, error: e.message }] };\n }\n\n return { added, problems: [] };\n }\n\n //\n // Template List Manipulation\n //\n\n private readonly templateIdCache = new LRUCache<TemplateId, SignedResourceId>({ max: 1024 });\n\n /**\n * Saves a project as a template: a snapshot of its blocks and their params, no data.\n *\n * The document the export produced is what gets stored; the YAML it also rendered is a\n * file format, and a stored template is rendered to it only on download.\n *\n * A block that cannot be expressed as a template entry stores nothing at all, and every\n * such block is reported — fixing an unexportable project takes one pass, not one per block.\n *\n * @param projectId project to snapshot\n * @param label label for the template; defaults to the project's own label\n */\n public async saveProjectAsTemplate(\n projectId: ProjectId,\n label?: string,\n ): Promise<SaveProjectAsTemplateOutcome> {\n const outcome = await this.exportProjectAsTemplate(projectId);\n if (!outcome.ok) return { ok: false, problems: outcome.problems };\n\n const rid = await this.resolveProjectId(projectId);\n let tpl: ResourceRef;\n await this.pl.withWriteTx(\"MLSaveProjectAsTemplate\", async (tx) => {\n const meta = await tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey);\n tpl = createTemplate(tx, this.templateListResourceId, label ?? meta.label, {\n schemaVersion: 1,\n document: outcome.document,\n sourceProjectLabel: meta.label,\n });\n await tx.commit();\n });\n await this.templateListTree.refreshState();\n\n const signedRid = await tpl!.globalId;\n const templateId = resourceIdToString(signedRid) as TemplateId;\n this.templateIdCache.set(templateId, signedRid);\n return { ok: true, templateId };\n }\n\n /** Changes a template's label. The stored document is immutable and stays untouched —\n * improving a template means saving a new one. */\n public async renameTemplate(id: TemplateId, label: string): Promise<void> {\n const rid = await this.resolveTemplateId(id);\n await this.pl.withWriteTx(\"MLRenameTemplate\", async (tx) => {\n renameTemplate(tx, rid, label);\n await tx.commit();\n });\n await this.templateListTree.refreshState();\n }\n\n /** Permanently deletes a template from the template list. */\n public async deleteTemplate(id: TemplateId): Promise<void> {\n await this.pl.withWriteTx(\"MLRemoveTemplate\", async (tx) => {\n await deleteTemplate(tx, this.templateListResourceId, id);\n await tx.commit();\n });\n this.templateIdCache.delete(id);\n await this.templateListTree.refreshState();\n }\n\n /** Reads a stored template: its document plus what was true when it was taken. */\n public async getTemplateData(id: TemplateId): Promise<StoredTemplateData> {\n const rid = await this.resolveTemplateId(id);\n return await this.pl.withReadTx(\"MLGetTemplate\", async (tx) => {\n const rd = await tx.getResourceData(rid, false);\n if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);\n return decodeStoredTemplateData(rd.data);\n });\n }\n\n /**\n * Where each entry of a stored template would get its block from, and which entries have\n * nowhere to get one — resolution creates nothing, so this is the preview a UI shows before\n * offering Apply. {@link createProjectFromTemplate} runs the same stage itself.\n */\n public async resolveTemplate(\n id: TemplateId,\n provider: BlockPackProvider,\n options: { allowUnstable?: boolean } = {},\n ): Promise<TemplateResolveOutcome> {\n const stored = await this.getTemplateData(id);\n return await resolveTemplateEntries(stored.document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n }\n\n /**\n * Creates one project holding every block the stored template lists, in the template's order.\n *\n * Resolution and preparation run before the project exists, so a template with an entry\n * nothing can supply a block for leaves no empty project in the list. The one write that\n * follows is all or nothing, and an entry it rejects takes the project with it.\n *\n * @param id template to apply\n * @param label label for the new project\n * @param provider where each entry's block comes from\n * @param options `allowUnstable` widens resolution to pre-release implementations\n */\n public async createProjectFromTemplate(\n id: TemplateId,\n label: string,\n provider: BlockPackProvider,\n options: { allowUnstable?: boolean; author?: AuthorMarker } = {},\n ): Promise<CreateProjectFromTemplateOutcome> {\n const stored = await this.getTemplateData(id);\n\n const preparation = await this.prepareTemplateEntries(stored.document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n if (preparation.problems.length > 0) return { ok: false, problems: preparation.problems };\n\n const projectId = await this.createProject({ label });\n const report = await this.applyPreparedEntries(\n projectId,\n stored.document,\n preparation.prepared,\n options.author,\n );\n if (report.problems.length > 0) {\n // The apply is one transaction, so the project holds none of the blocks: it is the empty\n // project this call created moments ago and nothing else, and leaving it in the list would\n // show the user a project they never asked for.\n await this.deleteProject(projectId);\n return { ok: false, problems: report.problems };\n }\n return { ok: true, projectId, added: report.added };\n }\n\n /** Resolves a TemplateId to a signed SignedResourceId.\n * Uses LRU cache with TX-scan fallback. */\n private async resolveTemplateId(templateId: TemplateId): Promise<SignedResourceId> {\n const cached = this.templateIdCache.get(templateId);\n if (cached !== undefined) return cached;\n\n // Cache miss — scan template list fields to find the matching resource\n const rid = await this.pl.withReadTx(\"ResolveTemplateId\", async (tx) => {\n const data = await tx.getResourceData(this.templateListResourceId, true);\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (resourceIdToString(f.value) === (templateId as string)) return f.value;\n }\n throw new Error(`Template ${templateId} not found in template list.`);\n });\n\n this.templateIdCache.set(templateId, rid);\n return rid;\n }\n\n /** Permanently deletes project from the project list, this will result in\n * destruction of all attached objects, like files, analysis results etc. */\n public async deleteProject(id: ProjectId): Promise<void> {\n await this.pl.withWriteTx(\"MLRemoveProject\", async (tx) => {\n const data = await tx.getResourceData(this.projectListResourceId, true);\n let fieldName: string | undefined;\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (resourceIdToString(f.value) === (id as string)) {\n fieldName = f.name;\n break;\n }\n }\n if (fieldName === undefined) throw new Error(`Project ${id} not found in project list.`);\n tx.removeField(field(this.projectListResourceId, fieldName));\n await tx.commit();\n });\n this.projectIdCache.delete(id);\n await this.projectListTree.refreshState();\n }\n\n /**\n * Duplicates an existing project and adds the copy to this user's project list.\n *\n * @param srcProjectId - project id of the project to duplicate\n * @param rename - optional function that receives the source label and all existing\n * project labels (read within the same transaction), and returns the label for the copy\n */\n public async duplicateProject(\n srcProjectId: ProjectId,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n ): Promise<ProjectId> {\n const sourceRid = await this.resolveProjectId(srcProjectId);\n\n const newPrj: ResourceRef = await this.pl.withWriteTx(\"MLDuplicateProject\", async (tx) => {\n // Read source project meta\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n\n // Read all existing project labels from the project list (parallel reads)\n const projectListData = await tx.getResourceData(this.projectListResourceId, true);\n const projectRids = projectListData.fields\n .map((f) => f.value)\n .filter(isNotNullSignedResourceId);\n const existingLabels = (\n await Promise.all(\n projectRids.map((rid) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)),\n )\n ).map((m) => m.label);\n\n // Compute new label\n const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;\n\n // Create the duplicate\n const newPrj = await duplicateProject(\n tx,\n sourceRid,\n { label: newLabel },\n this.env.projectHelper,\n );\n\n // Attach to project list with a random UUID field name\n tx.createField(field(this.projectListResourceId, randomUUID()), \"Dynamic\", newPrj);\n await tx.commit();\n\n return newPrj;\n });\n\n await this.projectListTree.refreshState();\n\n const signedRid = await newPrj.globalId;\n const newProjectId = resourceIdToString(signedRid) as ProjectId;\n this.projectIdCache.set(newProjectId, signedRid);\n return newProjectId;\n }\n\n /**\n * Duplicates a project into another user's root, minted in the TARGET user's color so the target\n * owns it. Sibling of {@link duplicateProject}, but writes into a different root. The source\n * project (on the current client root) is referenced cross-color for its block data, kept alive\n * by refcounting, exactly like accepting a shared project. Works both ways: pull (while\n * impersonating a user, copy their project to yourself) and push (from your own root, copy a\n * project to a user). Admin cross-root op; requires the crossTreeRefs:v1 backend capability.\n */\n public async duplicateProjectToUser(\n srcProjectId: ProjectId,\n targetLogin: string,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n ): Promise<void> {\n if (!this.serverCapabilities.includes(\"crossTreeRefs:v1\"))\n throw new Error(\"duplicateProjectToUser requires the crossTreeRefs:v1 backend capability.\");\n\n const sourceRid = await this.resolveProjectId(srcProjectId);\n const targetRoot = await this.pl.getUserRoot({ login: targetLogin, createIfNotExists: true });\n\n // Run on the target root: the tx default color is the target's, so the new project (and the\n // target's project list, if created here) are minted in the target's color.\n await this.pl.withWriteTxOnRoot(targetRoot, \"MLDuplicateProjectToUser\", async (tx) => {\n // Resolve or lazily create the target root's project list (tx.clientRoot === targetRoot).\n const targetProjectListRid = await ensureProjectListRid(tx);\n\n // Source label + the target's existing labels, for collision-aware renaming.\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const targetListData = await tx.getResourceData(targetProjectListRid, true);\n const existingLabels = (\n await Promise.all(\n targetListData.fields\n .map((f) => f.value)\n .filter(isNotNullSignedResourceId)\n .map((rid) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)),\n )\n ).map((m) => m.label);\n const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;\n\n const newPrj = await duplicateProject(\n tx,\n sourceRid,\n { label: newLabel },\n this.env.projectHelper,\n );\n tx.createField(field(targetProjectListRid, randomUUID()), \"Dynamic\", newPrj);\n await tx.commit();\n });\n }\n\n //\n // Project Sharing (Copy & Share)\n //\n\n /**\n * Shares the given projects (Copy & Share). Snapshots the projects, creates one envelope, and\n * grants it — all in one atomic write transaction, so a failed grant rolls the whole thing back\n * and the outbox is left as it was.\n *\n * Two variants (see {@link ShareProjectsOptions}):\n * - `{ recipients }` — one writable grant per named recipient; the envelope expires after the\n * default TTL (`sharedAt + envelopeTtlMs`).\n * - `{ everyone: true }` — one make-public grant (backend rewrites the target to the\n * everyone-user); the envelope's `expiresAt` is `null`, so it never expires.\n *\n * v1 always passes `mode: \"copy\"`.\n */\n public async shareProjects(\n projectIds: ProjectId[],\n options: ShareProjectsOptions,\n ): Promise<void> {\n if (projectIds.length === 0) throw new Error(\"shareProjects: no projects given\");\n\n // Everyone + replace: refresh the existing everyone-share of this project under its stable\n // shareId (so recipients who already decided aren't re-prompted), if one exists. Found\n // automatically by project overlap; falls through to a fresh share when none exists.\n if (\"everyone\" in options && options.replace) {\n const priorEveryone = (await this.findSupersedableEnvelopes(projectIds)).find(\n (p) => p.everyone,\n );\n if (priorEveryone !== undefined) {\n await this.changeShare(priorEveryone.shareId, { title: options.title });\n return;\n }\n }\n\n await this.createNewShare(projectIds, options);\n }\n\n /**\n * Mints a fresh share: snapshots the projects into one new envelope (a fresh shareId),\n * supersedes prior shares of the same project, and grants it — all in one atomic write\n * transaction, so a failed grant rolls the whole thing back and the outbox is left as it was.\n * The everyone-refresh path is the {@link changeShare} branch of {@link shareProjects}; this is\n * the mint-a-new-envelope branch.\n */\n private async createNewShare(\n projectIds: ProjectId[],\n options: ShareProjectsOptions,\n ): Promise<void> {\n const everyone = \"everyone\" in options;\n const sources: EnvelopeProjectSource[] = await Promise.all(\n projectIds.map(\n async (id): Promise<EnvelopeProjectSource> => ({\n kind: \"fresh\",\n projectId: id,\n sourceRid: await this.resolveProjectId(id),\n }),\n ),\n );\n const sender = this.currentUserLogin ?? \"\";\n // Targeted share: sharedAt + ttl. Share-with-everybody: never expires (null).\n const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;\n\n // Supersede prior shares of the same project(s) so they never pile up. Resolved before\n // the write tx (ListGrants is a separate RPC). Everyone-share supersedes a prior\n // everyone-share of the same project; a targeted share pulls each named recipient out of\n // any prior share of that project, deleting that share if it ends up with no recipients.\n const priors = await this.findSupersedableEnvelopes(projectIds);\n\n await this.pl.withWriteTx(\"MLShareProjects\", async (tx) => {\n if (everyone) {\n for (const prior of priors) {\n if (prior.everyone) tx.removeField(field(this.sharingOutboxResourceId, prior.fieldName));\n }\n } else {\n const newRecipients = new Set(options.recipients);\n for (const prior of priors) {\n if (prior.everyone) continue; // a single user can't be pulled from an everyone-grant\n const toRemove = prior.recipients.filter((u) => newRecipients.has(u));\n if (toRemove.length === 0) continue;\n const remaining = prior.recipients.filter((u) => !newRecipients.has(u));\n if (remaining.length === 0) {\n // Nobody left on the old share — drop the whole envelope.\n tx.removeField(field(this.sharingOutboxResourceId, prior.fieldName));\n } else {\n for (const u of toRemove) tx.revokeAccess(prior.rid, u);\n }\n }\n }\n\n const { envelope } = await buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {\n mode: options.mode,\n sender,\n title: options.title,\n expiresAt,\n });\n\n // Grant in the same transaction, atomic with the create.\n await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, {\n writable: true,\n });\n\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n }\n\n /**\n * Grants one freshly built envelope inside the transaction that created it: a single make-public\n * grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the\n * target to the everyone-user, gated by role + permission ceiling), or one grant per named\n * recipient.\n *\n * `writable` is not a preference. A project pack needs a writable grant because accepting copies\n * the snapshots out of the envelope, and the cross-color attach rule permits that only to a\n * writable grant holder. A template share copies nothing — the document sits in the envelope's\n * own immutable data — so it is granted read-only, and must be: a writable everyone-grant would\n * hand every user on the server write access to the envelope.\n */\n private async grantShareEnvelope(\n tx: PlTransaction,\n envelope: ResourceRef,\n everyone: boolean,\n recipients: string[],\n permissions: { writable: boolean },\n ): Promise<void> {\n const gid = await envelope.globalId;\n if (everyone) tx.grantAccess(gid, \"\", permissions, GrantType.ANY_AUTHORISED);\n else for (const r of recipients) tx.grantAccess(gid, r, permissions);\n }\n\n /**\n * Shares one stored template. The envelope carries the document itself, so there is no project\n * snapshot and no resource for the recipient to copy out — which is why the grant is read-only.\n * The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only\n * envelope, so a template share never reports who accepted it.\n *\n * Nothing about the document is checked: a stored template is shareable by virtue of existing.\n * An entry the recipient cannot resolve — a block installed from a folder on the sender's\n * machine, say — is theirs to see when they preview or apply it, where every unresolvable entry\n * is named anyway.\n *\n * @param id template to share\n * @param options recipients XOR everyone, plus the title recipients see\n */\n public async shareTemplate(\n id: TemplateId,\n options: ShareTemplateOptions,\n ): Promise<ShareTemplateOutcome> {\n const template = await this.loadTemplateForShare(id);\n\n const everyone = \"everyone\" in options;\n const sender = this.currentUserLogin ?? \"\";\n // Targeted share: sharedAt + ttl. Share-with-everybody: never expires (null).\n const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;\n\n let shareId: ShareId | undefined;\n await this.pl.withWriteTx(\"MLShareTemplate\", async (tx) => {\n const { envelope, data } = buildTemplateShareEnvelope(\n tx,\n this.sharingOutboxResourceId,\n template,\n { sender, title: options.title, expiresAt },\n );\n shareId = data.shareId;\n await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, {\n writable: false,\n });\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n return { shareId: shareId! };\n }\n\n /** The document and the label of a template about to be shared. The label is what the\n * recipient's own list will show, so it travels with the document. */\n private async loadTemplateForShare(\n id: TemplateId,\n ): Promise<{ document: ProjectTemplateV1; label: string }> {\n const rid = await this.resolveTemplateId(id);\n return await this.pl.withReadTx(\"MLReadTemplateForShare\", async (tx) => {\n const rd = await tx.getResourceData(rid, false);\n if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);\n return {\n document: decodeStoredTemplateData(rd.data).document,\n label: await tx.getKValueJson<string>(rid, TemplateLabelKey),\n };\n });\n }\n\n /**\n * Changes a share in place (same {@link ShareId}), in one write transaction: re-snapshots live\n * source projects and carries deleted ones' snapshots forward; applies edited recipients/title;\n * transfers already-decided recipients' accept/reject records (they keep their copy and aren't\n * re-prompted); re-grants; drops the old envelope.\n *\n * `opts.recipients` is the full targeted set (decided users are always kept). `opts.title`\n * replaces the title — omit keeps the current one. `opts.everyone` upgrades targeted ->\n * everyone; the reverse is impossible and ignored.\n *\n * `opts.projectActions` is a per-source-project decision, keyed by projectId: `update`\n * re-snapshots the live source (falls back to carry if the source is gone), `keep` carries the\n * existing snapshot (and its timestamp), `remove` drops the project from the pack. A project not\n * in the map defaults to `keep`. Omit the whole map for the legacy auto behavior (live sources\n * updated, gone ones kept) — the everyone-refresh path relies on that.\n *\n * `opts.templateId` is required for, and only used by, a share that carries a template: a stored\n * template is immutable, so an improved one is a different template and the share cannot re-read\n * the one it started from — the caller names the new target. Every other option means the same\n * thing for both kinds of share.\n */\n public async changeShare(\n shareId: ShareId,\n opts: {\n recipients?: string[];\n everyone?: boolean;\n title?: string;\n projectActions?: Record<ProjectId, ProjectChangeAction>;\n templateId?: TemplateId;\n } = {},\n ): Promise<void> {\n // Read outside the write tx: it is two round-trips of its own.\n const target =\n opts.templateId === undefined ? undefined : await this.loadTemplateForShare(opts.templateId);\n\n await this.pl.withWriteTx(\"MLChangeShare\", async (tx) => {\n const old = await this.resolveOutboxEnvelope(tx, shareId);\n if (old === undefined)\n throw new Error(`changeShare: no live share with id ${shareId} in the outbox.`);\n\n const self = this.currentUserLogin ?? \"\";\n const grants = await tx.listGrants(old.rid);\n // A targeted share may be upgraded to everyone; an everyone-share can't be narrowed back.\n const everyone = grants.some((g) => isEveryoneUserLogin(g.user)) || opts.everyone === true;\n const priorRecipients = grants\n .filter((g) => !isEveryoneUserLogin(g.user) && g.user !== self)\n .map((g) => g.user);\n\n if (old.data.payload.kind === \"template\") {\n if (target === undefined)\n throw new Error(\n `changeShare: share ${shareId} carries a template, so it needs an explicit target ` +\n \"template — a stored template never changes, so an improved one is a different template.\",\n );\n const recipients = everyone ? [] : (opts.recipients ?? priorRecipients);\n\n // Same shareId, same outbox field name — detach the old field before rebuilding, or they collide.\n tx.removeField(field(this.sharingOutboxResourceId, old.fieldName));\n const { envelope } = buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, target, {\n sender: self,\n title: opts.title === undefined ? old.data.title : opts.title.trim(),\n expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,\n shareId, // SAME shareId — the essence of change\n });\n\n // Nothing to transfer: a read-only grant cannot write an acceptance, so a template share\n // never accumulated one.\n await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: false });\n await tx.commit();\n return;\n }\n\n // Read the old envelope's project snapshots (uuid -> rid) and accept/reject records.\n const oldRd = await tx.getResourceData(old.rid, true);\n const snapshotByUuid = new Map<string, SignedResourceId>();\n const acceptances: { login: string; acc: EnvelopeAcceptance }[] = [];\n for (const f of oldRd.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (isEnvelopeProjectField(f.name)) {\n snapshotByUuid.set(envelopeProjectFieldUuid(f.name), f.value);\n } else if (isAcceptanceField(f.name)) {\n const raw = (await tx.getResourceData(f.value, false)).data;\n if (raw === undefined) continue;\n acceptances.push({\n login: acceptanceFieldLogin(f.name),\n acc: cachedDeserialize(raw) as EnvelopeAcceptance,\n });\n }\n }\n const decidedLogins = acceptances.map((a) => a.login);\n\n // Everyone-shares ignore recipients; targeted shares keep decided users plus the edited set.\n const recipients = everyone\n ? []\n : Array.from(new Set([...(opts.recipients ?? priorRecipients), ...decidedLogins]));\n\n // Live source projects by persistable id — these get a fresh snapshot.\n const liveProjects = new Map<string, SignedResourceId>();\n const projList = await tx.getResourceData(this.projectListResourceId, true);\n for (const f of projList.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n liveProjects.set(resourceIdToString(f.value), f.value);\n }\n\n // Per project (keyed by field uuid), apply the caller's decision (default `keep`); with no\n // projectActions map, fall back to the legacy auto behavior: update a live source, keep a gone one.\n const actions = opts.projectActions;\n const sources: EnvelopeProjectSource[] = [];\n const oldProjects = envelopeProjectMap(old.data);\n for (const uuid of Object.keys(oldProjects) as ProjectFieldUuid[]) {\n const { label, source, updatedAt } = oldProjects[uuid];\n const liveRid = liveProjects.get(source);\n\n const action = actions\n ? (actions[source] ?? \"keep\")\n : liveRid !== undefined\n ? \"update\"\n : \"keep\";\n if (action === \"remove\") continue;\n\n if (action === \"update\" && liveRid !== undefined) {\n sources.push({ kind: \"fresh\", projectId: source, sourceRid: liveRid });\n } else {\n // keep, or an \"update\" whose source vanished before commit (deleted meanwhile, e.g. from\n // another client): carry the prior snapshot. Liveness is read inside this write tx — race-safe.\n const snapshotRid = snapshotByUuid.get(uuid);\n if (snapshotRid !== undefined)\n sources.push({ kind: \"carry\", projectId: source, label, snapshotRid, updatedAt });\n }\n }\n\n // Omit (undefined) keeps the current title; a provided value replaces it.\n const title = opts.title === undefined ? old.data.title : opts.title.trim();\n const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;\n\n // Same shareId, same outbox field name — detach the old field before rebuilding, or they collide.\n tx.removeField(field(this.sharingOutboxResourceId, old.fieldName));\n\n const { envelope } = await buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {\n mode: old.data.mode,\n sender: self,\n title,\n expiresAt,\n shareId, // SAME shareId — the essence of change\n });\n\n // Transfer the decided users' records onto the new envelope (donor-written copies).\n for (const { login, acc } of acceptances) {\n if (!everyone && !recipients.includes(login)) continue;\n writeEnvelopeAcceptance(tx, envelope, login, acc.action, acc.timestamp);\n }\n\n await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: true });\n\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n }\n\n /**\n * Finds the donor's own outgoing envelopes built from any of the given source projects —\n * the supersede candidates for a fresh share of the same project(s). Reads each envelope's\n * recipient set via `ListGrants` so the caller can pull individual recipients or detect an\n * everyone-share.\n */\n private async findSupersedableEnvelopes(projectIds: ProjectId[]): Promise<\n {\n fieldName: string;\n rid: SignedResourceId;\n shareId: ShareId;\n everyone: boolean;\n recipients: string[];\n }[]\n > {\n const wanted = new Set(projectIds);\n\n const matched = await this.pl.withReadTx(\"MLFindSupersede\", async (tx) => {\n const outbox = await tx.getResourceData(this.sharingOutboxResourceId, true);\n const out: { fieldName: string; rid: SignedResourceId; shareId: ShareId }[] = [];\n for (const f of outbox.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const rd = await tx.getResourceData(f.value, false);\n if (rd.data === undefined) continue;\n const data = decodeEnvelopeData(rd.data);\n if (data === undefined) continue;\n if (Object.values(envelopeProjectMap(data)).some((p) => wanted.has(p.source)))\n out.push({ fieldName: f.name, rid: f.value, shareId: data.shareId });\n }\n return out;\n });\n\n return await Promise.all(\n matched.map(async ({ fieldName, rid, shareId }) => {\n const grants = await this.pl.userResources.listGrants(rid);\n return {\n fieldName,\n rid,\n shareId,\n everyone: grants.some((g) => isEveryoneUserLogin(g.user)),\n recipients: grants.filter((g) => !isEveryoneUserLogin(g.user)).map((g) => g.user),\n };\n }),\n );\n }\n\n /**\n * Revokes and deletes an outgoing share for all recipients: detaches and deletes the envelope, and\n * its grants are revoked along with it. Already-accepted copies are unaffected (ref-counting keeps\n * the adopted resources alive). Idempotent — revoking a share that is already gone is a no-op.\n */\n public async revokeShare(shareId: ShareId): Promise<void> {\n await this.pl.withWriteTx(\"MLRevokeShare\", async (tx) => {\n const target = await this.resolveOutboxEnvelope(tx, shareId);\n if (target === undefined) return;\n tx.removeField(field(this.sharingOutboxResourceId, target.fieldName));\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n }\n\n /**\n * Resolves a live envelope from the donor's own outbox by its logical `shareId`, returning the\n * outbox field name (for detach), the signed envelope id, and its decoded {@link EnvelopeData}.\n * The outbox is keyed by `{shareId}` directly, but a replaced/legacy share may have drifted, so\n * we match on the decoded `shareId` rather than the field name alone.\n */\n private async resolveOutboxEnvelope(\n tx: PlTransaction,\n shareId: ShareId,\n ): Promise<{ fieldName: string; rid: SignedResourceId; data: EnvelopeData } | undefined> {\n const outboxData = await tx.getResourceData(this.sharingOutboxResourceId, true);\n for (const f of outboxData.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const rd = await tx.getResourceData(f.value, false);\n if (rd.data === undefined) continue;\n const data = decodeEnvelopeData(rd.data);\n if (data === undefined) continue;\n if (data.shareId === shareId) return { fieldName: f.name, rid: f.value, data };\n }\n return undefined;\n }\n\n /**\n * Resolves currently-shared envelopes (granted to this user) to their resource ids, keyed by\n * the envelope's logical `shareId`.\n *\n * Reads the {@link liveEnvelopes} Computable — the same shared-resource discovery tree that\n * feeds {@link pendingShares}. This is the single discovery mechanism: there is no separate\n * `ListUserResources` re-stream on every accept/reject. `refreshState()` is awaited first so a\n * just-granted envelope is observed (the tree's discovery poll may otherwise lag a freshly\n * landed grant). The tree is gRPC-only, so this is empty on a REST-connected client.\n */\n private async resolveLiveEnvelopes(): Promise<Map<ShareId, LiveEnvelope>> {\n await this.pendingSharesTree.refreshState();\n const live = (await this.liveEnvelopes.getValue()) ?? [];\n // Dedup by logical shareId (last writer wins — at most one live envelope per shareId).\n const map = new Map<ShareId, LiveEnvelope>();\n for (const e of live) map.set(e.data.shareId, e);\n return map;\n }\n\n /**\n * Accepts one or more pending shares. What accepting does depends on what the share carries: a\n * pack of projects is duplicated into this user's project list, while a template is added to this\n * user's own template list and builds nothing — the recipient decides later whether to apply it.\n * Either way the decision is recorded per share, and a read-write share also gets the\n * donor-visible acceptance written onto its envelope. Per-share failures (e.g. an expiry race)\n * are collected, not short-circuited — the rest still get accepted. Accept-all = pass every\n * current pending shareId.\n *\n * `rename` resolves label collisions (same callback contract as {@link duplicateProject}), but\n * the source lives in the envelope tree, so accept calls the low-level mutator directly. It does\n * not apply to a template share, whose label is not required to be unique.\n */\n public async acceptShare(\n shareIds: ShareId[],\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n ): Promise<{\n accepted: ProjectId[];\n acceptedTemplates: TemplateId[];\n failed: { shareId: ShareId; error: string }[];\n }> {\n const live = await this.resolveLiveEnvelopes();\n const login = this.currentUserLogin;\n\n const accepted: ProjectId[] = [];\n const acceptedTemplates: TemplateId[] = [];\n const failed: { shareId: ShareId; error: string }[] = [];\n\n for (const shareId of shareIds) {\n const envelope = live.get(shareId);\n if (envelope === undefined) {\n failed.push({ shareId, error: \"Share is no longer available.\" });\n continue;\n }\n try {\n const now = Date.now();\n const payload = envelope.data.payload;\n\n if (payload.kind === \"template\") {\n const rid = await this.pl.withWriteTx(\"MLAcceptTemplateShare\", async (tx) => {\n // The template lands on this user's own shelf, keeping who sent it as its provenance.\n const tpl = createTemplate(tx, this.templateListResourceId, payload.label, {\n schemaVersion: 1,\n document: payload.document,\n sender: payload.from,\n });\n\n writeSharingDecision(tx, this.sharingStateResourceId, shareId, {\n decision: \"accepted\",\n timestamp: now,\n envelopeSharedAt: envelope.data.sharedAt,\n acceptedProjects: [], // a template share creates no project\n });\n\n // No acceptance/{login} on the envelope: the grant is read-only, so the write would be\n // refused by the backend, and the donor deliberately gave up that receipt.\n await tx.commit();\n return await tpl.globalId;\n });\n\n const templateId = resourceIdToString(rid) as TemplateId;\n this.templateIdCache.set(templateId, rid);\n acceptedTemplates.push(templateId);\n continue;\n }\n\n const createdRids = await this.pl.withWriteTx(\"MLAcceptShare\", async (tx) => {\n const created = await copyEnvelopeProjectsIntoList(\n tx,\n envelope.rid,\n this.projectListResourceId,\n rename,\n );\n\n // Record the decision on the acceptor's own SharingState, keyed on shareId.\n writeSharingDecision(tx, this.sharingStateResourceId, shareId, {\n decision: \"accepted\",\n timestamp: now,\n envelopeSharedAt: envelope.data.sharedAt,\n acceptedProjects: resourceIdsToStrings(created),\n });\n\n // Read-write share: write the donor-visible acceptance onto the envelope.\n if (login !== null && envelope.data.mode !== \"read-only\")\n writeEnvelopeAcceptance(tx, envelope.rid, login, \"accepted\", now);\n\n await tx.commit();\n return created;\n });\n for (const rid of createdRids) {\n const projectId = resourceIdToString(rid) as ProjectId;\n this.projectIdCache.set(projectId, rid);\n accepted.push(projectId);\n }\n } catch (e) {\n failed.push({ shareId, error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n await Promise.all([\n this.projectListTree.refreshState(),\n this.templateListTree.refreshState(),\n this.sharingStateTree.refreshState(),\n ]);\n return { accepted, acceptedTemplates, failed };\n }\n\n /** Records rejection of a pending share; it never surfaces again. */\n public async rejectShare(shareId: ShareId): Promise<void> {\n const live = await this.resolveLiveEnvelopes();\n const envelope = live.get(shareId);\n const login = this.currentUserLogin;\n const now = Date.now();\n\n await this.pl.withWriteTx(\"MLRejectShare\", async (tx) => {\n writeSharingDecision(tx, this.sharingStateResourceId, shareId, {\n decision: \"rejected\",\n timestamp: now,\n envelopeSharedAt: envelope?.data.sharedAt ?? now,\n acceptedProjects: [],\n });\n\n // Read-write share: write the donor-visible rejection onto the envelope (if still live).\n if (envelope !== undefined && login !== null && envelope.data.mode !== \"read-only\")\n writeEnvelopeAcceptance(tx, envelope.rid, login, \"rejected\", now);\n\n await tx.commit();\n });\n\n await this.sharingStateTree.refreshState();\n }\n\n //\n // Outbox cleanup (donor side)\n //\n\n private static readonly EnvelopeCleanupIntervalMs = 6 * 3600 * 1000; // every 6h\n private envelopeCleanupTimer: ReturnType<typeof setInterval> | undefined;\n\n /** On ML start and every 6h, delete envelopes whose immutable `expiresAt` has passed. */\n private startEnvelopeCleanup(): void {\n void this.runEnvelopeCleanup();\n this.envelopeCleanupTimer = setInterval(() => {\n void this.runEnvelopeCleanup();\n }, MiddleLayer.EnvelopeCleanupIntervalMs);\n // Don't keep the process alive solely for cleanup.\n this.envelopeCleanupTimer.unref?.();\n }\n\n /** Scans the donor's outbox and deletes expired envelopes (backend auto-revokes their grants).\n * Envelopes with `expiresAt: null` (share-with-everybody) are skipped. */\n private async runEnvelopeCleanup(): Promise<void> {\n try {\n const now = Date.now();\n const expired = await this.pl.withReadTx(\"MLEnvelopeCleanupScan\", async (tx) => {\n const data = await tx.getResourceData(this.sharingOutboxResourceId, true);\n const toDelete: { fieldName: string }[] = [];\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const rd = await tx.getResourceData(f.value, false);\n if (rd.data === undefined) continue;\n const envData = decodeEnvelopeData(rd.data);\n if (envData === undefined) continue;\n if (envData.expiresAt === null) continue; // never expires\n if (envData.expiresAt <= now) toDelete.push({ fieldName: f.name });\n }\n return toDelete;\n });\n\n if (expired.length === 0) return;\n\n await this.pl.withWriteTx(\"MLEnvelopeCleanup\", async (tx) => {\n for (const { fieldName } of expired)\n tx.removeField(field(this.sharingOutboxResourceId, fieldName));\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n } catch (e) {\n this.env.logger.warn(\n `envelope cleanup failed: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n\n //\n // Projects\n //\n\n private readonly openedProjects = new Map<ProjectId, Project>();\n\n /** Snapshot writes started by {@link closeProject} and not yet finished. Held only so\n * {@link close} can give them a bounded chance to land. */\n private readonly pendingSnapshotWrites = new Set<Promise<void>>();\n\n private trackSnapshotWrite(write: Promise<void>): void {\n this.pendingSnapshotWrites.add(write);\n void write.finally(() => this.pendingSnapshotWrites.delete(write));\n }\n\n /** Waits for close-boundary snapshot writes that are already running, up to `timeoutMs`.\n *\n * This starts no work: quitting still performs no snapshot of its own. It only lets a write\n * that a project close already began finish, so closing a project and immediately quitting\n * does not routinely lose it. Bounded, because a wedged filesystem must not hang the quit,\n * and losing the write costs one cold open rather than any correctness. */\n private async drainSnapshotWrites(timeoutMs: number): Promise<void> {\n if (this.pendingSnapshotWrites.size === 0) return;\n\n let timer: NodeJS.Timeout | undefined;\n const expiry = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n timer.unref?.();\n });\n try {\n await Promise.race([Promise.allSettled(this.pendingSnapshotWrites), expiry]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n }\n\n /** Opens a project, and starts corresponding project maintenance loop. */\n public async openProject(id: ProjectId): Promise<void> {\n if (this.openedProjects.has(id)) throw new Error(`Project ${id} already opened`);\n const rid = await this.resolveProjectId(id);\n this.openedProjects.set(id, await Project.init(this.env, id, rid));\n this.openedProjectsList.setValue([...this.openedProjects.keys()]);\n }\n\n /** Closes the project, and deallocate all corresponding resources. */\n public async closeProject(id: ProjectId): Promise<void> {\n const prj = this.openedProjects.get(id);\n if (prj === undefined) throw new Error(`Project ${id} not found among opened projects`);\n this.openedProjects.delete(id);\n\n // Snapshot before destroy, and here rather than inside destroy(): destroy() is also what\n // application shutdown runs, and quitting should perform no snapshot work. Terminating the\n // tree invalidates it, so the state has to be taken first either way.\n //\n // Started, not awaited. The capture happens synchronously inside, which is the part that\n // needs the tree alive; the encode and write are up to ten megabytes of work that closing a\n // project should not sit behind. Kept so shutdown can drain it.\n this.trackSnapshotWrite(prj.snapshotOnClose());\n\n await prj.destroy();\n this.openedProjectsList.setValue([...this.openedProjects.keys()]);\n }\n\n /** Returns a project access object for an opened project. */\n public getOpenedProject(id: ProjectId): Project {\n const prj = this.openedProjects.get(id);\n if (prj === undefined) throw new Error(`Project ${id} not found among opened projects`);\n return prj;\n }\n\n /** Returns true if project with given id is currently opened. */\n public isProjectOpened(id: ProjectId): boolean {\n return this.openedProjects.has(id);\n }\n\n /** Counters for the persisted project tree mirrors, or undefined when they are switched off.\n * Reads and hits are what show whether a reopen was actually warm, and the miss breakdown\n * says why it was not. */\n public get treeSnapshotStats(): Readonly<TreeSnapshotStat> | undefined {\n return this.env.treeSnapshots?.getStats();\n }\n\n /**\n * Deallocates all runtime resources consumed by this object and awaits\n * actual termination of event loops and other processes associated with\n * them.\n */\n public async close() {\n if (this.envelopeCleanupTimer !== undefined) clearInterval(this.envelopeCleanupTimer);\n await Promise.all([...this.openedProjects.values()].map((prj) => prj.destroy()));\n // this.env.quickJs;\n await Promise.all([\n this.projectListTree.terminate(),\n this.templateListTree.terminate(),\n this.sharingOutboxTree.terminate(),\n this.sharingStateTree.terminate(),\n this.pendingSharesTree.terminate(),\n ]);\n await this.drainSnapshotWrites(SNAPSHOT_DRAIN_TIMEOUT_MS);\n await this.env.dispose();\n await this.pl.close();\n }\n\n /** @deprecated */\n public async closeAndAwaitTermination() {\n await this.close();\n }\n\n /** Generates sufficiently random string to be used as local secret for the\n * middle layer */\n public static generateLocalSecret(): string {\n return HmacSha256Signer.generateSecret();\n }\n\n /** Returns a block event dispatcher, which can be used to listen to block events. */\n public get blockEventDispatcher(): BlockEventDispatcher {\n return this.env.blockEventDispatcher;\n }\n\n /** Initialize middle layer */\n public static async init(\n pl: PlClient,\n workdir: string,\n _ops: MiddleLayerOpsConstructor,\n ): Promise<MiddleLayer> {\n const ops: MiddleLayerOps = {\n ...DefaultMiddleLayerOpsSettings,\n ...DefaultMiddleLayerOpsPaths(workdir),\n ..._ops,\n };\n\n // overriding debug options from environment variables\n ops.defaultTreeOptions.logStat = getDebugFlags().logTreeStats;\n ops.debugOps.dumpInitialTreeState = getDebugFlags().dumpInitialTreeState;\n // apply MI_TREE_TRAVERSAL only when the embedder hasn't set an explicit mode\n if (\n ops.defaultTreeOptions.traversalMode === undefined &&\n getDebugFlags().treeTraversalMode !== undefined\n )\n ops.defaultTreeOptions.traversalMode = getDebugFlags().treeTraversalMode;\n\n const { projects, templates, sharingOutbox, sharingState } = await pl.withWriteTx(\n \"MLInitialization\",\n async (tx) => {\n // Lazily create each clientRoot-attached singleton resource. Returns the existing\n // resource id if the field is already populated, otherwise creates + locks + sets it.\n const lazyInit = async (\n fieldName: string,\n type: { name: string; version: string },\n ): Promise<{ ref?: ResourceRef; existing?: SignedResourceId }> => {\n const f = field(tx.clientRoot, fieldName);\n tx.createField(f, \"Dynamic\");\n const fData = await tx.getField(f);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(type);\n tx.lock(ref);\n tx.setField(f, ref);\n return { ref };\n }\n return { existing: fData.value };\n };\n\n const projectsR = await lazyInit(ProjectsField, ProjectsResourceType);\n const templatesR = await lazyInit(TemplatesField, TemplatesResourceType);\n const outboxR = await lazyInit(SharingOutboxField, SharingOutboxResourceType);\n const stateR = await lazyInit(SharingStateField, SharingStateResourceType);\n\n await tx.commit();\n\n return {\n projects: projectsR.existing ?? (await projectsR.ref!.globalId),\n templates: templatesR.existing ?? (await templatesR.ref!.globalId),\n sharingState: stateR.existing ?? (await stateR.ref!.globalId),\n sharingOutbox: outboxR.existing ?? (await outboxR.ref!.globalId),\n };\n },\n );\n\n const logger = ops.logger;\n\n const driverKit = await initDriverKit(pl, workdir, ops.frontendDownloadPath, ops);\n\n // passed to components having no own retry logic\n const retryHttpDispatcher = new RetryAgent(pl.httpDispatcher);\n\n const v2RegistryProvider = new V2RegistryProvider(retryHttpDispatcher);\n\n const bpPreparer = new BlockPackPreparer(\n v2RegistryProvider,\n driverKit.signer,\n retryHttpDispatcher,\n );\n\n const quickJs = await getQuickJS();\n\n const runtimeCapabilities = new RuntimeCapabilities();\n // add runtime capabilities of model here\n runtimeCapabilities.addSupportedRequirement(\"requiresModelAPIVersion\", 1);\n runtimeCapabilities.addSupportedRequirement(\"requiresModelAPIVersion\", 2);\n runtimeCapabilities.addSupportedRequirement(\"requiresCreatePTable\", 2);\n runtimeCapabilities.addSupportedRequirement(\"requiresPFramesVersion\", REQUIRES_PFRAMES_VERSION);\n registerServiceCapabilities((flag, value) =>\n runtimeCapabilities.addSupportedRequirement(flag, value),\n );\n // runtime capabilities of the desktop are to be added by the desktop app / test framework\n\n const serviceRegistry = createModelServiceRegistry({ logger });\n\n const treeSnapshots = TreeSnapshotStore.create(pl, {\n dir: ops.treeSnapshotPath,\n maxSizeBytes: ops.treeSnapshotOps.maxSizeBytes,\n enabled: ops.treeSnapshotOps.enabled,\n logger,\n });\n if (ops.treeSnapshotOps.enabled) {\n // Housekeeping before any project opens: drop snapshots from other builds, backends and\n // users, then trim to the ceiling.\n await treeSnapshots?.evict();\n } else {\n // Switched off, so reclaim what earlier sessions left on disk. The reason to reach for\n // this switch is usually the disk itself, and leaving the files behind would answer the\n // wrong half of that complaint. Keyed on the setting, not on the store being absent: it\n // is also absent for an impersonated client, whose session must not delete anything.\n await TreeSnapshotStore.purge(ops.treeSnapshotPath, logger);\n }\n\n const env: MiddleLayerEnvironment = {\n pl,\n blockEventDispatcher: new BlockEventDispatcher(),\n signer: driverKit.signer,\n logger,\n httpDispatcher: pl.httpDispatcher,\n retryHttpDispatcher,\n ops,\n bpPreparer,\n frontendDownloadDriver: driverKit.frontendDriver,\n driverKit,\n blockUpdateWatcher: new BlockUpdateWatcher(v2RegistryProvider, logger, {\n minDelay: ops.devBlockUpdateRecheckInterval,\n http: retryHttpDispatcher,\n preferredUpdateChannel: ops.preferredUpdateChannel,\n }),\n runtimeCapabilities,\n serviceRegistry,\n quickJs,\n projectHelper: new ProjectHelper(quickJs, logger),\n treeSnapshots,\n dispose: async () => {\n await serviceRegistry.dispose();\n await retryHttpDispatcher.destroy();\n await driverKit.dispose();\n },\n };\n\n const openedProjects = new WatchableValue<ProjectId[]>([]);\n const projectListTC = await createProjectList(pl, projects, openedProjects, env);\n const templateListTC = await createTemplateList(pl, templates, env);\n\n // Project sharing trees and reactive views.\n const outgoingTC = await createOutgoingShares(pl, sharingOutbox, env);\n const sharingStateTree = await createSharingStateTree(pl, sharingState, env);\n const pendingSharesTree = await createPendingSharesTree(pl, env);\n const pendingShares = createPendingSharesComputable(\n pendingSharesTree,\n sharingStateTree,\n pl.userResources.authUser,\n );\n const liveEnvelopes = createLiveEnvelopesComputable(pendingSharesTree);\n\n return new MiddleLayer(\n env,\n driverKit,\n driverKit.signer,\n projects,\n templates,\n sharingOutbox,\n sharingState,\n openedProjects,\n projectListTC.tree,\n templateListTC.tree,\n outgoingTC.tree,\n sharingStateTree,\n pendingSharesTree,\n v2RegistryProvider,\n projectListTC.computable,\n templateListTC.computable,\n outgoingTC.computable,\n pendingShares,\n liveEnvelopes,\n );\n }\n}\n\n//\n// Internals\n//\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"middle_layer.js","names":[],"sources":["../../src/middle_layer/middle_layer.ts"],"sourcesContent":["import type {\n PlClient,\n PlTransaction,\n SignedResourceId,\n ResourceRef,\n Role,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n GrantType,\n isNullSignedResourceId,\n resourceIdToString,\n} from \"@milaboratories/pl-client\";\nimport { LRUCache } from \"lru-cache\";\nimport {\n createProjectList,\n ensureProjectListRid,\n ProjectsField,\n ProjectsResourceType,\n} from \"./project_list\";\nimport type { FoldersListing, FoldersRids } from \"./folders\";\nimport {\n createFolder,\n createFolderList,\n deleteFolder,\n moveFolderItems,\n nameTakenMessage,\n openFoldersTx,\n previewFolderDeletion,\n previewFoldersMove,\n foldersLocalSubtree,\n FoldersField,\n FoldersResourceType,\n renameFolder,\n resetFolders,\n setFolderDescription,\n} from \"./folders\";\nimport type {\n FolderId,\n FoldersItem,\n FoldersLeafItem,\n FoldersMoveOutcome,\n FoldersMovePlan,\n FoldersMovePlanResult,\n FoldersRemoval,\n FoldersRemovalOutcome,\n FoldersRemovalPlanResult,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n CreateProjectFromTemplateOutcome,\n SaveProjectAsTemplateOutcome,\n StoredTemplateData,\n TemplateId,\n TemplateListEntry,\n} from \"./template_list\";\nimport {\n createTemplateList,\n decodeStoredTemplateData,\n TemplateDescriptionKey,\n TemplateLabelKey,\n TemplatesField,\n TemplatesResourceType,\n} from \"./template_list\";\nimport {\n createTemplate,\n deleteTemplate,\n renameTemplate,\n setTemplateDescription,\n} from \"../mutator/template\";\nimport { listedById, notListedError } from \"../mutator/list\";\nimport {\n createProject,\n duplicateProject,\n withProject,\n withProjectAuthored,\n} from \"../mutator/project\";\nimport type { ProjectTemplateExportOutcome } from \"../model/template_serializer\";\nimport type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { asProjectId, asTemplateId } from \"@milaboratories/pl-model-common\";\nimport { extractConfig, ensureError } from \"@platforma-sdk/model\";\nimport type { TemplateApplyProblem, TemplateApplyReport } from \"../model/template_apply\";\nimport { TemplateEntryRejected, kindMismatch } from \"../model/template_apply\";\nimport type { BlockPackProvider, TemplateResolveOutcome } from \"../model/template_resolve\";\nimport { resolveTemplateEntries } from \"../model/template_resolve\";\nimport type { PreparedTemplateEntry } from \"../mutator/template_construct\";\nimport { applyTemplateEntries } from \"../mutator/template_construct\";\nimport { throwIfMissingServerCapabilities } from \"./project\";\nimport { cacheBlockPackTemplate } from \"../mutator/template/template_cache\";\nimport { ProjectMetaKey } from \"../model/project_model\";\nimport type { ProjectId } from \"../model/project_model\";\nimport type { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport {\n canGrantToEveryone,\n canImpersonate,\n decodeEnvelopeData,\n SharingOutboxField,\n SharingOutboxResourceType,\n SharingStateField,\n SharingStateResourceType,\n envelopeFolderRoot,\n newEnvelopeFolderId,\n type EnvelopeData,\n type ShareFolderOptions,\n type ShareId,\n type ShareOptions,\n type ShareOutcome,\n type ShareProjectsOptions,\n type ShareTemplateOptions,\n} from \"../model/sharing_model\";\nimport {\n buildFolderShareEnvelope,\n buildShareEnvelope,\n buildTemplateShareEnvelope,\n copyEnvelopeProjectsIntoList,\n writeShareHidden,\n clearShareHidden,\n type EnvelopeFolderSubtree,\n type EnvelopeProjectSource,\n} from \"../mutator/sharing\";\nimport type { LiveEnvelope, OutgoingShare, AvailableShare } from \"./sharing_list\";\nimport {\n createLiveEnvelopesComputable,\n createOutgoingShares,\n createAvailableSharesComputable,\n createAvailableSharesTree,\n createSharingStateTree,\n} from \"./sharing_list\";\nimport { BlockPackPreparer } from \"../mutator/block-pack/block_pack\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { BlockEventDispatcher } from \"@milaboratories/ts-helpers\";\nimport { HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport type { Computable, ComputableStableDefined } from \"@milaboratories/computable\";\nimport { WatchableValue } from \"@milaboratories/computable\";\nimport { Project } from \"./project\";\nimport type { MiddleLayerOps, MiddleLayerOpsConstructor } from \"./ops\";\nimport { DefaultMiddleLayerOpsPaths, DefaultMiddleLayerOpsSettings } from \"./ops\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectListEntry } from \"../model\";\nimport type {\n AuthorMarker,\n ProjectMeta,\n BlockPlatform,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport {\n foldersNameTaken,\n foldersUniqueName,\n inheritedFolder,\n normalizeDescription,\n} from \"@milaboratories/pl-model-middle-layer\";\nimport type { AppliedEntry } from \"../model/template_apply\";\nimport { BlockUpdateWatcher } from \"../block_registry/watcher\";\nimport type { QuickJSWASMModule } from \"quickjs-emscripten\";\nimport { getQuickJS } from \"quickjs-emscripten\";\nimport type { MiddleLayerDriverKit } from \"./driver_kit\";\nimport { initDriverKit } from \"./driver_kit\";\nimport type { BlockCodeFeatureFlags, DriverKit, SupportedRequirement } from \"@platforma-sdk/model\";\nimport { RuntimeCapabilities } from \"@platforma-sdk/model\";\nimport {\n type ModelServiceRegistry,\n registerServiceCapabilities,\n REQUIRES_PFRAMES_VERSION,\n} from \"@milaboratories/pl-model-common\";\nimport { createModelServiceRegistry } from \"../service_factories\";\nimport type { DownloadUrlDriver } from \"@milaboratories/pl-drivers\";\nimport { V2RegistryProvider } from \"../block_registry\";\nimport type { Dispatcher } from \"undici\";\nimport { RetryAgent } from \"undici\";\nimport { getDebugFlags } from \"../debug\";\nimport { ProjectHelper } from \"../model/project_helper\";\nimport type { TreeSnapshotStat } from \"./tree_snapshot_store\";\nimport { TreeSnapshotStore } from \"./tree_snapshot_store\";\n\n/** How long shutdown waits for close-boundary snapshot writes that are already running. Long\n * enough for a ten-megabyte encode and write on ordinary storage, short enough that a wedged\n * filesystem does not hold the quit open. */\nconst SNAPSHOT_DRAIN_TIMEOUT_MS = 5_000;\n\nexport interface MiddleLayerEnvironment {\n dispose(): Promise<void>;\n readonly pl: PlClient;\n readonly runtimeCapabilities: RuntimeCapabilities;\n readonly logger: MiLogger;\n readonly blockEventDispatcher: BlockEventDispatcher;\n readonly httpDispatcher: Dispatcher;\n readonly retryHttpDispatcher: Dispatcher;\n readonly signer: Signer;\n readonly ops: MiddleLayerOps;\n readonly bpPreparer: BlockPackPreparer;\n readonly frontendDownloadDriver: DownloadUrlDriver;\n readonly blockUpdateWatcher: BlockUpdateWatcher;\n readonly quickJs: QuickJSWASMModule;\n readonly driverKit: MiddleLayerDriverKit;\n readonly serviceRegistry: ModelServiceRegistry;\n readonly projectHelper: ProjectHelper;\n /** Persisted project tree mirrors. Undefined when snapshots are switched off, or when the\n * client is impersonating another user, in which case nothing is read or written. */\n readonly treeSnapshots?: TreeSnapshotStore;\n}\n\n/**\n * Main access object to work with pl from UI.\n *\n * It implements an abstraction layer of projects and blocks.\n *\n * As a main entry point inside the pl, this object uses a resource attached\n * via the {@link ProjectsField} to the pl client's root, this resource\n * contains project list.\n *\n * Read about alternative roots, if isolated project lists (working environments)\n * are required.\n * */\nexport class MiddleLayer {\n public readonly pl: PlClient;\n\n private constructor(\n private readonly env: MiddleLayerEnvironment,\n public readonly driverKit: DriverKit,\n public readonly signer: Signer,\n private readonly projectListResourceId: SignedResourceId,\n private readonly templateListResourceId: SignedResourceId,\n private readonly sharingOutboxResourceId: SignedResourceId,\n private readonly sharingStateResourceId: SignedResourceId,\n private readonly foldersResourceId: SignedResourceId,\n private readonly openedProjectsList: WatchableValue<ProjectId[]>,\n private readonly projectListTree: SynchronizedTreeState,\n private readonly templateListTree: SynchronizedTreeState,\n private readonly foldersTree: SynchronizedTreeState,\n private readonly sharingOutboxTree: SynchronizedTreeState,\n private readonly sharingStateTree: SynchronizedTreeState,\n private readonly availableSharesTree: SynchronizedTreeState,\n public readonly blockRegistryProvider: V2RegistryProvider,\n /** Contains a reactive list of projects along with their meta information. */\n public readonly projectList: ComputableStableDefined<ProjectListEntry[]>,\n /** Contains a reactive list of stored templates along with their labels and provenance. */\n public readonly templateList: ComputableStableDefined<TemplateListEntry[]>,\n /** The folder tree and the project list, already joined. The desktop reads this and never\n * the two halves, so it cannot render a list and a tree that are one refresh apart. */\n public readonly folders: ComputableStableDefined<FoldersListing>,\n /** Reactive view of the donor's outbox — the shares this user has created. */\n public outgoingShares: Computable<OutgoingShare[] | undefined>,\n /** Shares granted to this user, hidden ones included and flagged as such. Fed by the\n * shared-resource discovery tree. */\n public availableShares: Computable<AvailableShare[] | undefined>,\n /** Internal: the recipient's currently-live envelopes, read from the same shared-resource\n * discovery tree as {@link availableShares}. The single source {@link copyShare} resolves\n * live envelopes from — no second discovery path. */\n private readonly liveEnvelopes: Computable<LiveEnvelope[] | undefined>,\n ) {\n this.pl = this.env.pl;\n this.startEnvelopeCleanup();\n }\n\n /**\n * Get the OS where backend is running.\n * For old backend versions returns undefined.\n */\n public get serverPlatform(): BlockPlatform | undefined {\n return this.pl.serverInfo.platform as BlockPlatform | undefined;\n }\n\n /**\n * Runtime capabilities advertised by the connected backend (tokens of\n * the form `<feature>:<version>`, e.g. \"wasm:v1\"). Empty list if the\n * backend predates the capability mechanism — that's the desired\n * fail-closed behaviour for blocks declaring any `requiredCapabilities`.\n */\n public get serverCapabilities(): string[] {\n return this.pl.serverInfo.capabilities ?? [];\n }\n\n /**\n * Login of the authenticated user, for the \"Signed in as\" UI. `null` when the\n * backend has no auth (local/dev mode) — the UI hides the element.\n */\n public get currentUserLogin(): string | null {\n return this.pl.userResources.authUser;\n }\n\n /**\n * Whether the connected backend supports project sharing. Synthetic — computed\n * in the middle layer from the backend capabilities the share flow needs (the\n * cross-color field-reference relaxation a copy out of a share rests on). It can absorb\n * additional required capabilities later without a UI change.\n */\n public get sharingSupported(): boolean {\n // Sharing does not compose with impersonation (discovery is scoped to the admin's session,\n // decisions attribute to the admin), so it is disabled while impersonating. Admins move\n // projects across roots with duplicateProjectToUser instead.\n return this.serverCapabilities.includes(\"crossTreeRefs:v1\") && !this.impersonating;\n }\n\n /** True when this session is impersonating another user (an admin opened another user's root). */\n public get impersonating(): boolean {\n return this.pl.conf.asUser !== undefined;\n }\n\n /**\n * Role of the authenticated user, from the `GetSessionInfo` RPC surfaced through the\n * pl-client. `null` in no-auth mode (when {@link currentUserLogin} is null).\n */\n public get currentUserRole(): Role | null {\n return this.pl.currentUserRole;\n }\n\n /**\n * Whether the UI offers share-with-everybody — no role policy lives in the UI:\n * serverCapabilities.has(\"publicGrants:v1\") && canGrantToEveryone(currentUserRole)\n * Not a security boundary: a crafted call still hits the backend's role +\n * permission-ceiling gate.\n */\n public get canShareWithEveryone(): boolean {\n return (\n !this.impersonating &&\n this.serverCapabilities.includes(\"publicGrants:v1\") &&\n canGrantToEveryone(this.currentUserRole)\n );\n }\n\n /**\n * Whether the authenticated user may impersonate others (open another user's root). Mirrors\n * the backend's `CanImpersonate` role gate — admin/controller only, never a regular user.\n * Derived from the session role, which stays the authenticated admin's even while\n * impersonating, so this stays true across a switch: the \"return to my root\" affordance must\n * not vanish mid-impersonation. Not a security boundary; the backend re-checks on every call.\n */\n public get currentUserCanImpersonate(): boolean {\n return canImpersonate(this.currentUserRole);\n }\n\n /** Adds a runtime capability to the middle layer. */\n public addRuntimeCapability(\n requirement: SupportedRequirement,\n value: number | boolean = true,\n ): void {\n this.env.runtimeCapabilities.addSupportedRequirement(requirement, value);\n }\n\n /** Checks if the given block feature flags are compatible with the runtime capabilities. */\n public checkBlockCompatibility(featureFlags: BlockCodeFeatureFlags | undefined): boolean {\n return this.env.runtimeCapabilities.checkCompatibility(featureFlags);\n }\n\n /** Returns extended API driver kit used internally by middle layer. */\n public get internalDriverKit(): MiddleLayerDriverKit {\n return this.env.driverKit;\n }\n\n /** Returns the service registry for service introspection. */\n public get serviceRegistry(): ModelServiceRegistry {\n return this.env.serviceRegistry;\n }\n\n //\n // ProjectId ↔ SignedResourceId resolution\n //\n\n private readonly projectIdCache = new LRUCache<ProjectId, SignedResourceId>({ max: 1024 });\n\n /** Resolves a ProjectId to a signed SignedResourceId.\n * Uses LRU cache with TX-scan fallback. */\n private async resolveProjectId(projectId: ProjectId): Promise<SignedResourceId> {\n const cached = this.projectIdCache.get(projectId);\n if (cached !== undefined) return cached;\n\n // Cache miss — scan project list fields to find the matching resource\n const rid = await this.pl.withReadTx(\"ResolveProjectId\", async (tx) => {\n const entry = (await listedById(tx, this.projectListResourceId)).get(projectId);\n if (entry === undefined) throw notListedError(\"Project\", projectId);\n return entry.rid;\n });\n\n this.projectIdCache.set(projectId, rid);\n return rid;\n }\n\n //\n // Project List Manipulation\n //\n\n /**\n * Creates a project with initial state and adds it to project list.\n *\n * `folder` is where it lands, placed in the transaction that creates it so the project never\n * shows up at the top level first. Left out, or naming a folder that is gone, it lands at the\n * top level.\n */\n public async createProject(meta: ProjectMeta, folder?: FolderId): Promise<ProjectId> {\n const signedRid = await this.pl.withWriteTx(\"MLCreateProject\", async (tx) => {\n const tree = folder === undefined ? undefined : await openFoldersTx(tx, this.foldersRids);\n const prj = await createProject(tx, meta);\n tx.createField(field(this.projectListResourceId, randomUUID()), \"Dynamic\", prj);\n const rid = await prj.globalId;\n // A folder deleted while the project was being made costs the project its placement, not\n // its existence.\n if (tree !== undefined && tree.view.folders.some((candidate) => candidate.id === folder))\n tree.place([{ kind: \"project\", id: asProjectId(resourceIdToString(rid)) }], folder);\n await tx.commit();\n return rid;\n });\n await Promise.all([\n this.projectListTree.refreshState(),\n ...(folder === undefined ? [] : [this.foldersTree.refreshState()]),\n ]);\n\n const projectId = asProjectId(resourceIdToString(signedRid));\n this.projectIdCache.set(projectId, signedRid);\n return projectId;\n }\n\n /**\n * Updates the project metadata fields the caller names, leaving the others as they are.\n *\n * A patch rather than a replacement because the label and the description are edited from two\n * different places: a rename that carried a stale description alongside the new name would\n * undo a description edit that happened in between, and the reverse.\n *\n * The label is stored trimmed, and the folder rule governs it, so a label another project\n * inside the same folder already carries is refused rather than written — a folder or a\n * 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\n * turned down. The check and the write share one transaction, so two renames racing for the\n * same name cannot both win. Duplicates an account already holds are tolerated and never\n * rewritten; see the name check of {@link openFoldersTx}.\n */\n public async setProjectMeta(\n id: ProjectId,\n meta: Partial<ProjectMeta>,\n author?: AuthorMarker,\n ): Promise<void> {\n const rid = await this.resolveProjectId(id);\n const label = meta.label?.trim();\n const patch = label === undefined ? meta : { ...meta, label };\n await this.pl.withWriteTx(\"ProjectAction: setProjectMeta\", async (tx) => {\n if (label !== undefined)\n (await openFoldersTx(tx, this.foldersRids)).assertNameFree({ kind: \"project\", id }, label);\n await withProjectAuthored(this.env.projectHelper, tx, rid, author, (prj) => {\n prj.updateMeta(patch);\n });\n await tx.commit();\n });\n await this.projectListTree.refreshState();\n }\n\n //\n // Folders\n //\n\n private get foldersRids(): FoldersRids {\n return {\n folders: this.foldersResourceId,\n projects: this.projectListResourceId,\n templates: this.templateListResourceId,\n };\n }\n\n /** Creates a folder inside `parent`, or at the top level, and returns its id. A name already\n * used there is rejected. */\n public async createFolder(name: string, parent?: FolderId): Promise<FolderId> {\n const id = await createFolder(this.pl, this.foldersRids, name, parent);\n await this.foldersTree.refreshState();\n return id;\n }\n\n /** Renames a folder. A name another folder beside it already carries is rejected. */\n public async renameFolder(folder: FolderId, name: string): Promise<void> {\n await renameFolder(this.pl, this.foldersRids, folder, name);\n await this.foldersTree.refreshState();\n }\n\n /** Sets what a folder says about itself; blank clears it. Descriptions are in no namespace, so\n * nothing is refused here. */\n public async setFolderDescription(folder: FolderId, description: string): Promise<void> {\n await setFolderDescription(this.pl, this.foldersRids, folder, description);\n await this.foldersTree.refreshState();\n }\n\n /** The plan a move would produce — every item and the name it ends up with. Shown for\n * confirmation, then handed back to {@link moveFolderItems} unchanged. */\n public async previewFoldersMove(\n items: readonly FoldersItem[],\n destination?: FolderId,\n ): Promise<FoldersMovePlanResult> {\n return await previewFoldersMove(this.pl, this.foldersRids, items, destination);\n }\n\n /**\n * Moves folders, projects and templates into one destination.\n *\n * The plan is recomputed inside the write transaction and the move commits only when it is\n * identical to `confirmedPlan`; otherwise nothing is written and the fresh plan comes back to\n * be confirmed again.\n */\n public async moveFolderItems(\n items: readonly FoldersItem[],\n destination: FolderId | undefined,\n confirmedPlan: FoldersMovePlan,\n ): Promise<FoldersMoveOutcome> {\n const outcome = await moveFolderItems(\n this.pl,\n this.foldersRids,\n items,\n destination,\n confirmedPlan,\n );\n if (outcome.ok)\n await Promise.all([\n this.foldersTree.refreshState(),\n this.projectListTree.refreshState(),\n this.templateListTree.refreshState(),\n ]);\n return outcome;\n }\n\n /** What deleting a folder would destroy: the subtree of folders, and everything in it. */\n public async previewFolderDeletion(folder: FolderId): Promise<FoldersRemovalPlanResult> {\n return await previewFolderDeletion(this.pl, this.foldersRids, folder);\n }\n\n /** Deletes a folder, every folder inside it, and every project and template held anywhere in\n * that subtree.\n * Refused as `needs-confirmation` until the removal it returns is passed back as\n * `confirmedRemoval`, and as `plan-changed` if the subtree has changed since. */\n public async deleteFolder(\n folder: FolderId,\n confirmedRemoval?: FoldersRemoval,\n ): Promise<FoldersRemovalOutcome> {\n const outcome = await deleteFolder(this.pl, this.foldersRids, folder, confirmedRemoval);\n if (outcome.ok) {\n // What the folder held is gone from the lists, so an id cached for it would resolve to a\n // resource nothing holds any more.\n for (const id of outcome.removal.projects) this.projectIdCache.delete(id);\n for (const id of outcome.removal.templates) this.templateIdCache.delete(id);\n await Promise.all([\n this.foldersTree.refreshState(),\n this.projectListTree.refreshState(),\n this.templateListTree.refreshState(),\n ]);\n }\n return outcome;\n }\n\n /**\n * Replaces a folder document this build cannot read — one written by a newer version, or one\n * nothing here can parse — with an empty one. Every folder is gone afterwards; every project and\n * template stays and shows at the top level. Refused while the document can be read.\n */\n public async resetFolders(): Promise<void> {\n await resetFolders(this.pl, this.foldersRids);\n await this.foldersTree.refreshState();\n }\n\n /**\n * Duplicates a folder and everything under it: the folders inside, a duplicate of every project\n * in them, and a copy of every template.\n *\n * The copy lands beside the source, so only its root needs a name of its own — `X (Copy)`.\n * Nothing inside is renamed: names are compared among siblings of one kind, and a copied folder's children\n * are only ever compared with each other, where they came in distinct already.\n *\n * The subtree is read first and rebuilt in one write transaction, so the folders and everything\n * they hold appear together or not at all.\n */\n public async duplicateFolder(folder: FolderId): Promise<void> {\n const subtree = await this.loadFolderSubtree(folder, () => randomUUID());\n\n const created = await this.pl.withWriteTx(\"MLDuplicateFolder\", async (tx) => {\n const tree = await openFoldersTx(tx, this.foldersRids);\n const items: (FoldersLeafItem & { folder: string })[] = [];\n const projects: SignedResourceId[] = [];\n const templates: { id: TemplateId; rid: SignedResourceId }[] = [];\n\n for (const project of subtree.projects) {\n // The whole metadata carries over, label included: the duplicate lands in a folder of its\n // own that holds nothing else, so there is nothing there for its name to collide with.\n const meta = await tx.getKValueJson<ProjectMeta>(project.rid, ProjectMetaKey);\n const copy = await duplicateProject(tx, project.rid, meta, this.env.projectHelper);\n tx.createField(field(this.projectListResourceId, randomUUID()), \"Dynamic\", copy);\n const rid = await copy.globalId;\n projects.push(rid);\n items.push({\n kind: \"project\",\n id: asProjectId(resourceIdToString(rid)),\n folder: project.folder,\n });\n }\n\n for (const template of subtree.templates) {\n // A stored template is immutable, so its copy is the same blob under a new resource —\n // provenance and all.\n const copy = createTemplate(\n tx,\n this.templateListResourceId,\n { label: template.label, description: template.description },\n template.data,\n );\n const rid = await copy.globalId;\n const id = asTemplateId(resourceIdToString(rid));\n templates.push({ id, rid });\n items.push({ kind: \"template\", id, folder: template.folder });\n }\n\n tree.graft({ root: subtree.root, folders: subtree.folders, items }, subtree.parent);\n\n await tx.commit();\n return { projects, templates };\n });\n\n for (const rid of created.projects)\n this.projectIdCache.set(asProjectId(resourceIdToString(rid)), rid);\n for (const { id, rid } of created.templates) this.templateIdCache.set(id, rid);\n\n await Promise.all([\n this.foldersTree.refreshState(),\n this.projectListTree.refreshState(),\n this.templateListTree.refreshState(),\n ]);\n }\n\n /**\n * A folder subtree as a copy needs it: the folders under ids local to this read, the key the\n * subtree's root goes by, the resource of every project in them, every template read whole, and\n * the folder the source sits in — which is where a duplicate lands.\n *\n * Read in a transaction of its own, before the write that copies it: every template is a read,\n * and none of it has to be atomic with the copying, because a project or a template that\n * disappears meanwhile fails that write on its own.\n */\n private async loadFolderSubtree<Id extends string>(\n folder: FolderId,\n mint: () => Id,\n ): Promise<FolderSubtree<Id>> {\n return await this.pl.withReadTx(\"MLReadFolderSubtree\", async (tx) => {\n const tree = await openFoldersTx(tx, this.foldersRids);\n const source = tree.view.folders.find((candidate) => candidate.id === folder);\n if (source === undefined) throw new Error(`Folder ${folder} does not exist.`);\n\n const { inSubtree, localId, folders } = foldersLocalSubtree(tree.view, folder, mint);\n\n const projects: FolderSubtree<Id>[\"projects\"] = [];\n for (const project of tree.view.projects) {\n if (project.folder === undefined || !inSubtree.has(project.folder)) continue;\n const rid = tree.projectRids.get(project.id);\n if (rid === undefined) throw notListedError(\"Project\", project.id);\n projects.push({ projectId: project.id, rid, folder: localId(project.folder) });\n }\n\n const templates: Promise<FolderSubtree<Id>[\"templates\"][number]>[] = [];\n for (const template of tree.view.templates) {\n if (template.folder === undefined || !inSubtree.has(template.folder)) continue;\n const rid = tree.templateRids.get(template.id);\n if (rid === undefined) throw notListedError(\"Template\", template.id);\n const local = localId(template.folder);\n templates.push(\n readStoredTemplate(tx, template.id, rid).then((stored) => ({ ...stored, folder: local })),\n );\n }\n\n return {\n ...(source.parent === undefined ? {} : { parent: source.parent }),\n root: localId(folder),\n folders,\n projects,\n templates: await Promise.all(templates),\n };\n });\n }\n\n /**\n * Renders a project as a `template-v1` YAML document, or reports every reason it\n * cannot be — the backing call for an \"Export Project as Template…\" command.\n *\n * Takes a project id rather than an open {@link Project} because exporting is a\n * property of the stored project, not of a session with it: the command belongs on\n * a project card, where the project is usually closed. Opening one to read it would\n * spin up trees and watchers for a one-shot read, and then have to decide whether to\n * close them again.\n *\n * Read-only — the underlying mutator touches no field, so the transaction is never\n * committed and the project list needs no refresh.\n *\n * @param id - project id of the project to export\n */\n public async exportProjectAsTemplate(id: ProjectId): Promise<ProjectTemplateExportOutcome> {\n const rid = await this.resolveProjectId(id);\n return await withProject(\n this.env.projectHelper,\n this.pl,\n rid,\n (prj) => prj.exportAsTemplateV1(),\n { name: \"exportProjectAsTemplate\" },\n );\n }\n\n /**\n * Creates the blocks a `template-v1` document describes in an existing project, in the\n * order the document lists them — the backing call for a \"Create Project from Template\n * file…\" command, which is `createProject` followed by this.\n *\n * Takes a project id rather than an open {@link Project}, like\n * {@link exportProjectAsTemplate} and for the same reason: applying a template is a\n * property of the stored project, not of a session with it, and the flow that needs it\n * has just created the project and has no session yet. An already-open session picks the\n * new blocks up through its own refresh loop.\n *\n * Three stages, and their order is the design:\n *\n * 1. **Resolve every entry** to a concrete block pack, through `provider`.\n * 2. **Prepare every block**: fetch it, check it can run against this backend, cache its\n * workflow template, and offer the entry's params to the block's kind for a shape\n * check.\n * 3. **Create the blocks**, in one transaction — each one's params first pointed at this\n * project by the block's own model, since which values in there are references is\n * knowledge only the block has.\n *\n * The first two create nothing, so either of them failing leaves the project exactly as it\n * was. They are also what leaves stage 3 with only in-memory work, and hence able to be a\n * single transaction.\n *\n * **Stage 3 is all or nothing too**, because it is that one transaction: an entry it cannot\n * create throws, the transaction is never committed, and the project keeps none of the\n * blocks the apply had placed. So `problems` non-empty always means `added` is empty, at\n * every stage — a caller never has to reconcile a half-built project, and never has to ask\n * which of the blocks present came from the file.\n *\n * What is NOT checked before the work starts: which entries an entry references. Reading\n * that means reading the params, which only the block can do, and no block exists until\n * stage 2 has fetched one. A file whose entry references one listed below it therefore\n * applies, and the block it creates reports itself as missing references — the same way a\n * reference to a deleted block already behaves.\n *\n * @param id Project to apply into\n * @param document A parsed template document\n * @param provider Where each entry's block comes from\n * @param options `allowUnstable` widens resolution to pre-release implementations, for\n * the whole document\n */\n public async applyTemplateToProject(\n id: ProjectId,\n document: ProjectTemplateV1,\n provider: BlockPackProvider,\n options: { allowUnstable?: boolean; author?: AuthorMarker } = {},\n ): Promise<TemplateApplyReport> {\n const preparation = await this.prepareTemplateEntries(document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n if (preparation.problems.length > 0) return { added: [], problems: preparation.problems };\n return await this.applyPreparedEntries(id, document, preparation.prepared, options.author);\n }\n\n /**\n * Stages 1 and 2 of an apply — resolve every entry to a block pack, then prepare every\n * block — for a document that may not have a project yet.\n *\n * Neither stage creates anything, which is what lets a caller run them before it decides to\n * create a project at all: {@link createProjectFromTemplate} does exactly that, so an\n * unapplicable template leaves no empty project behind.\n */\n private async prepareTemplateEntries(\n document: ProjectTemplateV1,\n provider: BlockPackProvider,\n options: { allowUnstable: boolean },\n ): Promise<{\n prepared: Map<string, PreparedTemplateEntry>;\n problems: TemplateApplyProblem[];\n }> {\n const prepared = new Map<string, PreparedTemplateEntry>();\n\n const resolution = await resolveTemplateEntries(document, provider, options);\n if (resolution.problems.length > 0) return { prepared, problems: [...resolution.problems] };\n\n // One map, not one per field: resolution reports by entry id, so everything this loop\n // needs about an entry is looked up the same way.\n const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));\n const problems: TemplateApplyProblem[] = [];\n\n for (const entry of resolution.resolved) {\n try {\n const documentEntry = byEntryId.get(entry.entryId)!;\n const preparedBp = await this.env.bpPreparer.prepare(entry.spec);\n const blockCfg = extractConfig(preparedBp.config);\n\n // The first question asked of the prepared block: is it the block this entry meant.\n // Everything below is only meaningful once the answer is yes.\n //\n // The locator is appended here rather than inside the check, which names no route:\n // when the file chose the implementation itself, what it chose is the thing to correct.\n const mismatch = kindMismatch(documentEntry.kind, preparedBp.config.kind);\n if (mismatch !== undefined) {\n const locator = documentEntry.location ?? documentEntry.block;\n problems.push({\n entryId: entry.entryId,\n error: locator === undefined ? `${mismatch}.` : `${mismatch} (${locator}).`,\n });\n continue;\n }\n\n // The same two gates `Project.addBlock` applies, for the same reason: a block that\n // cannot run here must not be installed. Here they become per-entry problems\n // rather than throws, so one unusable block reads as one bad entry.\n this.env.runtimeCapabilities.throwIfIncompatible(blockCfg.featureFlags);\n throwIfMissingServerCapabilities(this.pl, preparedBp.requiredCapabilities);\n\n const cachedBp = await cacheBlockPackTemplate(this.pl, preparedBp);\n\n // Offered to the block's kind while nothing has been created yet. Every entry is\n // checked, including one whose file omitted `params` — the parser read that as `{}`,\n // which a kind with required fields rejects, and rightly: it would otherwise apply as\n // a block that looks configured and is not.\n const checked = this.env.projectHelper.validateTemplateParamsInVM(\n blockCfg,\n documentEntry.params,\n );\n if (checked.error !== undefined) {\n problems.push({ entryId: entry.entryId, error: checked.error.message });\n continue;\n }\n\n // The block package's own title, the same thing the add-block UI writes. It is\n // what the user sees for a block whose model derives no title of its own, and it\n // is resolution's to supply — nothing here could reconstruct it.\n prepared.set(entry.entryId, { blockPack: cachedBp, label: entry.title });\n } catch (e) {\n problems.push({\n entryId: entry.entryId,\n error: `This entry's block could not be installed: ${ensureError(e).message}`,\n });\n }\n }\n\n return { prepared, problems };\n }\n\n /**\n * Stage 3 of an apply — create the blocks in one transaction, all or nothing.\n *\n * An entry it cannot create throws, the transaction is never committed, and the project keeps\n * none of the blocks the apply had placed.\n */\n private async applyPreparedEntries(\n id: ProjectId,\n document: ProjectTemplateV1,\n prepared: Map<string, PreparedTemplateEntry>,\n author?: AuthorMarker,\n ): Promise<TemplateApplyReport> {\n const rid = await this.resolveProjectId(id);\n let added: AppliedEntry[] = [];\n try {\n await withProjectAuthored(\n this.env.projectHelper,\n this.pl,\n rid,\n author,\n (mut) => {\n added = applyTemplateEntries({\n document,\n placer: mut,\n entries: prepared,\n projectHelper: this.env.projectHelper,\n });\n },\n // Under the same lock an open session's own mutations take, so an apply and a user\n // editing the project cannot interleave.\n { name: \"applyTemplateToProject\", lockId: `project:${id}` },\n );\n } catch (e: unknown) {\n // A statement about the file: the transaction went with the throw, so the project kept\n // none of the blocks the apply had placed, and `added` is empty by construction.\n // Anything else — a backend that refused the write, say — is not about the file and\n // keeps propagating.\n if (!(e instanceof TemplateEntryRejected)) throw e;\n return { added: [], problems: [{ entryId: e.entryId, error: e.message }] };\n }\n\n return { added, problems: [] };\n }\n\n //\n // Template List Manipulation\n //\n\n private readonly templateIdCache = new LRUCache<TemplateId, SignedResourceId>({ max: 1024 });\n\n /**\n * Saves a project as a template: a snapshot of its blocks and their params, no data.\n *\n * The document the export produced is what gets stored; the YAML it also rendered is a\n * file format, and a stored template is rendered to it only on download.\n *\n * A block that cannot be expressed as a template entry stores nothing at all, and every\n * such block is reported — fixing an unexportable project takes one pass, not one per block.\n *\n * The template lands beside its project, and is named by the rule templates there are named\n * by: a label the caller chose is stored trimmed and refused if another template there already\n * carries it, and without one the project's own label is taken — suffixed only when a template\n * there already answers to it. The project is not a template, so its own name is no obstacle.\n *\n * The template's description is the one the caller gives, stored trimmed; blank means none.\n * Without one the template is stored undescribed, whatever the project's own description says.\n *\n * @param projectId project to snapshot\n * @param label label for the template; defaults to the project's own label, made free\n * @param description what the template is for; blank or absent stores none\n */\n public async saveProjectAsTemplate(\n projectId: ProjectId,\n label?: string,\n description?: string,\n ): Promise<SaveProjectAsTemplateOutcome> {\n const outcome = await this.exportProjectAsTemplate(projectId);\n if (!outcome.ok) return { ok: false, problems: outcome.problems };\n\n const rid = await this.resolveProjectId(projectId);\n const wanted = label?.trim();\n const signedRid = await this.pl.withWriteTx(\"MLSaveProjectAsTemplate\", async (tx) => {\n const meta = await tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey);\n const tree = await openFoldersTx(tx, this.foldersRids);\n const taken = tree.namesTakenBeside(projectId, \"template\");\n if (wanted !== undefined && foldersNameTaken(wanted, taken))\n throw new Error(nameTakenMessage(\"template\", wanted));\n const name = wanted ?? foldersUniqueName(meta.label, taken);\n\n const tpl = createTemplate(\n tx,\n this.templateListResourceId,\n { label: name, description },\n { schemaVersion: 1, document: outcome.document, sourceProjectLabel: meta.label },\n );\n\n // A template taken from a project belongs beside that project, and the placement rides the\n // same transaction so the two can never disagree about where it is. Its name was chosen\n // free there, so nothing can send it anywhere else.\n const created = await tpl.globalId;\n tree.place(\n [{ kind: \"template\", id: asTemplateId(resourceIdToString(created)) }],\n tree.folderOf(projectId),\n );\n await tx.commit();\n return created;\n });\n await Promise.all([this.templateListTree.refreshState(), this.foldersTree.refreshState()]);\n\n const templateId = asTemplateId(resourceIdToString(signedRid));\n this.templateIdCache.set(templateId, signedRid);\n return { ok: true, templateId };\n }\n\n /**\n * Changes a template's label. The stored document is immutable and stays untouched —\n * improving a template means saving a new one.\n *\n * The label is stored trimmed. One another template beside it already carries is refused, in\n * the transaction that writes it; a folder or a project of that name beside it is no obstacle.\n */\n public async renameTemplate(id: TemplateId, label: string): Promise<void> {\n const rid = await this.resolveTemplateId(id);\n const wanted = label.trim();\n await this.pl.withWriteTx(\"MLRenameTemplate\", async (tx) => {\n (await openFoldersTx(tx, this.foldersRids)).assertNameFree({ kind: \"template\", id }, wanted);\n renameTemplate(tx, rid, wanted);\n await tx.commit();\n });\n await this.templateListTree.refreshState();\n }\n\n /** Sets what a stored template says about itself; blank clears it. The stored document is not\n * touched, so it stays byte-identical. */\n public async setTemplateDescription(id: TemplateId, description: string): Promise<void> {\n const rid = await this.resolveTemplateId(id);\n await this.pl.withWriteTx(\"MLSetTemplateDescription\", async (tx) => {\n setTemplateDescription(tx, rid, description);\n await tx.commit();\n });\n await this.templateListTree.refreshState();\n }\n\n /** Permanently deletes a template from the template list. */\n public async deleteTemplate(id: TemplateId): Promise<void> {\n await this.pl.withWriteTx(\"MLRemoveTemplate\", async (tx) => {\n await deleteTemplate(tx, this.templateListResourceId, id);\n await tx.commit();\n });\n this.templateIdCache.delete(id);\n await this.templateListTree.refreshState();\n }\n\n /** Reads a stored template: its document plus what was true when it was taken. */\n public async getTemplateData(id: TemplateId): Promise<StoredTemplateData> {\n const rid = await this.resolveTemplateId(id);\n return await this.pl.withReadTx(\"MLGetTemplate\", async (tx) => {\n const rd = await tx.getResourceData(rid, false);\n if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);\n return decodeStoredTemplateData(rd.data);\n });\n }\n\n /**\n * Where each entry of a stored template would get its block from, and which entries have\n * nowhere to get one — resolution creates nothing, so this is the preview a UI shows before\n * offering Apply. {@link createProjectFromTemplate} runs the same stage itself.\n */\n public async resolveTemplate(\n id: TemplateId,\n provider: BlockPackProvider,\n options: { allowUnstable?: boolean } = {},\n ): Promise<TemplateResolveOutcome> {\n const stored = await this.getTemplateData(id);\n return await resolveTemplateEntries(stored.document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n }\n\n /**\n * Creates one project holding every block the stored template lists, in the template's order.\n *\n * Resolution and preparation run before the project exists, so a template with an entry\n * nothing can supply a block for leaves no empty project in the list. The one write that\n * follows is all or nothing, and an entry it rejects takes the project with it.\n *\n * @param id template to apply\n * @param label label for the new project\n * @param provider where each entry's block comes from\n * @param options `allowUnstable` widens resolution to pre-release implementations; `folder` is\n * where the new project lands\n */\n public async createProjectFromTemplate(\n id: TemplateId,\n label: string,\n provider: BlockPackProvider,\n options: {\n allowUnstable?: boolean;\n author?: AuthorMarker;\n /** Where the project lands; the top level when left out. */\n folder?: FolderId;\n } = {},\n ): Promise<CreateProjectFromTemplateOutcome> {\n const stored = await this.getTemplateData(id);\n\n const preparation = await this.prepareTemplateEntries(stored.document, provider, {\n allowUnstable: options.allowUnstable ?? false,\n });\n if (preparation.problems.length > 0) return { ok: false, problems: preparation.problems };\n\n const projectId = await this.createProject({ label }, options.folder);\n const report = await this.applyPreparedEntries(\n projectId,\n stored.document,\n preparation.prepared,\n options.author,\n );\n if (report.problems.length > 0) {\n // The apply is one transaction, so the project holds none of the blocks: it is the empty\n // project this call created moments ago and nothing else, and leaving it in the list would\n // show the user a project they never asked for.\n await this.deleteProject(projectId);\n return { ok: false, problems: report.problems };\n }\n return { ok: true, projectId, added: report.added };\n }\n\n /** Resolves a TemplateId to a signed SignedResourceId.\n * Uses LRU cache with TX-scan fallback. */\n private async resolveTemplateId(templateId: TemplateId): Promise<SignedResourceId> {\n const cached = this.templateIdCache.get(templateId);\n if (cached !== undefined) return cached;\n\n // Cache miss — scan template list fields to find the matching resource\n const rid = await this.pl.withReadTx(\"ResolveTemplateId\", async (tx) => {\n const entry = (await listedById(tx, this.templateListResourceId)).get(templateId);\n if (entry === undefined) throw notListedError(\"Template\", templateId);\n return entry.rid;\n });\n\n this.templateIdCache.set(templateId, rid);\n return rid;\n }\n\n /** Permanently deletes project from the project list, this will result in\n * destruction of all attached objects, like files, analysis results etc. */\n public async deleteProject(id: ProjectId): Promise<void> {\n await this.pl.withWriteTx(\"MLRemoveProject\", async (tx) => {\n const entry = (await listedById(tx, this.projectListResourceId)).get(id);\n if (entry === undefined) throw notListedError(\"Project\", id);\n tx.removeField(field(this.projectListResourceId, entry.fieldName));\n await tx.commit();\n });\n this.projectIdCache.delete(id);\n await this.projectListTree.refreshState();\n }\n\n /**\n * Duplicates an existing project and adds the copy to this user's project list, beside the\n * project it was copied from.\n *\n * Without `rename` the copy is named by the rule everything beside the source is named by: the\n * source's own label, suffixed to be free there — `X (Copy)`. The name is chosen inside the\n * transaction that creates the copy, against the tree that transaction reads.\n *\n * @param srcProjectId - project id of the project to duplicate\n * @param rename - optional function that receives the source label and all existing\n * project labels (read within the same transaction), and returns the label for the copy.\n * A label chosen this way is the caller's and is never suffixed: when it is already taken\n * beside the source, the copy lands at the top level instead.\n */\n public async duplicateProject(\n srcProjectId: ProjectId,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n ): Promise<ProjectId> {\n const sourceRid = await this.resolveProjectId(srcProjectId);\n\n const newPrj: ResourceRef = await this.pl.withWriteTx(\"MLDuplicateProject\", async (tx) => {\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const tree = await openFoldersTx(tx, this.foldersRids);\n\n // The source's own label is taken beside it even when the tree cannot say what else is.\n const label =\n rename === undefined\n ? foldersUniqueName(sourceMeta.label, [\n sourceMeta.label,\n ...tree.namesTakenBeside(srcProjectId, \"project\"),\n ])\n : rename(sourceMeta.label, await existingProjectLabels(tx, this.projectListResourceId));\n\n // The whole source metadata carries over, so a copy keeps what the original said about\n // itself; only the label is the copy's own.\n const newPrj = await duplicateProject(\n tx,\n sourceRid,\n { ...sourceMeta, label },\n this.env.projectHelper,\n );\n\n // Attach to project list with a random UUID field name\n tx.createField(field(this.projectListResourceId, randomUUID()), \"Dynamic\", newPrj);\n\n // A copy belongs beside the project it was copied from, placed in the same transaction so\n // it is never shown at the top level first.\n const created: FoldersLeafItem = {\n kind: \"project\",\n id: asProjectId(resourceIdToString(await newPrj.globalId)),\n };\n tree.place(\n [created],\n rename === undefined\n ? tree.folderOf(srcProjectId)\n : inheritedFolder(tree.view, srcProjectId, created, label),\n );\n await tx.commit();\n\n return newPrj;\n });\n\n await Promise.all([this.projectListTree.refreshState(), this.foldersTree.refreshState()]);\n\n const signedRid = await newPrj.globalId;\n const newProjectId = asProjectId(resourceIdToString(signedRid));\n this.projectIdCache.set(newProjectId, signedRid);\n return newProjectId;\n }\n\n /**\n * Duplicates a project into another user's root, minted in the TARGET user's color so the target\n * owns it. Sibling of {@link duplicateProject}, but writes into a different root. The source\n * project (on the current client root) is referenced cross-color for its block data, kept alive\n * by refcounting, exactly like a project copied out of a share. Works both ways: pull (while\n * impersonating a user, copy their project to yourself) and push (from your own root, copy a\n * project to a user). Admin cross-root op; requires the crossTreeRefs:v1 backend capability.\n */\n public async duplicateProjectToUser(\n srcProjectId: ProjectId,\n targetLogin: string,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n ): Promise<void> {\n if (!this.serverCapabilities.includes(\"crossTreeRefs:v1\"))\n throw new Error(\"duplicateProjectToUser requires the crossTreeRefs:v1 backend capability.\");\n\n const sourceRid = await this.resolveProjectId(srcProjectId);\n const targetRoot = await this.pl.getUserRoot({ login: targetLogin, createIfNotExists: true });\n\n // Run on the target root: the tx default color is the target's, so the new project (and the\n // target's project list, if created here) are minted in the target's color.\n await this.pl.withWriteTxOnRoot(targetRoot, \"MLDuplicateProjectToUser\", async (tx) => {\n // Resolve or lazily create the target root's project list (tx.clientRoot === targetRoot).\n const targetProjectListRid = await ensureProjectListRid(tx);\n\n // Source label + the target's existing labels, for collision-aware renaming.\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const existingLabels = await existingProjectLabels(tx, targetProjectListRid);\n const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;\n\n // The whole source metadata carries over, so a copy keeps what the original said about\n // itself; only the label is the copy's own.\n const newPrj = await duplicateProject(\n tx,\n sourceRid,\n { ...sourceMeta, label: newLabel },\n this.env.projectHelper,\n );\n tx.createField(field(targetProjectListRid, randomUUID()), \"Dynamic\", newPrj);\n await tx.commit();\n });\n }\n\n //\n // Project Sharing (Copy & Share)\n //\n\n /**\n * Shares the given projects (Copy & Share): snapshots them into one envelope, in the transaction\n * {@link shareEnvelope} describes.\n *\n * v1 always passes `mode: \"copy\"`.\n */\n public async shareProjects(\n projectIds: ProjectId[],\n options: ShareProjectsOptions,\n ): Promise<ShareOutcome> {\n if (projectIds.length === 0) throw new Error(\"shareProjects: no projects given\");\n\n const sources: EnvelopeProjectSource[] = await Promise.all(\n projectIds.map(\n async (id): Promise<EnvelopeProjectSource> => ({\n projectId: id,\n sourceRid: await this.resolveProjectId(id),\n }),\n ),\n );\n\n return await this.shareEnvelope(\"MLShareProjects\", options, { writable: true }, (tx, meta) =>\n buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {\n mode: options.mode,\n ...meta,\n }),\n );\n }\n\n /**\n * Shares one stored template. The envelope carries the document itself, so there is no project\n * snapshot and no resource for the recipient to copy out — which is why the grant is read-only.\n *\n * Nothing about the document is checked: a stored template is shareable by virtue of existing.\n * An entry the recipient cannot resolve — a block installed from a folder on the sender's\n * machine, say — is theirs to see when they preview or apply it, where every unresolvable entry\n * is named anyway.\n *\n * @param id template to share\n * @param options recipients XOR everyone, plus the title recipients see\n */\n public async shareTemplate(id: TemplateId, options: ShareTemplateOptions): Promise<ShareOutcome> {\n const rid = await this.resolveTemplateId(id);\n const stored = await this.pl.withReadTx(\"MLReadStoredTemplate\", (tx) =>\n readStoredTemplate(tx, id, rid),\n );\n\n return await this.shareEnvelope(\"MLShareTemplate\", options, { writable: false }, (tx, meta) =>\n buildTemplateShareEnvelope(\n tx,\n this.sharingOutboxResourceId,\n {\n document: stored.data.document,\n label: stored.label,\n ...(stored.description === undefined ? {} : { description: stored.description }),\n source: id,\n },\n meta,\n ),\n );\n }\n\n /**\n * Shares one folder and everything under it: the subtree's folders, every project in them\n * snapshotted, and every template carried whole.\n *\n * The grant is writable, because a folder holding projects is copied out of the envelope the\n * way a project pack is. An everyone-share of a folder therefore hands every user on the server\n * write access to the envelope — the same trade a project share already makes.\n *\n * Folder ids do not travel. What the recipient gets is the shape of the subtree, rebuilt under\n * a folder of their own choosing with ids their own document mints.\n *\n * @param folder folder to share; everything beneath it goes with it\n * @param options recipients XOR everyone, plus the title recipients see\n */\n public async shareFolder(folder: FolderId, options: ShareFolderOptions): Promise<ShareOutcome> {\n const loaded = await this.loadFolderSubtree(folder, newEnvelopeFolderId);\n const subtree: EnvelopeFolderSubtree = {\n source: folder,\n folders: loaded.folders,\n projects: loaded.projects.map((project) => ({\n projectId: project.projectId,\n sourceRid: project.rid,\n folder: project.folder,\n })),\n templates: loaded.templates.map((template) => ({\n document: template.data.document,\n label: template.label,\n ...(template.description === undefined ? {} : { description: template.description }),\n folder: template.folder,\n })),\n };\n\n return await this.shareEnvelope(\"MLShareFolder\", options, { writable: true }, (tx, meta) =>\n buildFolderShareEnvelope(tx, this.sharingOutboxResourceId, subtree, meta),\n );\n }\n\n /**\n * The one transaction every share is made in: the shares named by `options.replace` are\n * dropped, the envelope `build` makes is created, and it is granted — all at once, so a failed\n * grant rolls the whole thing back and the outbox is left as it was.\n *\n * A share with named recipients grants each of them and expires after the default TTL\n * (`sharedAt + envelopeTtlMs`). A share with everyone is one make-public grant, and its\n * `expiresAt` is `null`, so it never expires.\n */\n private async shareEnvelope(\n txName: string,\n options: ShareOptions,\n permissions: { writable: boolean },\n build: (\n tx: PlTransaction,\n meta: { sender: string; title: string; expiresAt: number | null },\n ) =>\n | { envelope: ResourceRef; data: EnvelopeData }\n | Promise<{ envelope: ResourceRef; data: EnvelopeData }>,\n ): Promise<ShareOutcome> {\n const everyone = \"everyone\" in options;\n const meta = {\n sender: this.currentUserLogin ?? \"\",\n title: options.title,\n expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,\n };\n\n const outcome = await this.pl.withWriteTx(txName, async (tx) => {\n await this.dropShares(tx, options.replace);\n const { envelope, data } = await build(tx, meta);\n await this.grantShareEnvelope(\n tx,\n envelope,\n everyone,\n everyone ? [] : options.recipients,\n permissions,\n );\n await tx.commit();\n return { shareId: data.shareId };\n });\n\n await this.sharingOutboxTree.refreshState();\n return outcome;\n }\n\n /**\n * Detaches the named shares from the donor's outbox inside the caller's transaction, so a\n * replacement and the shares it supersedes land together or not at all.\n *\n * A share that no longer resolves is skipped rather than reported: the caller names shares the\n * author saw a moment ago, and one revoked meanwhile is already in the wanted state.\n */\n private async dropShares(tx: PlTransaction, shareIds: ShareId[] | undefined): Promise<void> {\n for (const shareId of shareIds ?? []) {\n const target = await this.resolveOutboxEnvelope(tx, shareId);\n if (target === undefined) continue;\n tx.removeField(field(this.sharingOutboxResourceId, target.fieldName));\n }\n }\n\n /**\n * Grants one freshly built envelope inside the transaction that created it: a single make-public\n * grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the\n * target to the everyone-user, gated by role + permission ceiling), or one grant per named\n * recipient.\n *\n * `writable` is not a preference. A project pack needs a writable grant because a copy out of\n * it takes the snapshots out of the envelope, and the cross-color attach rule permits that only\n * to a writable grant holder. A template share copies nothing — the document sits in the\n * envelope's own immutable data — so it is granted read-only, and must be: a writable\n * everyone-grant would hand every user on the server write access to the envelope.\n */\n private async grantShareEnvelope(\n tx: PlTransaction,\n envelope: ResourceRef,\n everyone: boolean,\n recipients: string[],\n permissions: { writable: boolean },\n ): Promise<void> {\n const gid = await envelope.globalId;\n if (everyone) tx.grantAccess(gid, \"\", permissions, GrantType.ANY_AUTHORISED);\n else for (const r of recipients) tx.grantAccess(gid, r, permissions);\n }\n\n /**\n * Revokes and deletes an outgoing share for all recipients: detaches and deletes the envelope, and\n * its grants are revoked along with it. Copies recipients already took out of it are unaffected\n * (ref-counting keeps the resources they point at alive). Idempotent — revoking a share that is\n * already gone is a no-op.\n */\n public async revokeShare(shareId: ShareId): Promise<void> {\n await this.pl.withWriteTx(\"MLRevokeShare\", async (tx) => {\n const target = await this.resolveOutboxEnvelope(tx, shareId);\n if (target === undefined) return;\n tx.removeField(field(this.sharingOutboxResourceId, target.fieldName));\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n }\n\n /**\n * Resolves a live envelope from the donor's own outbox by its logical `shareId`, returning the\n * outbox field name (for detach), the signed envelope id, and its decoded {@link EnvelopeData}.\n * The outbox is keyed by `{shareId}` directly, but a replaced/legacy share may have drifted, so\n * we match on the decoded `shareId` rather than the field name alone.\n */\n private async resolveOutboxEnvelope(\n tx: PlTransaction,\n shareId: ShareId,\n ): Promise<{ fieldName: string; rid: SignedResourceId; data: EnvelopeData } | undefined> {\n const outboxData = await tx.getResourceData(this.sharingOutboxResourceId, true);\n for (const f of outboxData.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const rd = await tx.getResourceData(f.value, false);\n if (rd.data === undefined) continue;\n const data = decodeEnvelopeData(rd.data);\n if (data === undefined) continue;\n if (data.shareId === shareId) return { fieldName: f.name, rid: f.value, data };\n }\n return undefined;\n }\n\n /**\n * Resolves currently-shared envelopes (granted to this user) to their resource ids, keyed by\n * the envelope's logical `shareId`.\n *\n * Reads the {@link liveEnvelopes} Computable — the same shared-resource discovery tree that\n * feeds {@link availableShares}. This is the single discovery mechanism: there is no separate\n * `ListUserResources` re-stream on every copy. `refreshState()` is awaited first so a\n * just-granted envelope is observed (the tree's discovery poll may otherwise lag a freshly\n * landed grant). The tree is gRPC-only, so this is empty on a REST-connected client.\n */\n private async resolveLiveEnvelopes(): Promise<Map<ShareId, LiveEnvelope>> {\n await this.availableSharesTree.refreshState();\n const live = (await this.liveEnvelopes.getValue()) ?? [];\n // Dedup by logical shareId (last writer wins — at most one live envelope per shareId).\n const map = new Map<ShareId, LiveEnvelope>();\n for (const e of live) map.set(e.data.shareId, e);\n return map;\n }\n\n /**\n * Copies what one or more shares carry into this user's own tree, optionally into a folder.\n *\n * A share is a shelf, not an invitation: copying takes nothing off it and records no decision,\n * so the same share can be copied from again, by this user or anyone else it was granted to.\n * What a copy produces depends on the payload — a pack of projects lands in the project list,\n * a template among their templates, building nothing until the recipient applies it, and a\n * folder is rebuilt whole with everything it held.\n *\n * Names are chosen against the destination folder, since that is where the uniqueness rule\n * applies, and a project and a template follow the same rule. A destination deleted meanwhile\n * fails the copy rather than spilling it at the top level.\n * Per-share failures (a revoked envelope, say) are collected rather than short-circuited, so\n * one dead share does not cost the others.\n */\n public async copyShare(\n shareIds: ShareId[],\n destination?: FolderId,\n ): Promise<{\n projects: ProjectId[];\n templates: TemplateId[];\n failed: { shareId: ShareId; error: string }[];\n }> {\n const live = await this.resolveLiveEnvelopes();\n\n const projects: ProjectId[] = [];\n const templates: TemplateId[] = [];\n const failed: { shareId: ShareId; error: string }[] = [];\n\n for (const shareId of shareIds) {\n const envelope = live.get(shareId);\n if (envelope === undefined) {\n failed.push({ shareId, error: \"Share is no longer available.\" });\n continue;\n }\n try {\n const payload = envelope.data.payload;\n\n if (payload.kind === \"template\") {\n const rid = await this.pl.withWriteTx(\"MLCopyTemplateShare\", async (tx) => {\n const tree = await openFoldersTx(tx, this.foldersRids);\n // The template lands in their templates, keeping who sent it as its provenance, under\n // a name that is free where it lands — the same rule a copied project follows. What\n // the donor said about the template is part of the template, not of the share.\n const tpl = createTemplate(\n tx,\n this.templateListResourceId,\n {\n label: foldersUniqueName(payload.label, tree.namesTakenIn(destination, \"template\")),\n description: payload.description,\n },\n { schemaVersion: 1, document: payload.document, sender: payload.from },\n );\n\n const created = await tpl.globalId;\n tree.place(\n [{ kind: \"template\", id: asTemplateId(resourceIdToString(created)) }],\n destination,\n );\n\n await tx.commit();\n return created;\n });\n\n const templateId = asTemplateId(resourceIdToString(rid));\n this.templateIdCache.set(templateId, rid);\n templates.push(templateId);\n continue;\n }\n\n if (payload.kind === \"folder\") {\n const root = envelopeFolderRoot(payload.folders);\n if (root === undefined)\n throw new Error(\"This share does not describe one folder, so nothing can be rebuilt.\");\n\n const copied = await this.pl.withWriteTx(\"MLCopyFolderShare\", async (tx) => {\n const tree = await openFoldersTx(tx, this.foldersRids);\n // The subtree is rebuilt whole, so names are only ever compared inside it — except\n // the root, which lands beside whatever the destination already holds.\n const created = await copyEnvelopeProjectsIntoList(\n tx,\n envelope.rid,\n this.projectListResourceId,\n );\n\n const createdTemplates: TemplateId[] = [];\n const items: (FoldersLeafItem & { folder: string })[] = [];\n for (const { uuid, rid } of created) {\n const inFolder = payload.projects[uuid];\n items.push({\n kind: \"project\",\n id: asProjectId(resourceIdToString(rid)),\n // A project whose payload entry is missing still exists; it lands at the root.\n folder: inFolder?.folder ?? root,\n });\n }\n\n for (const carried of payload.templates) {\n // The template lands in their templates, keeping who sent it as its provenance.\n const tpl = createTemplate(\n tx,\n this.templateListResourceId,\n { label: carried.label, description: carried.description },\n { schemaVersion: 1, document: carried.document, sender: payload.from },\n );\n const id = asTemplateId(resourceIdToString(await tpl.globalId));\n createdTemplates.push(id);\n items.push({ kind: \"template\", id, folder: carried.folder });\n }\n\n // Folders this build cannot rewrite leave the subtree unbuilt, and the copies land at\n // the top level. That is deliberate: folders are an arrangement, not the content, and\n // a copy the user asked for is not held back for their sake.\n tree.graft({ root, folders: payload.folders, items }, destination);\n\n await tx.commit();\n return { projects: created, templates: createdTemplates };\n });\n\n for (const { rid } of copied.projects) {\n const projectId = asProjectId(resourceIdToString(rid));\n this.projectIdCache.set(projectId, rid);\n projects.push(projectId);\n }\n templates.push(...copied.templates);\n continue;\n }\n\n const createdRids = await this.pl.withWriteTx(\"MLCopyShare\", async (tx) => {\n const tree = await openFoldersTx(tx, this.foldersRids);\n // Scoped to where the copies are going, because that is the only place their names have\n // to be free. `taken` grows as the pack is copied, so two projects of one name inside a\n // single share do not land on top of each other either.\n const taken = [...tree.namesTakenIn(destination, \"project\")];\n const created = await copyEnvelopeProjectsIntoList(\n tx,\n envelope.rid,\n this.projectListResourceId,\n (sourceLabel) => {\n const name = foldersUniqueName(sourceLabel, taken);\n taken.push(name);\n return name;\n },\n );\n\n tree.place(\n created.map(({ rid }) => ({\n kind: \"project\" as const,\n id: asProjectId(resourceIdToString(rid)),\n })),\n destination,\n );\n\n await tx.commit();\n return created;\n });\n for (const { rid } of createdRids) {\n const projectId = asProjectId(resourceIdToString(rid));\n this.projectIdCache.set(projectId, rid);\n projects.push(projectId);\n }\n } catch (e) {\n failed.push({ shareId, error: e instanceof Error ? e.message : String(e) });\n }\n }\n\n await Promise.all([\n this.projectListTree.refreshState(),\n this.templateListTree.refreshState(),\n this.foldersTree.refreshState(),\n ]);\n return { projects, templates, failed };\n }\n\n /**\n * Puts a share out of this user's sight. Private to them and reversible with\n * {@link unhideShare}: nothing is deleted, the donor is not told, and what was already copied\n * out of it is unaffected.\n */\n public async hideShare(shareId: ShareId): Promise<void> {\n const now = Date.now();\n await this.pl.withWriteTx(\"MLHideShare\", async (tx) => {\n writeShareHidden(tx, this.sharingStateResourceId, shareId, now);\n await tx.commit();\n });\n\n await this.sharingStateTree.refreshState();\n }\n\n /** Brings a hidden share back into this user's list. */\n public async unhideShare(shareId: ShareId): Promise<void> {\n await this.pl.withWriteTx(\"MLUnhideShare\", async (tx) => {\n clearShareHidden(tx, this.sharingStateResourceId, shareId);\n await tx.commit();\n });\n\n await this.sharingStateTree.refreshState();\n }\n\n //\n // Outbox cleanup (donor side)\n //\n\n private static readonly EnvelopeCleanupIntervalMs = 6 * 3600 * 1000; // every 6h\n private envelopeCleanupTimer: ReturnType<typeof setInterval> | undefined;\n\n /** On ML start and every 6h, delete envelopes whose immutable `expiresAt` has passed. */\n private startEnvelopeCleanup(): void {\n void this.runEnvelopeCleanup();\n this.envelopeCleanupTimer = setInterval(() => {\n void this.runEnvelopeCleanup();\n }, MiddleLayer.EnvelopeCleanupIntervalMs);\n // Don't keep the process alive solely for cleanup.\n this.envelopeCleanupTimer.unref?.();\n }\n\n /** Scans the donor's outbox and deletes expired envelopes (backend auto-revokes their grants).\n * Envelopes with `expiresAt: null` (share-with-everybody) are skipped. */\n private async runEnvelopeCleanup(): Promise<void> {\n try {\n const now = Date.now();\n const expired = await this.pl.withReadTx(\"MLEnvelopeCleanupScan\", async (tx) => {\n const data = await tx.getResourceData(this.sharingOutboxResourceId, true);\n const toDelete: { fieldName: string }[] = [];\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n const rd = await tx.getResourceData(f.value, false);\n if (rd.data === undefined) continue;\n const envData = decodeEnvelopeData(rd.data);\n if (envData === undefined) continue;\n if (envData.expiresAt === null) continue; // never expires\n if (envData.expiresAt <= now) toDelete.push({ fieldName: f.name });\n }\n return toDelete;\n });\n\n if (expired.length === 0) return;\n\n await this.pl.withWriteTx(\"MLEnvelopeCleanup\", async (tx) => {\n for (const { fieldName } of expired)\n tx.removeField(field(this.sharingOutboxResourceId, fieldName));\n await tx.commit();\n });\n\n await this.sharingOutboxTree.refreshState();\n } catch (e) {\n this.env.logger.warn(\n `envelope cleanup failed: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n\n //\n // Projects\n //\n\n private readonly openedProjects = new Map<ProjectId, Project>();\n\n /** Snapshot writes started by {@link closeProject} and not yet finished. Held only so\n * {@link close} can give them a bounded chance to land. */\n private readonly pendingSnapshotWrites = new Set<Promise<void>>();\n\n private trackSnapshotWrite(write: Promise<void>): void {\n this.pendingSnapshotWrites.add(write);\n void write.finally(() => this.pendingSnapshotWrites.delete(write));\n }\n\n /** Waits for close-boundary snapshot writes that are already running, up to `timeoutMs`.\n *\n * This starts no work: quitting still performs no snapshot of its own. It only lets a write\n * that a project close already began finish, so closing a project and immediately quitting\n * does not routinely lose it. Bounded, because a wedged filesystem must not hang the quit,\n * and losing the write costs one cold open rather than any correctness. */\n private async drainSnapshotWrites(timeoutMs: number): Promise<void> {\n if (this.pendingSnapshotWrites.size === 0) return;\n\n let timer: NodeJS.Timeout | undefined;\n const expiry = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n timer.unref?.();\n });\n try {\n await Promise.race([Promise.allSettled(this.pendingSnapshotWrites), expiry]);\n } finally {\n if (timer !== undefined) clearTimeout(timer);\n }\n }\n\n /** Opens a project, and starts corresponding project maintenance loop. */\n public async openProject(id: ProjectId): Promise<void> {\n if (this.openedProjects.has(id)) throw new Error(`Project ${id} already opened`);\n const rid = await this.resolveProjectId(id);\n this.openedProjects.set(id, await Project.init(this.env, id, rid));\n this.openedProjectsList.setValue([...this.openedProjects.keys()]);\n }\n\n /** Closes the project, and deallocate all corresponding resources. */\n public async closeProject(id: ProjectId): Promise<void> {\n const prj = this.openedProjects.get(id);\n if (prj === undefined) throw new Error(`Project ${id} not found among opened projects`);\n this.openedProjects.delete(id);\n\n // Snapshot before destroy, and here rather than inside destroy(): destroy() is also what\n // application shutdown runs, and quitting should perform no snapshot work. Terminating the\n // tree invalidates it, so the state has to be taken first either way.\n //\n // Started, not awaited. The capture happens synchronously inside, which is the part that\n // needs the tree alive; the encode and write are up to ten megabytes of work that closing a\n // project should not sit behind. Kept so shutdown can drain it.\n this.trackSnapshotWrite(prj.snapshotOnClose());\n\n await prj.destroy();\n this.openedProjectsList.setValue([...this.openedProjects.keys()]);\n }\n\n /** Returns a project access object for an opened project. */\n public getOpenedProject(id: ProjectId): Project {\n const prj = this.openedProjects.get(id);\n if (prj === undefined) throw new Error(`Project ${id} not found among opened projects`);\n return prj;\n }\n\n /** Returns true if project with given id is currently opened. */\n public isProjectOpened(id: ProjectId): boolean {\n return this.openedProjects.has(id);\n }\n\n /** Counters for the persisted project tree mirrors, or undefined when they are switched off.\n * Reads and hits are what show whether a reopen was actually warm, and the miss breakdown\n * says why it was not. */\n public get treeSnapshotStats(): Readonly<TreeSnapshotStat> | undefined {\n return this.env.treeSnapshots?.getStats();\n }\n\n /**\n * Deallocates all runtime resources consumed by this object and awaits\n * actual termination of event loops and other processes associated with\n * them.\n */\n public async close() {\n if (this.envelopeCleanupTimer !== undefined) clearInterval(this.envelopeCleanupTimer);\n await Promise.all([...this.openedProjects.values()].map((prj) => prj.destroy()));\n // this.env.quickJs;\n await Promise.all([\n this.projectListTree.terminate(),\n this.templateListTree.terminate(),\n this.foldersTree.terminate(),\n this.sharingOutboxTree.terminate(),\n this.sharingStateTree.terminate(),\n this.availableSharesTree.terminate(),\n ]);\n await this.drainSnapshotWrites(SNAPSHOT_DRAIN_TIMEOUT_MS);\n await this.env.dispose();\n await this.pl.close();\n }\n\n /** @deprecated */\n public async closeAndAwaitTermination() {\n await this.close();\n }\n\n /** Generates sufficiently random string to be used as local secret for the\n * middle layer */\n public static generateLocalSecret(): string {\n return HmacSha256Signer.generateSecret();\n }\n\n /** Returns a block event dispatcher, which can be used to listen to block events. */\n public get blockEventDispatcher(): BlockEventDispatcher {\n return this.env.blockEventDispatcher;\n }\n\n /** Initialize middle layer */\n public static async init(\n pl: PlClient,\n workdir: string,\n _ops: MiddleLayerOpsConstructor,\n ): Promise<MiddleLayer> {\n const ops: MiddleLayerOps = {\n ...DefaultMiddleLayerOpsSettings,\n ...DefaultMiddleLayerOpsPaths(workdir),\n ..._ops,\n };\n\n // overriding debug options from environment variables\n ops.defaultTreeOptions.logStat = getDebugFlags().logTreeStats;\n ops.debugOps.dumpInitialTreeState = getDebugFlags().dumpInitialTreeState;\n // apply MI_TREE_TRAVERSAL only when the embedder hasn't set an explicit mode\n if (\n ops.defaultTreeOptions.traversalMode === undefined &&\n getDebugFlags().treeTraversalMode !== undefined\n )\n ops.defaultTreeOptions.traversalMode = getDebugFlags().treeTraversalMode;\n\n const { projects, templates, sharingOutbox, sharingState, folders } = await pl.withWriteTx(\n \"MLInitialization\",\n async (tx) => {\n // Lazily create each clientRoot-attached singleton resource. Returns the existing\n // resource id if the field is already populated, otherwise creates + locks + sets it.\n // A created resource's id is known only once the transaction commits.\n type Singleton = { existing: SignedResourceId } | { ref: ResourceRef };\n const lazyInit = async (\n fieldName: string,\n type: { name: string; version: string },\n ): Promise<Singleton> => {\n const f = field(tx.clientRoot, fieldName);\n tx.createField(f, \"Dynamic\");\n const fData = await tx.getField(f);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(type);\n tx.lock(ref);\n tx.setField(f, ref);\n return { ref };\n }\n return { existing: fData.value };\n };\n\n const projectsR = await lazyInit(ProjectsField, ProjectsResourceType);\n const templatesR = await lazyInit(TemplatesField, TemplatesResourceType);\n const outboxR = await lazyInit(SharingOutboxField, SharingOutboxResourceType);\n const stateR = await lazyInit(SharingStateField, SharingStateResourceType);\n // The folder tree gets its own root-attached singleton rather than a field on the\n // projects resource: an extra field there is walked by the released project-list reader\n // and dereferenced as a project, which takes the whole list down, not just the folders.\n const foldersR = await lazyInit(FoldersField, FoldersResourceType);\n\n await tx.commit();\n\n const idOf = async (r: Singleton) => (\"existing\" in r ? r.existing : await r.ref.globalId);\n return {\n projects: await idOf(projectsR),\n templates: await idOf(templatesR),\n sharingState: await idOf(stateR),\n sharingOutbox: await idOf(outboxR),\n folders: await idOf(foldersR),\n };\n },\n );\n\n const logger = ops.logger;\n\n const driverKit = await initDriverKit(pl, workdir, ops.frontendDownloadPath, ops);\n\n // passed to components having no own retry logic\n const retryHttpDispatcher = new RetryAgent(pl.httpDispatcher);\n\n const v2RegistryProvider = new V2RegistryProvider(retryHttpDispatcher);\n\n const bpPreparer = new BlockPackPreparer(\n v2RegistryProvider,\n driverKit.signer,\n retryHttpDispatcher,\n );\n\n const quickJs = await getQuickJS();\n\n const runtimeCapabilities = new RuntimeCapabilities();\n // add runtime capabilities of model here\n runtimeCapabilities.addSupportedRequirement(\"requiresModelAPIVersion\", 1);\n runtimeCapabilities.addSupportedRequirement(\"requiresModelAPIVersion\", 2);\n runtimeCapabilities.addSupportedRequirement(\"requiresCreatePTable\", 2);\n runtimeCapabilities.addSupportedRequirement(\"requiresPFramesVersion\", REQUIRES_PFRAMES_VERSION);\n registerServiceCapabilities((flag, value) =>\n runtimeCapabilities.addSupportedRequirement(flag, value),\n );\n // runtime capabilities of the desktop are to be added by the desktop app / test framework\n\n const serviceRegistry = createModelServiceRegistry({ logger });\n\n const treeSnapshots = TreeSnapshotStore.create(pl, {\n dir: ops.treeSnapshotPath,\n maxSizeBytes: ops.treeSnapshotOps.maxSizeBytes,\n enabled: ops.treeSnapshotOps.enabled,\n logger,\n });\n if (ops.treeSnapshotOps.enabled) {\n // Housekeeping before any project opens: drop snapshots from other builds, backends and\n // users, then trim to the ceiling.\n await treeSnapshots?.evict();\n } else {\n // Switched off, so reclaim what earlier sessions left on disk. The reason to reach for\n // this switch is usually the disk itself, and leaving the files behind would answer the\n // wrong half of that complaint. Keyed on the setting, not on the store being absent: it\n // is also absent for an impersonated client, whose session must not delete anything.\n await TreeSnapshotStore.purge(ops.treeSnapshotPath, logger);\n }\n\n const env: MiddleLayerEnvironment = {\n pl,\n blockEventDispatcher: new BlockEventDispatcher(),\n signer: driverKit.signer,\n logger,\n httpDispatcher: pl.httpDispatcher,\n retryHttpDispatcher,\n ops,\n bpPreparer,\n frontendDownloadDriver: driverKit.frontendDriver,\n driverKit,\n blockUpdateWatcher: new BlockUpdateWatcher(v2RegistryProvider, logger, {\n minDelay: ops.devBlockUpdateRecheckInterval,\n http: retryHttpDispatcher,\n preferredUpdateChannel: ops.preferredUpdateChannel,\n }),\n runtimeCapabilities,\n serviceRegistry,\n quickJs,\n projectHelper: new ProjectHelper(quickJs, logger),\n treeSnapshots,\n dispose: async () => {\n await serviceRegistry.dispose();\n await retryHttpDispatcher.destroy();\n await driverKit.dispose();\n },\n };\n\n const openedProjects = new WatchableValue<ProjectId[]>([]);\n const projectListTC = await createProjectList(pl, projects, openedProjects, env);\n const templateListTC = await createTemplateList(pl, templates, env);\n const foldersTC = await createFolderList(\n pl,\n folders,\n projectListTC.tree,\n templateListTC.tree,\n openedProjects,\n env,\n );\n\n // Project sharing trees and reactive views.\n const outgoingTC = await createOutgoingShares(pl, sharingOutbox, env);\n const sharingStateTree = await createSharingStateTree(pl, sharingState, env);\n const availableSharesTree = await createAvailableSharesTree(pl, env);\n const availableShares = createAvailableSharesComputable(\n availableSharesTree,\n sharingStateTree,\n pl.userResources.authUser,\n );\n const liveEnvelopes = createLiveEnvelopesComputable(availableSharesTree);\n\n return new MiddleLayer(\n env,\n driverKit,\n driverKit.signer,\n projects,\n templates,\n sharingOutbox,\n sharingState,\n folders,\n openedProjects,\n projectListTC.tree,\n templateListTC.tree,\n foldersTC.tree,\n outgoingTC.tree,\n sharingStateTree,\n availableSharesTree,\n v2RegistryProvider,\n projectListTC.computable,\n templateListTC.computable,\n foldersTC.computable,\n outgoingTC.computable,\n availableShares,\n liveEnvelopes,\n );\n }\n}\n\n//\n// Internals\n//\n\n/** A folder subtree read for copying; see {@link MiddleLayer.loadFolderSubtree}. */\ninterface FolderSubtree<Id extends string> {\n /** Folder holding the subtree's root; absent when the root is at the top level. */\n readonly parent?: FolderId;\n /** Local key of the subtree's root. */\n readonly root: Id;\n readonly folders: Record<Id, { name: string; parent?: Id; description?: string }>;\n readonly projects: { projectId: ProjectId; rid: SignedResourceId; folder: Id }[];\n readonly templates: (StoredTemplate & { folder: Id })[];\n}\n\n/** Everything a stored template is: its immutable blob, plus the label and the description the\n * list shows, both of which live beside the blob rather than in it. */\ninterface StoredTemplate {\n readonly data: StoredTemplateData;\n readonly label: string;\n /** Absent when nobody described the template. */\n readonly description?: string;\n}\n\n/** Reads one stored template within the caller's transaction. A share carries its document on;\n * a duplicate carries the blob whole, so the copy says of itself what the original did. */\nasync function readStoredTemplate(\n tx: PlTransaction,\n id: TemplateId,\n rid: SignedResourceId,\n): Promise<StoredTemplate> {\n const rd = await tx.getResourceData(rid, false);\n if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);\n const [label, description] = await Promise.all([\n tx.getKValueJson<string>(rid, TemplateLabelKey),\n tx.getKValueJsonIfExists<string>(rid, TemplateDescriptionKey).then(normalizeDescription),\n ]);\n return {\n data: decodeStoredTemplateData(rd.data),\n label,\n ...(description === undefined ? {} : { description }),\n };\n}\n\n/** The label of every project in a project list, read within the caller's transaction. */\nasync function existingProjectLabels(\n tx: PlTransaction,\n listRid: SignedResourceId,\n): Promise<string[]> {\n const listed = await listedById(tx, listRid);\n const metas = await Promise.all(\n [...listed.values()].map(({ rid }) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)),\n );\n return metas.map((meta) => meta.label);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_project_model = require("../model/project_model.cjs");
|
|
2
|
-
let
|
|
2
|
+
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
3
3
|
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
4
4
|
let _milaboratories_pl_tree = require("@milaboratories/pl-tree");
|
|
5
5
|
let _milaboratories_computable = require("@milaboratories/computable");
|
|
@@ -43,34 +43,51 @@ async function createProjectList(pl, rid, openedProjects, env) {
|
|
|
43
43
|
const node = ctx.accessor(tree.entry()).node();
|
|
44
44
|
const oProjects = openedProjects.getValue(ctx);
|
|
45
45
|
if (node === void 0) return void 0;
|
|
46
|
-
|
|
47
|
-
for (const field of node.listDynamicFields()) {
|
|
48
|
-
const prj = node.traverse(field);
|
|
49
|
-
if (prj === void 0) continue;
|
|
50
|
-
const meta = (0, _milaboratories_ts_helpers.notEmpty)(prj.getKeyValueAsJson(require_project_model.ProjectMetaKey));
|
|
51
|
-
const created = (0, _milaboratories_ts_helpers.notEmpty)(prj.getKeyValueAsJson(require_project_model.ProjectCreatedTimestamp));
|
|
52
|
-
const lastModified = (0, _milaboratories_ts_helpers.notEmpty)(prj.getKeyValueAsJson(require_project_model.ProjectLastModifiedTimestamp));
|
|
53
|
-
const projectId = (0, _milaboratories_pl_client.resourceIdToString)(prj.id);
|
|
54
|
-
result.push({
|
|
55
|
-
id: projectId,
|
|
56
|
-
created: new Date(created),
|
|
57
|
-
lastModified: new Date(lastModified),
|
|
58
|
-
opened: oProjects.indexOf(projectId) >= 0,
|
|
59
|
-
meta
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
result.sort((p) => -p.lastModified.valueOf());
|
|
63
|
-
return result;
|
|
46
|
+
return projectListEntries(node, oProjects);
|
|
64
47
|
}).withStableType(),
|
|
65
48
|
tree
|
|
66
49
|
};
|
|
67
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds the project list out of the projects-list resource, most recently modified first.
|
|
53
|
+
*
|
|
54
|
+
* Skips, rather than throws on, anything that is not a fully synced project. The projects
|
|
55
|
+
* resource carries the user's entire project list, so one unexpected sibling field — or one
|
|
56
|
+
* project whose metadata has not arrived yet — must cost that entry and nothing else. Both the
|
|
57
|
+
* traversal and the metadata reads are watched, so an entry appears as soon as it syncs.
|
|
58
|
+
*
|
|
59
|
+
* The resource type is matched by name only: the point is to exclude foreign resources, not to
|
|
60
|
+
* hide a project stored under an earlier resource-type version.
|
|
61
|
+
*/
|
|
62
|
+
function projectListEntries(node, openedProjects) {
|
|
63
|
+
const result = [];
|
|
64
|
+
for (const field of node.listDynamicFields()) {
|
|
65
|
+
const prj = node.traverse(field);
|
|
66
|
+
if (prj === void 0) continue;
|
|
67
|
+
if (prj.resourceType.name !== require_project_model.ProjectResourceType.name) continue;
|
|
68
|
+
const meta = prj.getKeyValueAsJson(require_project_model.ProjectMetaKey);
|
|
69
|
+
const created = prj.getKeyValueAsJson(require_project_model.ProjectCreatedTimestamp);
|
|
70
|
+
const lastModified = prj.getKeyValueAsJson(require_project_model.ProjectLastModifiedTimestamp);
|
|
71
|
+
if (meta === void 0 || created === void 0 || lastModified === void 0) continue;
|
|
72
|
+
const projectId = (0, _milaboratories_pl_model_common.asProjectId)((0, _milaboratories_pl_client.resourceIdToString)(prj.id));
|
|
73
|
+
result.push({
|
|
74
|
+
id: projectId,
|
|
75
|
+
created: new Date(created),
|
|
76
|
+
lastModified: new Date(lastModified),
|
|
77
|
+
opened: openedProjects.indexOf(projectId) >= 0,
|
|
78
|
+
meta
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
result.sort((a, b) => b.lastModified.valueOf() - a.lastModified.valueOf());
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
68
84
|
//#endregion
|
|
69
85
|
exports.ProjectsField = ProjectsField;
|
|
70
86
|
exports.ProjectsListTreePruningFunction = ProjectsListTreePruningFunction;
|
|
71
87
|
exports.ProjectsResourceType = ProjectsResourceType;
|
|
72
88
|
exports.createProjectList = createProjectList;
|
|
73
89
|
exports.ensureProjectListRid = ensureProjectListRid;
|
|
90
|
+
exports.projectListEntries = projectListEntries;
|
|
74
91
|
exports.projectsListFieldFilter = projectsListFieldFilter;
|
|
75
92
|
|
|
76
93
|
//# sourceMappingURL=project_list.cjs.map
|