@milaboratories/pl-middle-layer 1.73.2 → 1.73.4

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.
@@ -1,7 +1,7 @@
1
1
  //#region src/middle_layer/build_stamp.ts
2
2
  function injectedStamp() {
3
3
  try {
4
- return "05f292767595-dirty-1790851398074";
4
+ return "fb50bbc8d6d5-dirty-1790872243079";
5
5
  } catch {
6
6
  return;
7
7
  }
@@ -1,7 +1,7 @@
1
1
  //#region src/middle_layer/build_stamp.ts
2
2
  function injectedStamp() {
3
3
  try {
4
- return "05f292767595-dirty-1790851398052";
4
+ return "fb50bbc8d6d5-dirty-1790872243052";
5
5
  } catch {
6
6
  return;
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sharing_list.cjs","names":["Computable","SharedEnvelopeResourceType","normalizeEnvelopeData","SynchronizedTreeState","isHiddenField","hiddenFieldShareId","SharingOutboxResourceType","treeFilter","SharingStateResourceType","envelopeProjectMap","envelopeFolderRoot"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PlTreeNodeAccessor, PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId, TemplateId } from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n hiddenFieldShareId,\n isHiddenField,\n envelopeFolderRoot,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** What a folder share carries, as both share lists show it. */\nexport interface EnvelopeFolderSummary {\n label: string;\n /** Donor's own id of the shared folder. */\n source: FolderId;\n folderCount: number;\n projectCount: number;\n templateCount: number;\n}\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // identity of the share; a share that replaces it gets its own\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, one template document, or a folder subtree. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack. `projectId` is the donor's source project id, which is\n * what the share dialog matches a project's prior shares on; `updatedAt` is when this\n * project's snapshot was taken. Empty for any share that is not a pack of projects. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent otherwise. `source` is the donor's own\n * template id, which is what the share dialog matches a template's prior shares on; a share\n * made before it was carried has none and matches nothing. */\n template?: { label: string; blockCount: number; source?: TemplateId };\n /** The shared folder, for a folder share; absent otherwise. `label` is the root folder's own\n * name, `source` the donor's own id for it — what a prior share of the same folder is matched\n * on — and the counts are of the whole subtree, the root folder itself excluded. */\n folder?: EnvelopeFolderSummary;\n /** What the shared thing says about itself: a template's, a folder's, or the one project's of\n * a single-project share. Absent for a pack of several, which has no one thing to describe. */\n description?: string;\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n}\n\n/**\n * Recipient-facing view of one share that is open to this user.\n *\n * A share is a shelf, not an invitation: it stays listed for as long as the envelope lives, and\n * the recipient may copy from it any number of times. {@link hidden} is that recipient's own\n * private choice to stop seeing it, and it can be undone.\n */\nexport interface AvailableShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode;\n /** What the share carries — projects to copy, a template to add to the recipient's templates,\n * or a folder subtree to rebuild in the recipient's tree. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n /** What the shared thing says about itself. See {@link OutgoingShare.description}. */\n description?: string;\n /** The shared folder, for a folder share; absent otherwise. See {@link OutgoingShare.folder}. */\n folder?: EnvelopeFolderSummary;\n /** Whether this recipient has put it out of sight. Hidden shares are listed, not dropped: the\n * view that shows them is what \"show hidden\" turns on. */\n hidden: boolean;\n}\n\n/** A live envelope discovered in the recipient's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. A copy out of a share finds it by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via\n * `ListGrants` on the envelope (`[\"*\"]` for an everyone-share). `ListGrants` is gated backend-side\n * to the envelope owner (the donor), which is exactly who reads this view.\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n ...(data.payload.source === undefined ? {} : { source: data.payload.source }),\n },\n }\n : {}),\n ...(folder === undefined ? {} : { folder }),\n ...(description === undefined ? {} : { description }),\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\n/**\n * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and\n * {@link createLiveEnvelopesComputable} both read it.\n */\nexport async function createAvailableSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: AvailableSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/**\n * Builds a Computable yielding the recipient's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there\n * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write\n * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is\n * expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));\n}\n\n/**\n * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking\n * each entry with whether this user has hidden it (a hiddenField in their own SharingState).\n * Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope here. A donor already owns what\n * they shared, so their own envelopes are dropped from this view.\n */\nexport function createAvailableSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<AvailableShare[] | undefined> {\n return Computable.make((ctx) => {\n // Shares this user has put out of sight, keyed on the share. A share replaced by a newer one\n // is a new share, so its replacement arrives unhidden.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const hidden = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (isHiddenField(f)) hidden.add(hiddenFieldShareId(f));\n\n const result: AvailableShare[] = [];\n for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n ...(description === undefined ? {} : { description }),\n ...(folder === undefined ? {} : { folder }),\n hidden: hidden.has(data.shareId),\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\n/** Creates the recipient's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n//\n// Internals\n//\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst AvailableSharesFieldFilter: Filter = treeFilter.resourceTypeEq(\n SharedEnvelopeResourceType.name,\n);\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Every envelope among the discovery tree's roots that this build can read, decoded. A root that\n * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build\n * does not know, is left out rather than offered: there is nothing useful to do with a share that\n * cannot be read.\n */\nfunction decodedEnvelopes(roots: readonly (PlTreeNodeAccessor | undefined)[]): LiveEnvelope[] {\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n}\n\n/** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload\n * that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/**\n * What a folder share is, in the words both lists show it in: the shared folder's name and how\n * much is inside. `undefined` for any other payload.\n *\n * The root folder is excluded from `folderCount` — it is the thing being described, not something\n * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is\n * described as nothing rather than guessed at.\n */\nfunction envelopeFolderSummary(data: EnvelopeData): EnvelopeFolderSummary | undefined {\n if (data.payload.kind !== \"folder\") return undefined;\n const { folders, projects, templates } = data.payload;\n const root = envelopeFolderRoot(folders);\n if (root === undefined) return undefined;\n return {\n label: folders[root].name,\n source: data.payload.source,\n folderCount: Object.keys(folders).length - 1,\n projectCount: Object.keys(projects).length,\n templateCount: templates.length,\n };\n}\n\n/**\n * What the shared thing says about itself: the template's description, the root folder's, or the\n * one project's of a single-project share. `undefined` when it has none, for a pack of several\n * projects, which describes no single thing, and for a folder envelope with no single root.\n */\nfunction envelopeDescription(data: EnvelopeData): string | undefined {\n const payload = data.payload;\n switch (payload.kind) {\n case \"template\":\n return payload.description;\n case \"folder\": {\n const root = envelopeFolderRoot(payload.folders);\n return root === undefined ? undefined : payload.folders[root].description;\n }\n case \"projects\": {\n const projects = Object.values(payload.projects);\n return projects.length === 1 ? projects[0]?.description : undefined;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sharing_list.cjs","names":["Computable","SharedEnvelopeResourceType","normalizeEnvelopeData","SynchronizedTreeState","isHiddenField","hiddenFieldShareId","SharingOutboxResourceType","treeFilter","SharingStateResourceType","envelopeProjectMap","envelopeFolderRoot"],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PlTreeNodeAccessor, PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId, TemplateId } from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n hiddenFieldShareId,\n isHiddenField,\n envelopeFolderRoot,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** What a folder share carries, as both share lists show it. */\nexport interface EnvelopeFolderSummary {\n label: string;\n /** Donor's own id of the shared folder. */\n source: FolderId;\n folderCount: number;\n projectCount: number;\n templateCount: number;\n}\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // identity of the share; a share that replaces it gets its own\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, one template document, or a folder subtree. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack. `projectId` is the donor's source project id, which is\n * what the share dialog matches a project's prior shares on; `updatedAt` is when this\n * project's snapshot was taken. Empty for any share that is not a pack of projects. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent otherwise. `source` is the donor's own\n * template id, which is what the share dialog matches a template's prior shares on; a share\n * made before it was carried has none and matches nothing. */\n template?: { label: string; blockCount: number; source?: TemplateId };\n /** The shared folder, for a folder share; absent otherwise. `label` is the root folder's own\n * name, `source` the donor's own id for it — what a prior share of the same folder is matched\n * on — and the counts are of the whole subtree, the root folder itself excluded. */\n folder?: EnvelopeFolderSummary;\n /** What the shared thing says about itself: a template's, a folder's, or the one project's of\n * a single-project share. Absent for a pack of several, which has no one thing to describe. */\n description?: string;\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n}\n\n/**\n * Recipient-facing view of one share that is open to this user.\n *\n * A share is a shelf, not an invitation: it stays listed for as long as the envelope lives, and\n * the recipient may copy from it any number of times. {@link hidden} is that recipient's own\n * private choice to stop seeing it, and it can be undone.\n */\nexport interface AvailableShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode;\n /** What the share carries — projects to copy, a template to add to the recipient's templates,\n * or a folder subtree to rebuild in the recipient's tree. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n /** What the shared thing says about itself. See {@link OutgoingShare.description}. */\n description?: string;\n /** The shared folder, for a folder share; absent otherwise. See {@link OutgoingShare.folder}. */\n folder?: EnvelopeFolderSummary;\n /** Whether this recipient has put it out of sight. Hidden shares are listed, not dropped: the\n * view that shows them is what \"show hidden\" turns on. */\n hidden: boolean;\n}\n\n/** A live envelope discovered in the recipient's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. A copy out of a share finds it by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via\n * `ListGrants` on the envelope (`[\"*\"]` for an everyone-share). `ListGrants` is gated backend-side\n * to the envelope owner (the donor), which is exactly who reads this view.\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n ...(data.payload.source === undefined ? {} : { source: data.payload.source }),\n },\n }\n : {}),\n ...(folder === undefined ? {} : { folder }),\n ...(description === undefined ? {} : { description }),\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\n/**\n * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and\n * {@link createLiveEnvelopesComputable} both read it.\n */\nexport async function createAvailableSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: AvailableSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/**\n * Builds a Computable yielding the recipient's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there\n * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write\n * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is\n * expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));\n}\n\n/**\n * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking\n * each entry with whether this user has hidden it (a hiddenField in their own SharingState).\n * Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope here. A donor already owns what\n * they shared, so their own envelopes are dropped from this view.\n */\nexport function createAvailableSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<AvailableShare[] | undefined> {\n return Computable.make((ctx) => {\n // Shares this user has put out of sight, keyed on the share. A share replaced by a newer one\n // is a new share, so its replacement arrives unhidden.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const hidden = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (isHiddenField(f)) hidden.add(hiddenFieldShareId(f));\n\n const result: AvailableShare[] = [];\n for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n ...(description === undefined ? {} : { description }),\n ...(folder === undefined ? {} : { folder }),\n hidden: hidden.has(data.shareId),\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\n/** Creates the recipient's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n//\n// Internals\n//\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), which the tree would\n// then hold, with its fields pruned to [], and re-poll for nothing. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst AvailableSharesFieldFilter: Filter = treeFilter.resourceTypeEq(\n SharedEnvelopeResourceType.name,\n);\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Every envelope among the discovery tree's roots that this build can read, decoded. A root that\n * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build\n * does not know, is left out rather than offered: there is nothing useful to do with a share that\n * cannot be read.\n */\nfunction decodedEnvelopes(roots: readonly (PlTreeNodeAccessor | undefined)[]): LiveEnvelope[] {\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n}\n\n/** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload\n * that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/**\n * What a folder share is, in the words both lists show it in: the shared folder's name and how\n * much is inside. `undefined` for any other payload.\n *\n * The root folder is excluded from `folderCount` — it is the thing being described, not something\n * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is\n * described as nothing rather than guessed at.\n */\nfunction envelopeFolderSummary(data: EnvelopeData): EnvelopeFolderSummary | undefined {\n if (data.payload.kind !== \"folder\") return undefined;\n const { folders, projects, templates } = data.payload;\n const root = envelopeFolderRoot(folders);\n if (root === undefined) return undefined;\n return {\n label: folders[root].name,\n source: data.payload.source,\n folderCount: Object.keys(folders).length - 1,\n projectCount: Object.keys(projects).length,\n templateCount: templates.length,\n };\n}\n\n/**\n * What the shared thing says about itself: the template's description, the root folder's, or the\n * one project's of a single-project share. `undefined` when it has none, for a pack of several\n * projects, which describes no single thing, and for a folder envelope with no single root.\n */\nfunction envelopeDescription(data: EnvelopeData): string | undefined {\n const payload = data.payload;\n switch (payload.kind) {\n case \"template\":\n return payload.description;\n case \"folder\": {\n const root = envelopeFolderRoot(payload.folders);\n return root === undefined ? undefined : payload.folders[root].description;\n }\n case \"projects\": {\n const projects = Object.values(payload.projects);\n return projects.length === 1 ? projects[0]?.description : undefined;\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sharing_list.js","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PlTreeNodeAccessor, PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId, TemplateId } from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n hiddenFieldShareId,\n isHiddenField,\n envelopeFolderRoot,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** What a folder share carries, as both share lists show it. */\nexport interface EnvelopeFolderSummary {\n label: string;\n /** Donor's own id of the shared folder. */\n source: FolderId;\n folderCount: number;\n projectCount: number;\n templateCount: number;\n}\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // identity of the share; a share that replaces it gets its own\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, one template document, or a folder subtree. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack. `projectId` is the donor's source project id, which is\n * what the share dialog matches a project's prior shares on; `updatedAt` is when this\n * project's snapshot was taken. Empty for any share that is not a pack of projects. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent otherwise. `source` is the donor's own\n * template id, which is what the share dialog matches a template's prior shares on; a share\n * made before it was carried has none and matches nothing. */\n template?: { label: string; blockCount: number; source?: TemplateId };\n /** The shared folder, for a folder share; absent otherwise. `label` is the root folder's own\n * name, `source` the donor's own id for it — what a prior share of the same folder is matched\n * on — and the counts are of the whole subtree, the root folder itself excluded. */\n folder?: EnvelopeFolderSummary;\n /** What the shared thing says about itself: a template's, a folder's, or the one project's of\n * a single-project share. Absent for a pack of several, which has no one thing to describe. */\n description?: string;\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n}\n\n/**\n * Recipient-facing view of one share that is open to this user.\n *\n * A share is a shelf, not an invitation: it stays listed for as long as the envelope lives, and\n * the recipient may copy from it any number of times. {@link hidden} is that recipient's own\n * private choice to stop seeing it, and it can be undone.\n */\nexport interface AvailableShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode;\n /** What the share carries — projects to copy, a template to add to the recipient's templates,\n * or a folder subtree to rebuild in the recipient's tree. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n /** What the shared thing says about itself. See {@link OutgoingShare.description}. */\n description?: string;\n /** The shared folder, for a folder share; absent otherwise. See {@link OutgoingShare.folder}. */\n folder?: EnvelopeFolderSummary;\n /** Whether this recipient has put it out of sight. Hidden shares are listed, not dropped: the\n * view that shows them is what \"show hidden\" turns on. */\n hidden: boolean;\n}\n\n/** A live envelope discovered in the recipient's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. A copy out of a share finds it by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via\n * `ListGrants` on the envelope (`[\"*\"]` for an everyone-share). `ListGrants` is gated backend-side\n * to the envelope owner (the donor), which is exactly who reads this view.\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n ...(data.payload.source === undefined ? {} : { source: data.payload.source }),\n },\n }\n : {}),\n ...(folder === undefined ? {} : { folder }),\n ...(description === undefined ? {} : { description }),\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\n/**\n * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and\n * {@link createLiveEnvelopesComputable} both read it.\n */\nexport async function createAvailableSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: AvailableSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/**\n * Builds a Computable yielding the recipient's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there\n * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write\n * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is\n * expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));\n}\n\n/**\n * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking\n * each entry with whether this user has hidden it (a hiddenField in their own SharingState).\n * Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope here. A donor already owns what\n * they shared, so their own envelopes are dropped from this view.\n */\nexport function createAvailableSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<AvailableShare[] | undefined> {\n return Computable.make((ctx) => {\n // Shares this user has put out of sight, keyed on the share. A share replaced by a newer one\n // is a new share, so its replacement arrives unhidden.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const hidden = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (isHiddenField(f)) hidden.add(hiddenFieldShareId(f));\n\n const result: AvailableShare[] = [];\n for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n ...(description === undefined ? {} : { description }),\n ...(folder === undefined ? {} : { folder }),\n hidden: hidden.has(data.shareId),\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\n/** Creates the recipient's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n//\n// Internals\n//\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven\n// finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst AvailableSharesFieldFilter: Filter = treeFilter.resourceTypeEq(\n SharedEnvelopeResourceType.name,\n);\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Every envelope among the discovery tree's roots that this build can read, decoded. A root that\n * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build\n * does not know, is left out rather than offered: there is nothing useful to do with a share that\n * cannot be read.\n */\nfunction decodedEnvelopes(roots: readonly (PlTreeNodeAccessor | undefined)[]): LiveEnvelope[] {\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n}\n\n/** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload\n * that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/**\n * What a folder share is, in the words both lists show it in: the shared folder's name and how\n * much is inside. `undefined` for any other payload.\n *\n * The root folder is excluded from `folderCount` — it is the thing being described, not something\n * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is\n * described as nothing rather than guessed at.\n */\nfunction envelopeFolderSummary(data: EnvelopeData): EnvelopeFolderSummary | undefined {\n if (data.payload.kind !== \"folder\") return undefined;\n const { folders, projects, templates } = data.payload;\n const root = envelopeFolderRoot(folders);\n if (root === undefined) return undefined;\n return {\n label: folders[root].name,\n source: data.payload.source,\n folderCount: Object.keys(folders).length - 1,\n projectCount: Object.keys(projects).length,\n templateCount: templates.length,\n };\n}\n\n/**\n * What the shared thing says about itself: the template's description, the root folder's, or the\n * one project's of a single-project share. `undefined` when it has none, for a pack of several\n * projects, which describes no single thing, and for a folder envelope with no single root.\n */\nfunction envelopeDescription(data: EnvelopeData): string | undefined {\n const payload = data.payload;\n switch (payload.kind) {\n case \"template\":\n return payload.description;\n case \"folder\": {\n const root = envelopeFolderRoot(payload.folders);\n return root === undefined ? undefined : payload.folders[root].description;\n }\n case \"projects\": {\n const projects = Object.values(payload.projects);\n return projects.length === 1 ? projects[0]?.description : undefined;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sharing_list.js","names":[],"sources":["../../src/middle_layer/sharing_list.ts"],"sourcesContent":["import type { PlTreeNodeAccessor, PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type { Filter, PlClient, SignedResourceId } from \"@milaboratories/pl-client\";\nimport {\n isEveryoneUserLogin,\n resourceType,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport type { ProjectId, TemplateId } from \"@milaboratories/pl-model-common\";\nimport type { FolderId } from \"@milaboratories/pl-model-middle-layer\";\nimport type {\n EnvelopeData,\n EnvelopeMode,\n EnvelopePayloadKind,\n ShareId,\n} from \"../model/sharing_model\";\nimport {\n hiddenFieldShareId,\n isHiddenField,\n envelopeFolderRoot,\n envelopeProjectMap,\n normalizeEnvelopeData,\n SharedEnvelopeResourceType,\n SharingOutboxResourceType,\n SharingStateResourceType,\n} from \"../model/sharing_model\";\n\n/** What a folder share carries, as both share lists show it. */\nexport interface EnvelopeFolderSummary {\n label: string;\n /** Donor's own id of the shared folder. */\n source: FolderId;\n folderCount: number;\n projectCount: number;\n templateCount: number;\n}\n\n/** Donor-facing view of one outgoing share. */\nexport interface OutgoingShare {\n shareId: ShareId; // identity of the share; a share that replaces it gets its own\n sharedAt: number; // this instance's creation time (ms epoch)\n expiresAt?: number; // EnvelopeData.expiresAt; null maps to undefined = never expires\n mode: EnvelopeMode;\n title: string; // display name shown to recipients; defaults to the first project's name\n /** What the share carries — a pack of projects, one template document, or a folder subtree. */\n payloadKind: EnvelopePayloadKind;\n /** One entry per project in the pack. `projectId` is the donor's source project id, which is\n * what the share dialog matches a project's prior shares on; `updatedAt` is when this\n * project's snapshot was taken. Empty for any share that is not a pack of projects. */\n projects: { projectId: ProjectId; label: string; updatedAt: number }[];\n /** The shared template, for a template share; absent otherwise. `source` is the donor's own\n * template id, which is what the share dialog matches a template's prior shares on; a share\n * made before it was carried has none and matches nothing. */\n template?: { label: string; blockCount: number; source?: TemplateId };\n /** The shared folder, for a folder share; absent otherwise. `label` is the root folder's own\n * name, `source` the donor's own id for it — what a prior share of the same folder is matched\n * on — and the counts are of the whole subtree, the root folder itself excluded. */\n folder?: EnvelopeFolderSummary;\n /** What the shared thing says about itself: a template's, a folder's, or the one project's of\n * a single-project share. Absent for a pack of several, which has no one thing to describe. */\n description?: string;\n /** Full recipient logins, from `ListGrants` on the envelope; `[\"*\"]` for everyone-shares. */\n recipients: string[];\n}\n\n/**\n * Recipient-facing view of one share that is open to this user.\n *\n * A share is a shelf, not an invitation: it stays listed for as long as the envelope lives, and\n * the recipient may copy from it any number of times. {@link hidden} is that recipient's own\n * private choice to stop seeing it, and it can be undone.\n */\nexport interface AvailableShare {\n shareId: ShareId;\n sender: string; // EnvelopeData.sender, display only\n title: string; // display name shown to recipients; defaults to the first project's name\n mode: EnvelopeMode;\n /** What the share carries — projects to copy, a template to add to the recipient's templates,\n * or a folder subtree to rebuild in the recipient's tree. */\n payloadKind: EnvelopePayloadKind;\n grantedAt: number;\n /** What the shared thing says about itself. See {@link OutgoingShare.description}. */\n description?: string;\n /** The shared folder, for a folder share; absent otherwise. See {@link OutgoingShare.folder}. */\n folder?: EnvelopeFolderSummary;\n /** Whether this recipient has put it out of sight. Hidden shares are listed, not dropped: the\n * view that shows them is what \"show hidden\" turns on. */\n hidden: boolean;\n}\n\n/** A live envelope discovered in the recipient's shared-resource tree: its signed resource id and\n * decoded {@link EnvelopeData}. A copy out of a share finds it by `data.shareId`. */\nexport type LiveEnvelope = { rid: SignedResourceId; data: EnvelopeData };\n\n/**\n * Reactive view of the donor's own outbox. Reads each live envelope's immutable\n * {@link EnvelopeData} from the tree, then enriches each share's full recipient list via\n * `ListGrants` on the envelope (`[\"*\"]` for an everyone-share). `ListGrants` is gated backend-side\n * to the envelope owner (the donor), which is exactly who reads this view.\n */\nexport function createOutgoingSharesComputable(\n pl: PlClient,\n tree: SynchronizedTreeState,\n): Computable<OutgoingShare[] | undefined> {\n return Computable.make(\n (ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n\n const drafts: OutgoingShareDraft[] = [];\n // Outbox fields are keyed by shareId; each value is a SharedEnvelope.\n for (const fieldName of node.listDynamicFields()) {\n const envelope = node.traverse(fieldName);\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue; // unknown version or payload kind — not ours to show\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n drafts.push({\n shareId: data.shareId,\n sharedAt: data.sharedAt,\n ...(data.expiresAt !== null ? { expiresAt: data.expiresAt } : {}),\n mode: data.mode,\n title: data.title,\n payloadKind: data.payload.kind,\n projects: envelopeProjects(data),\n ...(data.payload.kind === \"template\"\n ? {\n template: {\n label: data.payload.label,\n blockCount: data.payload.document.blocks.length,\n ...(data.payload.source === undefined ? {} : { source: data.payload.source }),\n },\n }\n : {}),\n ...(folder === undefined ? {} : { folder }),\n ...(description === undefined ? {} : { description }),\n envelopeRid: envelope.id,\n });\n }\n drafts.sort((a, b) => b.sharedAt - a.sharedAt);\n return drafts;\n },\n {\n // Resolve each share's recipients via listGrants. An everyone-grant maps to \"*\"; the donor's\n // own grant is dropped. One tx per envelope so a just-revoked rid faults only its own read,\n // which allSettled degrades to []. Recipients aren't cached — a live share's recipient set can\n // change. Transactional listGrants when available, else the standalone gRPC-only RPC.\n postprocessValue: async (\n drafts: OutgoingShareDraft[] | undefined,\n ): Promise<OutgoingShare[] | undefined> => {\n if (drafts === undefined) return undefined;\n const self = pl.userResources.authUser;\n const toRecipients = (grants: { user: string }[]): string[] =>\n grants.some((g) => isEveryoneUserLogin(g.user))\n ? [\"*\"]\n : grants.map((g) => g.user).filter((u) => u !== self);\n\n const txGrants = pl.hasCapability(\"txListGrants:v1\");\n const settled = await Promise.allSettled(\n drafts.map((d) =>\n txGrants\n ? pl.withReadTx(\"ListShareGrant\", (tx) => tx.listGrants(d.envelopeRid))\n : pl.userResources.listGrants(d.envelopeRid),\n ),\n );\n\n return drafts.map(({ envelopeRid: _envelopeRid, ...share }, i): OutgoingShare => {\n const r = settled[i];\n return { ...share, recipients: r.status === \"fulfilled\" ? toRecipients(r.value) : [] };\n });\n },\n },\n );\n}\n\n/**\n * Creates the donor's outbox synchronized tree (single explicit root) plus the\n * {@link OutgoingShare} computable over it.\n */\nexport async function createOutgoingShares(\n pl: PlClient,\n outboxRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<{ tree: SynchronizedTreeState; computable: Computable<OutgoingShare[] | undefined> }> {\n const tree = await SynchronizedTreeState.init(\n pl,\n outboxRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingOutboxPruningFunction,\n fieldFilter: SharingOutboxFieldFilter,\n },\n env.logger,\n );\n return { computable: createOutgoingSharesComputable(pl, tree), tree };\n}\n\n/**\n * Creates the recipient's shared-resource discovery tree: a `{kind:'shared'}` seed over\n * {@link SharedEnvelopeResourceType}. {@link createAvailableSharesComputable} and\n * {@link createLiveEnvelopesComputable} both read it.\n */\nexport async function createAvailableSharesTree(\n pl: PlClient,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n {\n kind: \"shared\",\n resourceType: resourceType(\n SharedEnvelopeResourceType.name,\n SharedEnvelopeResourceType.version,\n ),\n },\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharedEnvelopePruningFunction,\n fieldFilter: AvailableSharesFieldFilter,\n },\n env.logger,\n );\n}\n\n/**\n * Builds a Computable yielding the recipient's currently-live envelopes, read from the\n * shared-resource discovery tree the ML already maintains — the single discovery mechanism.\n * A copy out of a share `.getValue()`s this instead of re-streaming `ListUserResources`, so there\n * is no second discovery path. The envelope's signed `rid` (from the tree node) is what the write\n * tx needs; `copyEnvelopeProjectsIntoList` reads the project snapshots itself inside the tx.\n *\n * Yields a flat list; the caller dedups by `shareId` — at most one live envelope per shareId is\n * expected (the donor keeps one), and a replace tears the old one down.\n */\nexport function createLiveEnvelopesComputable(\n sharedTree: SynchronizedTreeState,\n): Computable<LiveEnvelope[] | undefined> {\n return Computable.make((ctx) => decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes()));\n}\n\n/**\n * Builds the {@link AvailableShare} computable over the shared-resource discovery tree, marking\n * each entry with whether this user has hidden it (a hiddenField in their own SharingState).\n * Both trees feed one Computable so it recomputes when either changes.\n *\n * `currentUserLogin` (when known) suppresses the user's own shares: a share-with-everybody grants\n * the everyone-user, so the donor discovers their own envelope here. A donor already owns what\n * they shared, so their own envelopes are dropped from this view.\n */\nexport function createAvailableSharesComputable(\n sharedTree: SynchronizedTreeState,\n sharingStateTree: SynchronizedTreeState,\n currentUserLogin: string | null,\n): Computable<AvailableShare[] | undefined> {\n return Computable.make((ctx) => {\n // Shares this user has put out of sight, keyed on the share. A share replaced by a newer one\n // is a new share, so its replacement arrives unhidden.\n const stateNode = ctx.accessor(sharingStateTree.entry()).node();\n const hidden = new Set<ShareId>();\n if (stateNode !== undefined)\n for (const f of stateNode.listDynamicFields())\n if (isHiddenField(f)) hidden.add(hiddenFieldShareId(f));\n\n const result: AvailableShare[] = [];\n for (const { data } of decodedEnvelopes(ctx.accessor(sharedTree.rootsEntry()).nodes())) {\n if (currentUserLogin !== null && data.sender === currentUserLogin) continue; // own share\n if (data.expiresAt !== null && data.expiresAt <= Date.now()) continue;\n\n const folder = envelopeFolderSummary(data);\n const description = envelopeDescription(data);\n result.push({\n shareId: data.shareId,\n sender: data.sender,\n title: data.title,\n mode: data.mode,\n payloadKind: data.payload.kind,\n grantedAt: data.sharedAt,\n ...(description === undefined ? {} : { description }),\n ...(folder === undefined ? {} : { folder }),\n hidden: hidden.has(data.shareId),\n });\n }\n result.sort((a, b) => b.grantedAt - a.grantedAt);\n return result;\n });\n}\n\n/** Creates the recipient's SharingState synchronized tree (single explicit root). */\nexport async function createSharingStateTree(\n pl: PlClient,\n stateRid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<SynchronizedTreeState> {\n return await SynchronizedTreeState.init(\n pl,\n stateRid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: SharingStatePruningFunction,\n fieldFilter: SharingStateFieldFilter,\n },\n env.logger,\n );\n}\n\n//\n// Internals\n//\n\nconst SharingOutboxPruningFunction: PruningFunction = (resource) => {\n if (\n !resourceTypesEqual(resource.type, SharingOutboxResourceType) &&\n !resourceTypesEqual(resource.type, SharedEnvelopeResourceType)\n )\n return [];\n return resource.fields;\n};\n\n// Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does\n// NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's\n// project/{uuid} snapshots into the whole project graph (StreamManager etc.), which the tree would\n// then hold, with its fields pruned to [], and re-poll for nothing. Following fields only FROM the outbox\n// and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.\nconst SharingOutboxFieldFilter: Filter = treeFilter.or(\n treeFilter.resourceTypeEq(SharingOutboxResourceType.name),\n treeFilter.resourceTypeEq(SharedEnvelopeResourceType.name),\n);\n\nconst SharedEnvelopePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharedEnvelopeResourceType)) return [];\n return resource.fields;\n};\n\n// Discovery only needs each envelope's immutable EnvelopeData (basic resource data); it must NOT\n// descend into the project snapshots. Following fields only FROM the envelope stops the walk at\n// the UserProject snapshots (their fields are never followed).\nconst AvailableSharesFieldFilter: Filter = treeFilter.resourceTypeEq(\n SharedEnvelopeResourceType.name,\n);\n\nconst SharingStatePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, SharingStateResourceType)) return [];\n return resource.fields;\n};\n\n// decision/{shareId} values are leaf JSON resources; follow fields only from SharingState.\nconst SharingStateFieldFilter: Filter = treeFilter.resourceTypeEq(SharingStateResourceType.name);\n\n/** Intermediate of an outgoing share before its full recipient list is fetched: everything the\n * tree carries synchronously, plus the envelope's signed id for the async `ListGrants` enrich. */\ntype OutgoingShareDraft = Omit<OutgoingShare, \"recipients\"> & { envelopeRid: SignedResourceId };\n\n/**\n * Every envelope among the discovery tree's roots that this build can read, decoded. A root that\n * is not a {@link SharedEnvelopeResourceType}, or whose schemaVersion or payload kind this build\n * does not know, is left out rather than offered: there is nothing useful to do with a share that\n * cannot be read.\n */\nfunction decodedEnvelopes(roots: readonly (PlTreeNodeAccessor | undefined)[]): LiveEnvelope[] {\n const result: LiveEnvelope[] = [];\n for (const envelope of roots) {\n if (envelope === undefined) continue;\n if (!resourceTypesEqual(envelope.resourceType, SharedEnvelopeResourceType)) continue;\n const data = normalizeEnvelopeData(envelope.getDataAsJson<unknown>());\n if (data === undefined) continue;\n result.push({ rid: envelope.id, data });\n }\n return result;\n}\n\n/** Per-project view for the donor, from the envelope's `projects` payload; empty for a payload\n * that carries no project. */\nfunction envelopeProjects(data: EnvelopeData): OutgoingShare[\"projects\"] {\n return Object.values(envelopeProjectMap(data)).map((p) => ({\n projectId: p.source,\n label: p.label,\n updatedAt: p.updatedAt,\n }));\n}\n\n/**\n * What a folder share is, in the words both lists show it in: the shared folder's name and how\n * much is inside. `undefined` for any other payload.\n *\n * The root folder is excluded from `folderCount` — it is the thing being described, not something\n * inside it. A folder envelope with no single root is one nothing can be rebuilt from, and it is\n * described as nothing rather than guessed at.\n */\nfunction envelopeFolderSummary(data: EnvelopeData): EnvelopeFolderSummary | undefined {\n if (data.payload.kind !== \"folder\") return undefined;\n const { folders, projects, templates } = data.payload;\n const root = envelopeFolderRoot(folders);\n if (root === undefined) return undefined;\n return {\n label: folders[root].name,\n source: data.payload.source,\n folderCount: Object.keys(folders).length - 1,\n projectCount: Object.keys(projects).length,\n templateCount: templates.length,\n };\n}\n\n/**\n * What the shared thing says about itself: the template's description, the root folder's, or the\n * one project's of a single-project share. `undefined` when it has none, for a pack of several\n * projects, which describes no single thing, and for a folder envelope with no single root.\n */\nfunction envelopeDescription(data: EnvelopeData): string | undefined {\n const payload = data.payload;\n switch (payload.kind) {\n case \"template\":\n return payload.description;\n case \"folder\": {\n const root = envelopeFolderRoot(payload.folders);\n return root === undefined ? undefined : payload.folders[root].description;\n }\n case \"projects\": {\n const projects = Object.values(payload.projects);\n return projects.length === 1 ? projects[0]?.description : undefined;\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-middle-layer",
3
- "version": "1.73.2",
3
+ "version": "1.73.4",
4
4
  "description": "Pl Middle Layer",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -33,22 +33,22 @@
33
33
  "@milaboratories/columns-collection-driver": "0.2.7",
34
34
  "@milaboratories/computable": "2.9.8",
35
35
  "@milaboratories/pf-driver": "1.9.6",
36
+ "@milaboratories/pl-client": "3.17.7",
36
37
  "@milaboratories/pf-spec-driver": "1.5.6",
37
38
  "@milaboratories/helpers": "1.14.5",
38
- "@milaboratories/pl-crash-recorder": "0.3.3",
39
- "@milaboratories/pl-client": "3.17.5",
40
39
  "@milaboratories/pl-deployments": "3.0.19",
41
- "@milaboratories/pl-drivers": "1.16.28",
42
- "@milaboratories/pl-errors": "1.4.46",
40
+ "@milaboratories/pl-crash-recorder": "0.3.3",
41
+ "@milaboratories/pl-drivers": "1.16.30",
42
+ "@milaboratories/pl-errors": "1.4.48",
43
+ "@milaboratories/pl-model-backend": "1.4.33",
43
44
  "@milaboratories/pl-http": "1.2.4",
44
- "@milaboratories/pl-model-backend": "1.4.31",
45
45
  "@milaboratories/pl-model-common": "1.51.0",
46
46
  "@milaboratories/pl-model-middle-layer": "1.34.0",
47
- "@milaboratories/pl-tree": "1.15.6",
48
47
  "@milaboratories/resolve-helper": "1.1.3",
48
+ "@milaboratories/pl-tree": "1.16.0",
49
49
  "@milaboratories/ts-helpers": "1.8.6",
50
+ "@platforma-sdk/block-tools": "2.16.8",
50
51
  "@platforma-sdk/model": "1.84.3",
51
- "@platforma-sdk/block-tools": "2.16.6",
52
52
  "@platforma-sdk/workflow-tengo": "6.12.1"
53
53
  },
54
54
  "devDependencies": {
@@ -325,8 +325,8 @@ const SharingOutboxPruningFunction: PruningFunction = (resource) => {
325
325
 
326
326
  // Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does
327
327
  // NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's
328
- // project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven
329
- // finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox
328
+ // project/{uuid} snapshots into the whole project graph (StreamManager etc.), which the tree would
329
+ // then hold, with its fields pruned to [], and re-poll for nothing. Following fields only FROM the outbox
330
330
  // and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.
331
331
  const SharingOutboxFieldFilter: Filter = treeFilter.or(
332
332
  treeFilter.resourceTypeEq(SharingOutboxResourceType.name),