@aglyn/tenant-runtime 1.0.0-beta.219 → 1.0.0-beta.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/tenant-runtime",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.220",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.220",
|
|
29
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.220",
|
|
30
30
|
"@swc/helpers": "0.5.23"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { assignmentRuleMatches, consentGroupForHost, contactFacetPath, CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH, personKey, readContactFacet, readCrmAssignmentSettings, roundRobinOrder } from "@aglyn/aglyn/server";
|
|
18
|
-
import { firebaseAdmin, getOrgForHost, notifyUsers, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { firebaseAdmin, getHostDocAdmin, getOrgForHost, hostDisplayName, notifyUsers, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { FieldValue } from "firebase-admin/firestore";
|
|
20
20
|
/** The words the run history and the log give each refusal. */ export const OWNER_ASSIGNMENT_REFUSALS = {
|
|
21
21
|
'no-org': 'this site has no organization',
|
|
@@ -61,12 +61,27 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
61
61
|
var _input_actorUid;
|
|
62
62
|
if (!input.ownerUid || input.ownerUid === ((_input_actorUid = input.actorUid) != null ? _input_actorUid : '')) return false;
|
|
63
63
|
const lead = input.record.kind === 'lead';
|
|
64
|
+
/*
|
|
65
|
+
* THE BODY SAYS WHAT HAPPENED, AND ON WHICH SITE (AGL-3432).
|
|
66
|
+
*
|
|
67
|
+
* It was the bare name, so a member who owns records on several sites read
|
|
68
|
+
* "Ada" in an email body and could not tell what Ada was or where. The
|
|
69
|
+
* site's name is one request-cached read of a document most callers have
|
|
70
|
+
* already paid for, taken only when somebody is actually notified; a read
|
|
71
|
+
* that fails drops the "on {site}" clause rather than the notification.
|
|
72
|
+
*/ let site = '';
|
|
73
|
+
try {
|
|
74
|
+
const host = await getHostDocAdmin(input.hostId);
|
|
75
|
+
if (host) site = ` on ${hostDisplayName(host, input.hostId)}`;
|
|
76
|
+
} catch (unused) {
|
|
77
|
+
// The clause goes, never the notification.
|
|
78
|
+
}
|
|
64
79
|
await notifyUsers([
|
|
65
80
|
input.ownerUid
|
|
66
81
|
], {
|
|
67
82
|
type: lead ? 'content.leadAssigned' : 'content.contactAssigned',
|
|
68
83
|
title: lead ? 'Lead assigned to you' : 'Contact assigned to you',
|
|
69
|
-
body: input.who
|
|
84
|
+
body: `${input.who} was assigned to you as a ${lead ? 'lead' : 'contact'}${site}.`,
|
|
70
85
|
link: recordLink(input.hostId, input.record),
|
|
71
86
|
orgId: input.orgId,
|
|
72
87
|
hostId: input.hostId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/assign-contact-owner.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n assignmentRuleMatches,\n consentGroupForHost,\n contactFacetPath,\n type ContactSource,\n CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH,\n type CrmAssignmentTarget,\n personKey,\n readContactFacet,\n readCrmAssignmentSettings,\n roundRobinOrder,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getOrgForHost,\n notifyUsers,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * WHO A NEW RECORD BELONGS TO — the one place an owner is decided on the\n * server (AGL-2618).\n *\n * Two entry points over one transaction. `assignOwnerForCapture` is the\n * capture door's pass: it runs the org's assignment rules in order, falls\n * back to the capturing site's default owner, and touches only a record\n * that has NO owner — a door that named one (the console's drawer, an\n * import column, a conversion with a picked owner) has expressed a person's\n * choice. `reassignContactOwner` is the automation's: an author who put\n * \"assign an owner\" on a stage change means to reassign, and the step hands\n * this either a member or the rotation.\n *\n * ## One transaction, because the pointer is shared\n *\n * The round-robin pool's pointer lives on the org document, and two\n * captures landing together must take two different members. Firestore\n * transactions serialize on what they read, so the org document, the\n * contact and the roster documents are all read inside the transaction and\n * the pointer is advanced in the same commit that writes the owner: a\n * concurrent capture that read the same pointer retries and reads the moved\n * one. The alternative — read the pointer, decide, then write — is exactly\n * the read-then-write shape that lets two captures pick the same person.\n *\n * ## The roster is checked, once per member per attempt\n *\n * A rule may name a member who has since left, and a pool may hold one. A\n * rule naming nobody on the roster is passed over rather than stopping the\n * pass — the next rule may well assign — and a pool member who is gone is\n * skipped for the next in the order. `orgs/{orgId}/members/{uid}` is keyed\n * by uid, so the check is one document read per candidate, and only the\n * roster is consulted: a project-level Auth lookup would admit people who\n * are not on this organization at all (AGL-1122).\n *\n * ## The lead follows the contact\n *\n * The capture doors that file a lead (`addHostLead`: a form routed to leads,\n * a sign-up, a booking) key it by `personKey(email)`, so the lead's document\n * is addressable from the capture alone. When it exists it is given the same\n * owner in the same transaction; two doors write the lead AFTER the contact\n * they `void`ed, so for those sources the mirror is retried a few times over\n * the next seconds, detached, and the person's Leads list shows the owner\n * as soon as the lead is there.\n *\n * ## The owner is told, unless they chose themselves\n *\n * One console notification per assignment, for the record the recipient\n * will open: the lead when the site holds one for this person, the contact\n * otherwise. Not when the actor IS the new owner — a rep who converts a\n * lead into their own contact has not been handed anything.\n *\n * ## Never rejects\n *\n * The capture has already succeeded and a form submission must not fail\n * because the CRM could not decide who follows up; a failure is logged and\n * answered as `none`, and the record is exactly as assignable by hand as it\n * was before this existed. The automation executor turns a `none` into the\n * step's recorded error.\n */\n\nexport interface OwnerAssignmentInput {\n hostId: string\n /** `orgs/{orgId}/contacts/{contactId}` — the record the owner is written on. */\n contactId: string\n /** The contact's address, which keys the lead and names the person in the notification. */\n email: string\n /**\n * The signed-in person making the assignment, when there is one. Never\n * notified about their own choice; absent for a capture and an automation.\n */\n actorUid?: string | null\n}\n\nexport interface CaptureAssignmentInput extends OwnerAssignmentInput {\n source: ContactSource\n /** The form the capture came through, for a rule conditioned on it. */\n formId?: string | null\n /** The tags the capture carried; the contact's own are read from the facet. */\n tags?: readonly string[]\n}\n\nexport interface ReassignmentInput extends OwnerAssignmentInput {\n assign: CrmAssignmentTarget\n}\n\n/** Why an assignment assigned nobody — the automation reports these verbatim. */\nexport type OwnerAssignmentRefusal =\n | 'no-org'\n | 'no-contact'\n | 'no-rule'\n | 'empty-pool'\n | 'not-a-member'\n | 'failed'\n\n/** What decided an assignment: a rule, the site default, the step's member, or the rotation. */\nexport type OwnerAssignedBy = 'rule' | 'default' | 'member' | 'roundRobin'\n\nexport type OwnerAssignment =\n | {\n outcome: 'assigned'\n ownerUid: string\n by: OwnerAssignedBy\n /** The rule that decided it, when one did. */\n ruleId?: string\n /** The lead document existed and now carries the owner too. */\n leadMirrored: boolean\n /** A notification was fanned out to the new owner. */\n notified: boolean\n }\n /** The record already had this owner (a capture), or the same one was named again (a reassignment). */\n | { outcome: 'unchanged'; ownerUid: string }\n | { outcome: 'none'; reason: OwnerAssignmentRefusal }\n\n/** The words the run history and the log give each refusal. */\nexport const OWNER_ASSIGNMENT_REFUSALS: Record<OwnerAssignmentRefusal, string> = {\n 'no-org': 'this site has no organization',\n 'no-contact': 'the contact no longer exists',\n 'no-rule': 'no assignment rule matched and the site has no default owner',\n 'empty-pool': 'the round-robin pool has nobody on the roster in it',\n 'not-a-member': 'the owner named is not on the team',\n failed: 'the owner could not be assigned',\n}\n\n/**\n * The doors whose lead lands after the contact they `void`ed — the ones the\n * mirror is worth retrying for. An order, an import, a manual add and an\n * API call file no lead, and a retry for them would be reads for nothing.\n */\nconst LEAD_FILING_SOURCES: ReadonlySet<ContactSource> = new Set([\n 'form',\n 'member',\n 'booking',\n])\n\n/** How long the detached lead mirror waits between its looks, in order. */\nconst LEAD_MIRROR_RETRY_MS: readonly number[] = [1_000, 2_000, 3_000]\n\ntype Policy =\n | { kind: 'capture'; input: CaptureAssignmentInput }\n | { kind: 'reassign'; input: ReassignmentInput }\n\ninterface Decision {\n ownerUid: string\n by: OwnerAssignedBy\n ruleId?: string\n /** The pointer moves only when the rotation chose. */\n advancePointer: boolean\n}\n\n/**\n * The console path the owner's notification opens, in the `/{hostDocId}/rest`\n * shape every host notification uses — the console rewrites it to\n * `/{orgSlug}/hosts/{subdomain}/rest` when the link is followed, which is\n * why `hostId` and `orgId` travel on the document. The same spelling the\n * CRM's task notification uses; the hub's own route builder lives in the\n * plugin, which this layer cannot import.\n */\nfunction recordLink(hostId: string, record: AssignedRecord): string {\n const section = record.kind === 'contact' ? 'contacts' : 'leads'\n return `/${hostId}/crm/${section}/${encodeURIComponent(record.id)}`\n}\n\n/** The record an assignment notification opens. */\nexport type AssignedRecord =\n | { kind: 'contact'; id: string }\n | { kind: 'lead'; id: string }\n\n/**\n * Tells a member a record is now theirs — one console notification, typed\n * by the record it opens — unless the member handed it to themselves.\n * Shared by the pass above and by the routes where a person picks the\n * owner outright (a lead conversion naming a colleague), so every server\n * path that writes `ownerUid` says so in the same words. Never throws;\n * the fan-out swallows its own failures.\n */\nexport async function notifyRecordAssigned(input: {\n hostId: string\n orgId: string\n ownerUid: string\n actorUid?: string | null\n record: AssignedRecord\n /** How the person reads in the message: their name, else their address. */\n who: string\n}): Promise<boolean> {\n if (!input.ownerUid || input.ownerUid === (input.actorUid ?? '')) return false\n const lead = input.record.kind === 'lead'\n await notifyUsers([input.ownerUid], {\n type: lead ? 'content.leadAssigned' : 'content.contactAssigned',\n title: lead ? 'Lead assigned to you' : 'Contact assigned to you',\n body: input.who,\n link: recordLink(input.hostId, input.record),\n orgId: input.orgId,\n hostId: input.hostId,\n })\n return true\n}\n\nasync function assignOwner(policy: Policy): Promise<OwnerAssignment> {\n const { input } = policy\n try {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const { orgId } = resolved\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contactRef = orgRef.collection('contacts').doc(input.contactId)\n const membersRef = orgRef.collection('members')\n const leadKey = personKey(input.email)\n // Beside the contact on the org, keyed by the same address (AGL-3275).\n const leadRef = leadKey ? orgRef.collection('leads').doc(leadKey) : null\n\n let contactName = ''\n const verdict = await firestore.runTransaction(\n async (tx): Promise<OwnerAssignment> => {\n /*\n * ALL READS BEFORE THE WRITE, which Firestore requires. The org\n * document is read here rather than trusted from `getOrgForHost`,\n * because the pointer it carries is the one thing a concurrent\n * capture may have moved, and the rules and the pool are read from\n * the same snapshot so the decision and the pointer agree.\n */\n const [orgSnapshot, contactSnapshot, leadSnapshot] = await Promise.all([\n tx.get(orgRef),\n tx.get(contactRef),\n leadRef ? tx.get(leadRef) : Promise.resolve(null),\n ])\n if (!contactSnapshot.exists) {\n return { outcome: 'none', reason: 'no-contact' }\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const contact = (contactSnapshot.data() ?? {}) as Record<string, unknown>\n contactName = String(contact['name'] ?? '')\n const group = consentGroupForHost(org, input.hostId)\n const facet = readContactFacet(contact, group.groupId)\n const current = typeof facet.ownerUid === 'string' ? facet.ownerUid : ''\n if (policy.kind === 'capture' && current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n const settings = readCrmAssignmentSettings(org)\n\n // One read per candidate per attempt; a retried transaction reads\n // afresh, as it must.\n const onRoster = new Map<string, Promise<boolean>>()\n const isMember = (uid: string): Promise<boolean> => {\n let known = onRoster.get(uid)\n if (!known) {\n known = tx.get(membersRef.doc(uid)).then((snapshot) => snapshot.exists)\n onRoster.set(uid, known)\n }\n return known\n }\n const fromPool = async (): Promise<string | null> => {\n for (const uid of roundRobinOrder(\n settings.pool.memberUids,\n settings.pool.lastAssignedUid,\n )) {\n if (await isMember(uid)) return uid\n }\n return null\n }\n const resolveTarget = async (\n target: CrmAssignmentTarget,\n ): Promise<{ uid: string; advancePointer: boolean } | null> => {\n if ('roundRobin' in target) {\n const uid = await fromPool()\n return uid ? { uid, advancePointer: true } : null\n }\n return (await isMember(target.memberUid))\n ? { uid: target.memberUid, advancePointer: false }\n : null\n }\n\n let decision: Decision | null = null\n let refusal: OwnerAssignmentRefusal = 'no-rule'\n if (policy.kind === 'capture') {\n const capture = {\n source: policy.input.source,\n email: input.email,\n formId: policy.input.formId ?? null,\n tags: [...(policy.input.tags ?? []), ...(facet.tags ?? [])],\n }\n for (const rule of settings.rules) {\n if (!assignmentRuleMatches(rule.when, capture)) continue\n const hit = await resolveTarget(rule.assign)\n if (hit) {\n decision = { ...hit, ownerUid: hit.uid, by: 'rule', ruleId: rule.id }\n break\n }\n }\n const fallback = settings.hostDefaultOwners[input.hostId]\n if (!decision && fallback && (await isMember(fallback))) {\n decision = { ownerUid: fallback, by: 'default', advancePointer: false }\n }\n } else {\n const { assign } = policy.input\n const hit = await resolveTarget(assign)\n if (hit) {\n decision = {\n ...hit,\n ownerUid: hit.uid,\n by: 'roundRobin' in assign ? 'roundRobin' : 'member',\n }\n } else {\n refusal = 'roundRobin' in assign ? 'empty-pool' : 'not-a-member'\n }\n }\n if (!decision) return { outcome: 'none', reason: refusal }\n if (decision.ownerUid === current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n\n tx.update(contactRef, {\n [contactFacetPath(group.groupId, 'ownerUid')]: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (decision.advancePointer) {\n tx.update(orgRef, {\n [CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH]: decision.ownerUid,\n })\n }\n /*\n * A lead somebody already assigned by hand keeps its owner on a\n * capture — the contact was new, the lead may not have been — and\n * follows the contact on a deliberate reassignment.\n */\n const leadOwner = String(leadSnapshot?.get('ownerUid') ?? '')\n const leadMirrored = Boolean(\n leadSnapshot?.exists && (policy.kind === 'reassign' || !leadOwner),\n )\n if (leadMirrored && leadRef) {\n tx.update(leadRef, {\n ownerUid: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n return {\n outcome: 'assigned',\n ownerUid: decision.ownerUid,\n by: decision.by,\n ...(decision.ruleId ? { ruleId: decision.ruleId } : {}),\n leadMirrored,\n notified: false,\n }\n },\n )\n if (verdict.outcome !== 'assigned') return verdict\n // The owner is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n\n if (\n !verdict.leadMirrored &&\n leadRef &&\n policy.kind === 'capture' &&\n LEAD_FILING_SOURCES.has(policy.input.source)\n ) {\n void mirrorOwnerOntoLeadLater(leadRef, verdict.ownerUid)\n }\n\n const notified = await notifyRecordAssigned({\n hostId: input.hostId,\n orgId,\n ownerUid: verdict.ownerUid,\n actorUid: input.actorUid,\n record:\n verdict.leadMirrored && leadKey\n ? { kind: 'lead', id: leadKey }\n : { kind: 'contact', id: input.contactId },\n who: contactName || input.email,\n })\n return { ...verdict, notified }\n } catch (error) {\n console.error(\n 'assignContactOwner failed',\n input.hostId,\n input.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\n/**\n * The detached second look for a lead that lands after the contact.\n *\n * Bounded, and only ever ADDS an owner: a lead that arrived with one (a\n * person assigned it in the meantime) is left alone, and a lead that never\n * arrives costs three reads over six seconds and nothing else. Detached\n * because the capture must not hold `contactCreated` back for it — an\n * automation welcoming the contact has nothing to do with the lead's owner\n * column.\n */\nasync function mirrorOwnerOntoLeadLater(\n leadRef: FirebaseFirestore.DocumentReference,\n ownerUid: string,\n delaysMs: readonly number[] = LEAD_MIRROR_RETRY_MS,\n): Promise<boolean> {\n for (const delay of delaysMs) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n try {\n const lead = await leadRef.get()\n if (!lead.exists) continue\n if (String(lead.get('ownerUid') ?? '')) return false\n await leadRef.update({\n ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n console.error('assignContactOwner lead mirror failed', leadRef.path, error)\n return false\n }\n }\n return false\n}\n\n/**\n * The capture door's pass: the org's rules in order, then the capturing\n * site's default owner, on a record that has no owner yet. See the module\n * comment for the whole of it.\n */\nexport function assignOwnerForCapture(\n input: CaptureAssignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'capture', input })\n}\n\n/**\n * The deliberate assignment — the `assignContactOwner` step, and a person\n * handing a record to somebody: a member by uid (verified on the roster) or\n * the next in the round-robin pool, overwriting whoever held it. Naming the\n * owner the record already has changes nothing and tells nobody.\n */\nexport function reassignContactOwner(\n input: ReassignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'reassign', input })\n}\n\nexport default assignOwnerForCapture\n"],"names":["assignmentRuleMatches","consentGroupForHost","contactFacetPath","CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH","personKey","readContactFacet","readCrmAssignmentSettings","roundRobinOrder","firebaseAdmin","getOrgForHost","notifyUsers","restampCrmListFieldsAt","FieldValue","OWNER_ASSIGNMENT_REFUSALS","failed","LEAD_FILING_SOURCES","Set","LEAD_MIRROR_RETRY_MS","recordLink","hostId","record","section","kind","encodeURIComponent","id","notifyRecordAssigned","input","ownerUid","actorUid","lead","type","title","body","who","link","orgId","assignOwner","policy","resolved","outcome","reason","firestore","app","orgRef","collection","doc","contactRef","contactId","membersRef","leadKey","email","leadRef","contactName","verdict","runTransaction","tx","orgSnapshot","contactSnapshot","contact","leadSnapshot","Promise","all","get","resolve","exists","org","data","String","group","facet","groupId","current","settings","onRoster","Map","isMember","uid","known","then","snapshot","set","fromPool","pool","memberUids","lastAssignedUid","resolveTarget","target","advancePointer","memberUid","decision","refusal","capture","source","formId","tags","rule","rules","when","hit","assign","by","ruleId","fallback","hostDefaultOwners","update","updatedAt","serverTimestamp","leadOwner","leadMirrored","Boolean","notified","has","mirrorOwnerOntoLeadLater","error","console","delaysMs","delay","setTimeout","path","assignOwnerForCapture","reassignContactOwner"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,mBAAmB,EACnBC,gBAAgB,EAEhBC,kCAAkC,EAElCC,SAAS,EACTC,gBAAgB,EAChBC,yBAAyB,EACzBC,eAAe,QACV,sBAAqB;AAC5B,SACEC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,sBAAsB,QACjB,2BAA0B;AACjC,SAASC,UAAU,QAAQ,2BAA0B;AAmHrD,6DAA6D,GAC7D,OAAO,MAAMC,4BAAoE;IAC/E,UAAU;IACV,cAAc;IACd,WAAW;IACX,cAAc;IACd,gBAAgB;IAChBC,QAAQ;AACV,EAAC;AAED;;;;CAIC,GACD,MAAMC,sBAAkD,IAAIC,IAAI;IAC9D;IACA;IACA;CACD;AAED,yEAAyE,GACzE,MAAMC,uBAA0C;IAAC;IAAO;IAAO;CAAM;AAcrE;;;;;;;CAOC,GACD,SAASC,WAAWC,MAAc,EAAEC,MAAsB;IACxD,MAAMC,UAAUD,OAAOE,IAAI,KAAK,YAAY,aAAa;IACzD,OAAO,CAAC,CAAC,EAAEH,OAAO,KAAK,EAAEE,QAAQ,CAAC,EAAEE,mBAAmBH,OAAOI,EAAE,GAAG;AACrE;AAOA;;;;;;;CAOC,GACD,OAAO,eAAeC,qBAAqBC,KAQ1C;QAC4CA;IAA3C,IAAI,CAACA,MAAMC,QAAQ,IAAID,MAAMC,QAAQ,OAAMD,kBAAAA,MAAME,QAAQ,YAAdF,kBAAkB,KAAK,OAAO;IACzE,MAAMG,OAAOH,MAAMN,MAAM,CAACE,IAAI,KAAK;IACnC,MAAMZ,YAAY;QAACgB,MAAMC,QAAQ;KAAC,EAAE;QAClCG,MAAMD,OAAO,yBAAyB;QACtCE,OAAOF,OAAO,yBAAyB;QACvCG,MAAMN,MAAMO,GAAG;QACfC,MAAMhB,WAAWQ,MAAMP,MAAM,EAAEO,MAAMN,MAAM;QAC3Ce,OAAOT,MAAMS,KAAK;QAClBhB,QAAQO,MAAMP,MAAM;IACtB;IACA,OAAO;AACT;AAEA,eAAeiB,YAAYC,MAAc;IACvC,MAAM,EAAEX,KAAK,EAAE,GAAGW;IAClB,IAAI;QACF,MAAMC,WAAW,MAAM7B,cAAciB,MAAMP,MAAM;QACjD,IAAI,CAACmB,UAAU,OAAO;YAAEC,SAAS;YAAQC,QAAQ;QAAS;QAC1D,MAAM,EAAEL,KAAK,EAAE,GAAGG;QAClB,MAAMG,YAAYjC,cAAckC,GAAG,GAAGD,SAAS;QAC/C,MAAME,SAASF,UAAUG,UAAU,CAAC,QAAQC,GAAG,CAACV;QAChD,MAAMW,aAAaH,OAAOC,UAAU,CAAC,YAAYC,GAAG,CAACnB,MAAMqB,SAAS;QACpE,MAAMC,aAAaL,OAAOC,UAAU,CAAC;QACrC,MAAMK,UAAU7C,UAAUsB,MAAMwB,KAAK;QACrC,uEAAuE;QACvE,MAAMC,UAAUF,UAAUN,OAAOC,UAAU,CAAC,SAASC,GAAG,CAACI,WAAW;QAEpE,IAAIG,cAAc;QAClB,MAAMC,UAAU,MAAMZ,UAAUa,cAAc,CAC5C,OAAOC;gBAgBQC,mBACIC,uBACIC;YAjBrB;;;;;;SAMC,GACD,MAAM,CAACF,aAAaC,iBAAiBE,aAAa,GAAG,MAAMC,QAAQC,GAAG,CAAC;gBACrEN,GAAGO,GAAG,CAACnB;gBACPY,GAAGO,GAAG,CAAChB;gBACPK,UAAUI,GAAGO,GAAG,CAACX,WAAWS,QAAQG,OAAO,CAAC;aAC7C;YACD,IAAI,CAACN,gBAAgBO,MAAM,EAAE;gBAC3B,OAAO;oBAAEzB,SAAS;oBAAQC,QAAQ;gBAAa;YACjD;YACA,MAAMyB,OAAOT,oBAAAA,YAAYU,IAAI,cAAhBV,oBAAsB,CAAC;YACpC,MAAME,WAAWD,wBAAAA,gBAAgBS,IAAI,cAApBT,wBAA0B,CAAC;YAC5CL,cAAce,QAAOT,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB;YACxC,MAAMU,QAAQnE,oBAAoBgE,KAAKvC,MAAMP,MAAM;YACnD,MAAMkD,QAAQhE,iBAAiBqD,SAASU,MAAME,OAAO;YACrD,MAAMC,UAAU,OAAOF,MAAM1C,QAAQ,KAAK,WAAW0C,MAAM1C,QAAQ,GAAG;YACtE,IAAIU,OAAOf,IAAI,KAAK,aAAaiD,SAAS;gBACxC,OAAO;oBAAEhC,SAAS;oBAAaZ,UAAU4C;gBAAQ;YACnD;YACA,MAAMC,WAAWlE,0BAA0B2D;YAE3C,kEAAkE;YAClE,sBAAsB;YACtB,MAAMQ,WAAW,IAAIC;YACrB,MAAMC,WAAW,CAACC;gBAChB,IAAIC,QAAQJ,SAASX,GAAG,CAACc;gBACzB,IAAI,CAACC,OAAO;oBACVA,QAAQtB,GAAGO,GAAG,CAACd,WAAWH,GAAG,CAAC+B,MAAME,IAAI,CAAC,CAACC,WAAaA,SAASf,MAAM;oBACtES,SAASO,GAAG,CAACJ,KAAKC;gBACpB;gBACA,OAAOA;YACT;YACA,MAAMI,WAAW;gBACf,KAAK,MAAML,OAAOrE,gBAChBiE,SAASU,IAAI,CAACC,UAAU,EACxBX,SAASU,IAAI,CAACE,eAAe,EAC5B;oBACD,IAAI,MAAMT,SAASC,MAAM,OAAOA;gBAClC;gBACA,OAAO;YACT;YACA,MAAMS,gBAAgB,OACpBC;gBAEA,IAAI,gBAAgBA,QAAQ;oBAC1B,MAAMV,MAAM,MAAMK;oBAClB,OAAOL,MAAM;wBAAEA;wBAAKW,gBAAgB;oBAAK,IAAI;gBAC/C;gBACA,OAAO,AAAC,MAAMZ,SAASW,OAAOE,SAAS,IACnC;oBAAEZ,KAAKU,OAAOE,SAAS;oBAAED,gBAAgB;gBAAM,IAC/C;YACN;YAEA,IAAIE,WAA4B;YAChC,IAAIC,UAAkC;YACtC,IAAIrD,OAAOf,IAAI,KAAK,WAAW;oBAInBe,sBACGA,oBAA8BgC;gBAJ3C,MAAMsB,UAAU;oBACdC,QAAQvD,OAAOX,KAAK,CAACkE,MAAM;oBAC3B1C,OAAOxB,MAAMwB,KAAK;oBAClB2C,MAAM,GAAExD,uBAAAA,OAAOX,KAAK,CAACmE,MAAM,YAAnBxD,uBAAuB;oBAC/ByD,MAAM;4BAAKzD,qBAAAA,OAAOX,KAAK,CAACoE,IAAI,YAAjBzD,qBAAqB,EAAE;4BAAOgC,cAAAA,MAAMyB,IAAI,YAAVzB,cAAc,EAAE;qBAAE;gBAC7D;gBACA,KAAK,MAAM0B,QAAQvB,SAASwB,KAAK,CAAE;oBACjC,IAAI,CAAChG,sBAAsB+F,KAAKE,IAAI,EAAEN,UAAU;oBAChD,MAAMO,MAAM,MAAMb,cAAcU,KAAKI,MAAM;oBAC3C,IAAID,KAAK;wBACPT,WAAW,aAAKS;4BAAKvE,UAAUuE,IAAItB,GAAG;4BAAEwB,IAAI;4BAAQC,QAAQN,KAAKvE,EAAE;;wBACnE;oBACF;gBACF;gBACA,MAAM8E,WAAW9B,SAAS+B,iBAAiB,CAAC7E,MAAMP,MAAM,CAAC;gBACzD,IAAI,CAACsE,YAAYa,YAAa,MAAM3B,SAAS2B,WAAY;oBACvDb,WAAW;wBAAE9D,UAAU2E;wBAAUF,IAAI;wBAAWb,gBAAgB;oBAAM;gBACxE;YACF,OAAO;gBACL,MAAM,EAAEY,MAAM,EAAE,GAAG9D,OAAOX,KAAK;gBAC/B,MAAMwE,MAAM,MAAMb,cAAcc;gBAChC,IAAID,KAAK;oBACPT,WAAW,aACNS;wBACHvE,UAAUuE,IAAItB,GAAG;wBACjBwB,IAAI,gBAAgBD,SAAS,eAAe;;gBAEhD,OAAO;oBACLT,UAAU,gBAAgBS,SAAS,eAAe;gBACpD;YACF;YACA,IAAI,CAACV,UAAU,OAAO;gBAAElD,SAAS;gBAAQC,QAAQkD;YAAQ;YACzD,IAAID,SAAS9D,QAAQ,KAAK4C,SAAS;gBACjC,OAAO;oBAAEhC,SAAS;oBAAaZ,UAAU4C;gBAAQ;YACnD;YAEAhB,GAAGiD,MAAM,CAAC1D,YAAY;gBACpB,CAAC5C,iBAAiBkE,MAAME,OAAO,EAAE,YAAY,EAAEmB,SAAS9D,QAAQ;gBAChE8E,WAAW7F,WAAW8F,eAAe;YACvC;YACA,IAAIjB,SAASF,cAAc,EAAE;gBAC3BhC,GAAGiD,MAAM,CAAC7D,QAAQ;oBAChB,CAACxC,mCAAmC,EAAEsF,SAAS9D,QAAQ;gBACzD;YACF;YACA;;;;SAIC,GACD,MAAMgF,YAAYxC,eAAOR,gCAAAA,aAAcG,GAAG,CAAC,8BAAe;YAC1D,MAAM8C,eAAeC,QACnBlD,CAAAA,gCAAAA,aAAcK,MAAM,KAAK3B,CAAAA,OAAOf,IAAI,KAAK,cAAc,CAACqF,SAAQ;YAElE,IAAIC,gBAAgBzD,SAAS;gBAC3BI,GAAGiD,MAAM,CAACrD,SAAS;oBACjBxB,UAAU8D,SAAS9D,QAAQ;oBAC3B8E,WAAW7F,WAAW8F,eAAe;gBACvC;YACF;YACA,OAAO;gBACLnE,SAAS;gBACTZ,UAAU8D,SAAS9D,QAAQ;gBAC3ByE,IAAIX,SAASW,EAAE;eACXX,SAASY,MAAM,GAAG;gBAAEA,QAAQZ,SAASY,MAAM;YAAC,IAAI,CAAC;gBACrDO;gBACAE,UAAU;;QAEd;QAEF,IAAIzD,QAAQd,OAAO,KAAK,YAAY,OAAOc;QAC3C,6DAA6D;QAC7D,MAAM1C,uBAAuBmC,YAAY;QAEzC,IACE,CAACO,QAAQuD,YAAY,IACrBzD,WACAd,OAAOf,IAAI,KAAK,aAChBP,oBAAoBgG,GAAG,CAAC1E,OAAOX,KAAK,CAACkE,MAAM,GAC3C;YACA,KAAKoB,yBAAyB7D,SAASE,QAAQ1B,QAAQ;QACzD;QAEA,MAAMmF,WAAW,MAAMrF,qBAAqB;YAC1CN,QAAQO,MAAMP,MAAM;YACpBgB;YACAR,UAAU0B,QAAQ1B,QAAQ;YAC1BC,UAAUF,MAAME,QAAQ;YACxBR,QACEiC,QAAQuD,YAAY,IAAI3D,UACpB;gBAAE3B,MAAM;gBAAQE,IAAIyB;YAAQ,IAC5B;gBAAE3B,MAAM;gBAAWE,IAAIE,MAAMqB,SAAS;YAAC;YAC7Cd,KAAKmB,eAAe1B,MAAMwB,KAAK;QACjC;QACA,OAAO,aAAKG;YAASyD;;IACvB,EAAE,OAAOG,OAAO;QACdC,QAAQD,KAAK,CACX,6BACAvF,MAAMP,MAAM,EACZO,MAAMqB,SAAS,EACfkE;QAEF,OAAO;YAAE1E,SAAS;YAAQC,QAAQ;QAAS;IAC7C;AACF;AAEA;;;;;;;;;CASC,GACD,eAAewE,yBACb7D,OAA4C,EAC5CxB,QAAgB,EAChBwF,WAA8BlG,oBAAoB;IAElD,KAAK,MAAMmG,SAASD,SAAU;QAC5B,MAAM,IAAIvD,QAAQ,CAACG,UAAYsD,WAAWtD,SAASqD;QACnD,IAAI;gBAGSvF;YAFX,MAAMA,OAAO,MAAMsB,QAAQW,GAAG;YAC9B,IAAI,CAACjC,KAAKmC,MAAM,EAAE;YAClB,IAAIG,QAAOtC,YAAAA,KAAKiC,GAAG,CAAC,uBAATjC,YAAwB,KAAK,OAAO;YAC/C,MAAMsB,QAAQqD,MAAM,CAAC;gBACnB7E;gBACA8E,WAAW7F,WAAW8F,eAAe;YACvC;YACA,OAAO;QACT,EAAE,OAAOO,OAAO;YACdC,QAAQD,KAAK,CAAC,yCAAyC9D,QAAQmE,IAAI,EAAEL;YACrE,OAAO;QACT;IACF;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASM,sBACd7F,KAA6B;IAE7B,OAAOU,YAAY;QAAEd,MAAM;QAAWI;IAAM;AAC9C;AAEA;;;;;CAKC,GACD,OAAO,SAAS8F,qBACd9F,KAAwB;IAExB,OAAOU,YAAY;QAAEd,MAAM;QAAYI;IAAM;AAC/C;AAEA,eAAe6F,sBAAqB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/assign-contact-owner.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n assignmentRuleMatches,\n consentGroupForHost,\n contactFacetPath,\n type ContactSource,\n CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH,\n type CrmAssignmentTarget,\n personKey,\n readContactFacet,\n readCrmAssignmentSettings,\n roundRobinOrder,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getHostDocAdmin,\n getOrgForHost,\n hostDisplayName,\n notifyUsers,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * WHO A NEW RECORD BELONGS TO — the one place an owner is decided on the\n * server (AGL-2618).\n *\n * Two entry points over one transaction. `assignOwnerForCapture` is the\n * capture door's pass: it runs the org's assignment rules in order, falls\n * back to the capturing site's default owner, and touches only a record\n * that has NO owner — a door that named one (the console's drawer, an\n * import column, a conversion with a picked owner) has expressed a person's\n * choice. `reassignContactOwner` is the automation's: an author who put\n * \"assign an owner\" on a stage change means to reassign, and the step hands\n * this either a member or the rotation.\n *\n * ## One transaction, because the pointer is shared\n *\n * The round-robin pool's pointer lives on the org document, and two\n * captures landing together must take two different members. Firestore\n * transactions serialize on what they read, so the org document, the\n * contact and the roster documents are all read inside the transaction and\n * the pointer is advanced in the same commit that writes the owner: a\n * concurrent capture that read the same pointer retries and reads the moved\n * one. The alternative — read the pointer, decide, then write — is exactly\n * the read-then-write shape that lets two captures pick the same person.\n *\n * ## The roster is checked, once per member per attempt\n *\n * A rule may name a member who has since left, and a pool may hold one. A\n * rule naming nobody on the roster is passed over rather than stopping the\n * pass — the next rule may well assign — and a pool member who is gone is\n * skipped for the next in the order. `orgs/{orgId}/members/{uid}` is keyed\n * by uid, so the check is one document read per candidate, and only the\n * roster is consulted: a project-level Auth lookup would admit people who\n * are not on this organization at all (AGL-1122).\n *\n * ## The lead follows the contact\n *\n * The capture doors that file a lead (`addHostLead`: a form routed to leads,\n * a sign-up, a booking) key it by `personKey(email)`, so the lead's document\n * is addressable from the capture alone. When it exists it is given the same\n * owner in the same transaction; two doors write the lead AFTER the contact\n * they `void`ed, so for those sources the mirror is retried a few times over\n * the next seconds, detached, and the person's Leads list shows the owner\n * as soon as the lead is there.\n *\n * ## The owner is told, unless they chose themselves\n *\n * One console notification per assignment, for the record the recipient\n * will open: the lead when the site holds one for this person, the contact\n * otherwise. Not when the actor IS the new owner — a rep who converts a\n * lead into their own contact has not been handed anything.\n *\n * ## Never rejects\n *\n * The capture has already succeeded and a form submission must not fail\n * because the CRM could not decide who follows up; a failure is logged and\n * answered as `none`, and the record is exactly as assignable by hand as it\n * was before this existed. The automation executor turns a `none` into the\n * step's recorded error.\n */\n\nexport interface OwnerAssignmentInput {\n hostId: string\n /** `orgs/{orgId}/contacts/{contactId}` — the record the owner is written on. */\n contactId: string\n /** The contact's address, which keys the lead and names the person in the notification. */\n email: string\n /**\n * The signed-in person making the assignment, when there is one. Never\n * notified about their own choice; absent for a capture and an automation.\n */\n actorUid?: string | null\n}\n\nexport interface CaptureAssignmentInput extends OwnerAssignmentInput {\n source: ContactSource\n /** The form the capture came through, for a rule conditioned on it. */\n formId?: string | null\n /** The tags the capture carried; the contact's own are read from the facet. */\n tags?: readonly string[]\n}\n\nexport interface ReassignmentInput extends OwnerAssignmentInput {\n assign: CrmAssignmentTarget\n}\n\n/** Why an assignment assigned nobody — the automation reports these verbatim. */\nexport type OwnerAssignmentRefusal =\n | 'no-org'\n | 'no-contact'\n | 'no-rule'\n | 'empty-pool'\n | 'not-a-member'\n | 'failed'\n\n/** What decided an assignment: a rule, the site default, the step's member, or the rotation. */\nexport type OwnerAssignedBy = 'rule' | 'default' | 'member' | 'roundRobin'\n\nexport type OwnerAssignment =\n | {\n outcome: 'assigned'\n ownerUid: string\n by: OwnerAssignedBy\n /** The rule that decided it, when one did. */\n ruleId?: string\n /** The lead document existed and now carries the owner too. */\n leadMirrored: boolean\n /** A notification was fanned out to the new owner. */\n notified: boolean\n }\n /** The record already had this owner (a capture), or the same one was named again (a reassignment). */\n | { outcome: 'unchanged'; ownerUid: string }\n | { outcome: 'none'; reason: OwnerAssignmentRefusal }\n\n/** The words the run history and the log give each refusal. */\nexport const OWNER_ASSIGNMENT_REFUSALS: Record<OwnerAssignmentRefusal, string> = {\n 'no-org': 'this site has no organization',\n 'no-contact': 'the contact no longer exists',\n 'no-rule': 'no assignment rule matched and the site has no default owner',\n 'empty-pool': 'the round-robin pool has nobody on the roster in it',\n 'not-a-member': 'the owner named is not on the team',\n failed: 'the owner could not be assigned',\n}\n\n/**\n * The doors whose lead lands after the contact they `void`ed — the ones the\n * mirror is worth retrying for. An order, an import, a manual add and an\n * API call file no lead, and a retry for them would be reads for nothing.\n */\nconst LEAD_FILING_SOURCES: ReadonlySet<ContactSource> = new Set([\n 'form',\n 'member',\n 'booking',\n])\n\n/** How long the detached lead mirror waits between its looks, in order. */\nconst LEAD_MIRROR_RETRY_MS: readonly number[] = [1_000, 2_000, 3_000]\n\ntype Policy =\n | { kind: 'capture'; input: CaptureAssignmentInput }\n | { kind: 'reassign'; input: ReassignmentInput }\n\ninterface Decision {\n ownerUid: string\n by: OwnerAssignedBy\n ruleId?: string\n /** The pointer moves only when the rotation chose. */\n advancePointer: boolean\n}\n\n/**\n * The console path the owner's notification opens, in the `/{hostDocId}/rest`\n * shape every host notification uses — the console rewrites it to\n * `/{orgSlug}/hosts/{subdomain}/rest` when the link is followed, which is\n * why `hostId` and `orgId` travel on the document. The same spelling the\n * CRM's task notification uses; the hub's own route builder lives in the\n * plugin, which this layer cannot import.\n */\nfunction recordLink(hostId: string, record: AssignedRecord): string {\n const section = record.kind === 'contact' ? 'contacts' : 'leads'\n return `/${hostId}/crm/${section}/${encodeURIComponent(record.id)}`\n}\n\n/** The record an assignment notification opens. */\nexport type AssignedRecord =\n | { kind: 'contact'; id: string }\n | { kind: 'lead'; id: string }\n\n/**\n * Tells a member a record is now theirs — one console notification, typed\n * by the record it opens — unless the member handed it to themselves.\n * Shared by the pass above and by the routes where a person picks the\n * owner outright (a lead conversion naming a colleague), so every server\n * path that writes `ownerUid` says so in the same words. Never throws;\n * the fan-out swallows its own failures.\n */\nexport async function notifyRecordAssigned(input: {\n hostId: string\n orgId: string\n ownerUid: string\n actorUid?: string | null\n record: AssignedRecord\n /** How the person reads in the message: their name, else their address. */\n who: string\n}): Promise<boolean> {\n if (!input.ownerUid || input.ownerUid === (input.actorUid ?? '')) return false\n const lead = input.record.kind === 'lead'\n /*\n * THE BODY SAYS WHAT HAPPENED, AND ON WHICH SITE (AGL-3432).\n *\n * It was the bare name, so a member who owns records on several sites read\n * \"Ada\" in an email body and could not tell what Ada was or where. The\n * site's name is one request-cached read of a document most callers have\n * already paid for, taken only when somebody is actually notified; a read\n * that fails drops the \"on {site}\" clause rather than the notification.\n */\n let site = ''\n try {\n const host = await getHostDocAdmin(input.hostId)\n if (host) site = ` on ${hostDisplayName(host, input.hostId)}`\n } catch {\n // The clause goes, never the notification.\n }\n await notifyUsers([input.ownerUid], {\n type: lead ? 'content.leadAssigned' : 'content.contactAssigned',\n title: lead ? 'Lead assigned to you' : 'Contact assigned to you',\n body: `${input.who} was assigned to you as a ${lead ? 'lead' : 'contact'}${site}.`,\n link: recordLink(input.hostId, input.record),\n orgId: input.orgId,\n hostId: input.hostId,\n })\n return true\n}\n\nasync function assignOwner(policy: Policy): Promise<OwnerAssignment> {\n const { input } = policy\n try {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const { orgId } = resolved\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contactRef = orgRef.collection('contacts').doc(input.contactId)\n const membersRef = orgRef.collection('members')\n const leadKey = personKey(input.email)\n // Beside the contact on the org, keyed by the same address (AGL-3275).\n const leadRef = leadKey ? orgRef.collection('leads').doc(leadKey) : null\n\n let contactName = ''\n const verdict = await firestore.runTransaction(\n async (tx): Promise<OwnerAssignment> => {\n /*\n * ALL READS BEFORE THE WRITE, which Firestore requires. The org\n * document is read here rather than trusted from `getOrgForHost`,\n * because the pointer it carries is the one thing a concurrent\n * capture may have moved, and the rules and the pool are read from\n * the same snapshot so the decision and the pointer agree.\n */\n const [orgSnapshot, contactSnapshot, leadSnapshot] = await Promise.all([\n tx.get(orgRef),\n tx.get(contactRef),\n leadRef ? tx.get(leadRef) : Promise.resolve(null),\n ])\n if (!contactSnapshot.exists) {\n return { outcome: 'none', reason: 'no-contact' }\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const contact = (contactSnapshot.data() ?? {}) as Record<string, unknown>\n contactName = String(contact['name'] ?? '')\n const group = consentGroupForHost(org, input.hostId)\n const facet = readContactFacet(contact, group.groupId)\n const current = typeof facet.ownerUid === 'string' ? facet.ownerUid : ''\n if (policy.kind === 'capture' && current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n const settings = readCrmAssignmentSettings(org)\n\n // One read per candidate per attempt; a retried transaction reads\n // afresh, as it must.\n const onRoster = new Map<string, Promise<boolean>>()\n const isMember = (uid: string): Promise<boolean> => {\n let known = onRoster.get(uid)\n if (!known) {\n known = tx.get(membersRef.doc(uid)).then((snapshot) => snapshot.exists)\n onRoster.set(uid, known)\n }\n return known\n }\n const fromPool = async (): Promise<string | null> => {\n for (const uid of roundRobinOrder(\n settings.pool.memberUids,\n settings.pool.lastAssignedUid,\n )) {\n if (await isMember(uid)) return uid\n }\n return null\n }\n const resolveTarget = async (\n target: CrmAssignmentTarget,\n ): Promise<{ uid: string; advancePointer: boolean } | null> => {\n if ('roundRobin' in target) {\n const uid = await fromPool()\n return uid ? { uid, advancePointer: true } : null\n }\n return (await isMember(target.memberUid))\n ? { uid: target.memberUid, advancePointer: false }\n : null\n }\n\n let decision: Decision | null = null\n let refusal: OwnerAssignmentRefusal = 'no-rule'\n if (policy.kind === 'capture') {\n const capture = {\n source: policy.input.source,\n email: input.email,\n formId: policy.input.formId ?? null,\n tags: [...(policy.input.tags ?? []), ...(facet.tags ?? [])],\n }\n for (const rule of settings.rules) {\n if (!assignmentRuleMatches(rule.when, capture)) continue\n const hit = await resolveTarget(rule.assign)\n if (hit) {\n decision = { ...hit, ownerUid: hit.uid, by: 'rule', ruleId: rule.id }\n break\n }\n }\n const fallback = settings.hostDefaultOwners[input.hostId]\n if (!decision && fallback && (await isMember(fallback))) {\n decision = { ownerUid: fallback, by: 'default', advancePointer: false }\n }\n } else {\n const { assign } = policy.input\n const hit = await resolveTarget(assign)\n if (hit) {\n decision = {\n ...hit,\n ownerUid: hit.uid,\n by: 'roundRobin' in assign ? 'roundRobin' : 'member',\n }\n } else {\n refusal = 'roundRobin' in assign ? 'empty-pool' : 'not-a-member'\n }\n }\n if (!decision) return { outcome: 'none', reason: refusal }\n if (decision.ownerUid === current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n\n tx.update(contactRef, {\n [contactFacetPath(group.groupId, 'ownerUid')]: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (decision.advancePointer) {\n tx.update(orgRef, {\n [CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH]: decision.ownerUid,\n })\n }\n /*\n * A lead somebody already assigned by hand keeps its owner on a\n * capture — the contact was new, the lead may not have been — and\n * follows the contact on a deliberate reassignment.\n */\n const leadOwner = String(leadSnapshot?.get('ownerUid') ?? '')\n const leadMirrored = Boolean(\n leadSnapshot?.exists && (policy.kind === 'reassign' || !leadOwner),\n )\n if (leadMirrored && leadRef) {\n tx.update(leadRef, {\n ownerUid: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n return {\n outcome: 'assigned',\n ownerUid: decision.ownerUid,\n by: decision.by,\n ...(decision.ruleId ? { ruleId: decision.ruleId } : {}),\n leadMirrored,\n notified: false,\n }\n },\n )\n if (verdict.outcome !== 'assigned') return verdict\n // The owner is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n\n if (\n !verdict.leadMirrored &&\n leadRef &&\n policy.kind === 'capture' &&\n LEAD_FILING_SOURCES.has(policy.input.source)\n ) {\n void mirrorOwnerOntoLeadLater(leadRef, verdict.ownerUid)\n }\n\n const notified = await notifyRecordAssigned({\n hostId: input.hostId,\n orgId,\n ownerUid: verdict.ownerUid,\n actorUid: input.actorUid,\n record:\n verdict.leadMirrored && leadKey\n ? { kind: 'lead', id: leadKey }\n : { kind: 'contact', id: input.contactId },\n who: contactName || input.email,\n })\n return { ...verdict, notified }\n } catch (error) {\n console.error(\n 'assignContactOwner failed',\n input.hostId,\n input.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\n/**\n * The detached second look for a lead that lands after the contact.\n *\n * Bounded, and only ever ADDS an owner: a lead that arrived with one (a\n * person assigned it in the meantime) is left alone, and a lead that never\n * arrives costs three reads over six seconds and nothing else. Detached\n * because the capture must not hold `contactCreated` back for it — an\n * automation welcoming the contact has nothing to do with the lead's owner\n * column.\n */\nasync function mirrorOwnerOntoLeadLater(\n leadRef: FirebaseFirestore.DocumentReference,\n ownerUid: string,\n delaysMs: readonly number[] = LEAD_MIRROR_RETRY_MS,\n): Promise<boolean> {\n for (const delay of delaysMs) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n try {\n const lead = await leadRef.get()\n if (!lead.exists) continue\n if (String(lead.get('ownerUid') ?? '')) return false\n await leadRef.update({\n ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n console.error('assignContactOwner lead mirror failed', leadRef.path, error)\n return false\n }\n }\n return false\n}\n\n/**\n * The capture door's pass: the org's rules in order, then the capturing\n * site's default owner, on a record that has no owner yet. See the module\n * comment for the whole of it.\n */\nexport function assignOwnerForCapture(\n input: CaptureAssignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'capture', input })\n}\n\n/**\n * The deliberate assignment — the `assignContactOwner` step, and a person\n * handing a record to somebody: a member by uid (verified on the roster) or\n * the next in the round-robin pool, overwriting whoever held it. Naming the\n * owner the record already has changes nothing and tells nobody.\n */\nexport function reassignContactOwner(\n input: ReassignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'reassign', input })\n}\n\nexport default assignOwnerForCapture\n"],"names":["assignmentRuleMatches","consentGroupForHost","contactFacetPath","CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH","personKey","readContactFacet","readCrmAssignmentSettings","roundRobinOrder","firebaseAdmin","getHostDocAdmin","getOrgForHost","hostDisplayName","notifyUsers","restampCrmListFieldsAt","FieldValue","OWNER_ASSIGNMENT_REFUSALS","failed","LEAD_FILING_SOURCES","Set","LEAD_MIRROR_RETRY_MS","recordLink","hostId","record","section","kind","encodeURIComponent","id","notifyRecordAssigned","input","ownerUid","actorUid","lead","site","host","type","title","body","who","link","orgId","assignOwner","policy","resolved","outcome","reason","firestore","app","orgRef","collection","doc","contactRef","contactId","membersRef","leadKey","email","leadRef","contactName","verdict","runTransaction","tx","orgSnapshot","contactSnapshot","contact","leadSnapshot","Promise","all","get","resolve","exists","org","data","String","group","facet","groupId","current","settings","onRoster","Map","isMember","uid","known","then","snapshot","set","fromPool","pool","memberUids","lastAssignedUid","resolveTarget","target","advancePointer","memberUid","decision","refusal","capture","source","formId","tags","rule","rules","when","hit","assign","by","ruleId","fallback","hostDefaultOwners","update","updatedAt","serverTimestamp","leadOwner","leadMirrored","Boolean","notified","has","mirrorOwnerOntoLeadLater","error","console","delaysMs","delay","setTimeout","path","assignOwnerForCapture","reassignContactOwner"],"mappings":"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"}
|
|
@@ -666,10 +666,12 @@ export async function composeNodesWithChrome(options) {
|
|
|
666
666
|
* with the page exactly as a visitor will receive it. So this is where a
|
|
667
667
|
* page that links to a brand's lookalike, asks for a password or a card
|
|
668
668
|
* in a field of its own, or (for a new workspace) dresses a brand's
|
|
669
|
-
* sign-in
|
|
669
|
+
* sign-in or sends its call to action off the site beside a document or
|
|
670
|
+
* account lure is held rather than served. See `hosted-page-review.ts`.
|
|
670
671
|
*
|
|
671
672
|
* A held version serves the last version this page served clean, screened
|
|
672
|
-
* again, or nothing
|
|
673
|
+
* again, or nothing — and so does a flagged page whose review could not
|
|
674
|
+
* finish, which is held rather than served.
|
|
673
675
|
*/ const nodes = await composed;
|
|
674
676
|
const version = versionRes.version;
|
|
675
677
|
const review = await reviewHostedPage({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-screen-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\n// By path: the overlay is server-only, and every `@aglyn/aglyn` barrel\n// re-exports `app-utils/server` into published pages.\nimport {\n applyMediaAssetFacts,\n mediaAssetRefs,\n} from '@aglyn/aglyn/app-utils/media-asset-facts'\nimport {\n recordServedPageVersion,\n reviewHostedPage,\n servedPageVersion,\n} from '@aglyn/tenant-data-admin/server/hosted-page-review'\nimport type { HostedPagePart } from '@aglyn/tenant-data-admin/server/held-page-subject'\n// By path: the contracts a repeat's rows and a form's record target are\n// asked through, both server-only.\nimport { stampFormRecordTargets } from '@aglyn/aglyn/plugin-manager/submission-record-target'\nimport { readRepeatRows } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { prepareComputedVariables } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport applyDuePublishSchedule from './apply-publish-schedule'\nimport getComponents from './get-components'\nimport getForms from './get-forms'\nimport getMediaAssetFacts from './get-media-asset-facts'\nimport {\n getPublishedCollectionSource,\n type PublishedCollectionSource,\n} from './get-collection-content'\nimport getPluginInstalls from './get-plugin-installs'\nimport getVariables, { getFunctions } from './get-variables'\nimport getPublishedLayoutVersion from './get-layout-version'\nimport getScreenVersion from './get-screen-version'\nimport {\n type ComposeSocialImages,\n socialImageAssetFacts,\n socialImageRefs,\n} from './social-image-facts'\n\n/**\n * Content-collection context for a compose (AGL-551): the collection the\n * route resolved (list/entry template screens). `entries` rides along when\n * the route already fetched them (list pages); blocks bound to other\n * collections — or to this one when `entries` is absent — fetch on demand.\n */\nexport interface ComposeCollectionContext {\n slug: string\n entries?: Aglyn.CollectionEntryRecord[]\n /**\n * The entry being rendered (AGL-582, entry-template screens / entry\n * fallback) — the Related posts block resolves against it.\n */\n entry?: Aglyn.CollectionEntryRecord | null\n /**\n * The routed collection's category taxonomy (AGL-582): entry\n * `categoryId`s resolve to display names against it during expansion.\n */\n categories?: Aglyn.CollectionCategory[]\n /**\n * The list page the URL asked for (AGL-1321). Fills in an entries block\n * that declares `perPage` but no `page` — design time cannot know which\n * page a visitor is on.\n */\n page?: number\n /**\n * The category segment the URL filtered on (AGL-1321); marks the current\n * pill in a Category Pills block. `entries` arrives already filtered.\n */\n categorySlug?: string\n /**\n * Whether the read behind `entries` stopped at its `.limit()` (AGL-1516).\n * Travels with the entries because the search boxes downstream have to say\n * what they actually searched, and — with the liveness gate and the route's\n * category filter both in between — `entries.length` no longer tells them.\n */\n entriesReachedBound?: boolean\n /**\n * Where `entries` begins in the collection's own order (AGL-3213).\n *\n * Zero for every listing served from the cached read. A page past that\n * read's bound carries only its own ten entries, and the block windows\n * `[(page - 1) * perPage, …)` — so without this it would slice from an\n * offset its own array never reaches and render an empty listing.\n */\n windowStart?: number\n /**\n * Whether {@link slug} is a cache KEY rather than an address (AGL-2524).\n *\n * The author page mixes collections, and the compose pipeline keys entry\n * sources by collection slug — so it hands over a synthetic one\n * (`AUTHOR_ENTRIES_SOURCE_SLUG`) with its entries already in hand. That is\n * harmless for the entries block, whose every row carries its OWN\n * `collectionSlug` and builds `entry.url` from it, and for the search box,\n * whose index is built the same way.\n *\n * It is NOT harmless for anything that builds a URL from the source's slug\n * itself. Set this and such a block resolves nothing rather than pointing\n * readers at `/{synthetic}/…`.\n */\n routeless?: boolean\n}\n\ninterface CollectionBlockScan {\n slugs: Set<string>\n hasRelated: boolean\n hasCategories: boolean\n hasSearch: boolean\n}\n\n/**\n * Which collections does this tree ask for (AGL-1152)?\n *\n * Extracted so the PREFETCH below and the expansion that consumes it read the\n * tree through one function rather than two copies of the same predicate. A\n * divergence between them is not a type error — it is a slug prefetched and\n * never awaited, or (worse) a slug the prefetch missed that then pays the full\n * serial read anyway — so there is deliberately no second implementation to\n * drift.\n */\nfunction scanCollectionBlocks(\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): CollectionBlockScan {\n const slugs = new Set<string>()\n let hasRelated = false\n let hasCategories = false\n let hasSearch = false\n for (const node of Object.values(nodes)) {\n if (node?.componentId === Aglyn.COLLECTION_ENTRIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) slugs.add(slug)\n }\n // Category pills (AGL-1321) need only the taxonomy, but it rides on the\n // same source, so they count as a reason to resolve one.\n if (node?.componentId === Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasCategories = true\n slugs.add(slug)\n }\n }\n // The standalone toolbar search box (AGL-1516, Figma 494:1220) needs the\n // collection's entries to index, and rides the same source the listing\n // beside it was built from — one read, one answer.\n if (node?.componentId === Aglyn.COLLECTION_SEARCH_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasSearch = true\n slugs.add(slug)\n }\n }\n // Related posts (AGL-582) always resolve against the ROUTED collection\n // — they only mean something with a current entry in context.\n if (\n node?.componentId === Aglyn.COLLECTION_RELATED_COMPONENT_ID &&\n collection?.slug &&\n collection.entry\n ) {\n hasRelated = true\n slugs.add(collection.slug)\n }\n }\n return { slugs, hasRelated, hasCategories, hasSearch }\n}\n\n/**\n * Issue the collection reads AS SOON AS THE SCREEN NODES EXIST (AGL-1152).\n *\n * `getPublishedCollectionSource` is three SEQUENTIAL round trips —\n * `findContentCollection`, then `listLiveEntries`, then `attachEntryAuthors`\n * — and until this existed it ran after the chrome bundle had already been\n * awaited, so a page carrying a Collection entries block paid the whole thing\n * as a serial tail on the compose phase. Measured on the tenant: a page with\n * no collection block composes in ~20 ms, one with a block in ~572 ms, and the\n * tree work itself accounts for under 2 ms of that at 50 entries. The gap is\n * this read, waiting for reads it shares nothing with.\n *\n * Same shape as `screenRepeatRowsPromise` directly below, and the same caveat\n * applies: the SCREEN's own nodes are a fast path, NOT the correctness gate. A\n * collection block can arrive from a layout or a grafted reusable component,\n * neither of which exists yet at this point, so the real scan still runs\n * against the composed tree and still fetches anything this missed.\n *\n * The ROUTED collection is deliberately excluded when its entries are already\n * in hand: the expansion below answers that slug from `collection.entries`\n * without reading at all, so prefetching it would buy a read nobody awaits.\n */\nfunction prefetchCollectionSources(\n hostId: string,\n screenNodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): Record<string, Promise<PublishedCollectionSource>> {\n const prefetched: Record<\n string,\n Promise<PublishedCollectionSource>\n > = {}\n for (const slug of scanCollectionBlocks(screenNodes, collection).slugs) {\n if (slug === collection?.slug && collection.entries) continue\n const pending = getPublishedCollectionSource({\n hostId,\n collectionSlug: slug,\n })\n // Marked handled the moment it exists, for the reason `composed` is in\n // `composeScreenNodes`: a tree that turns out not to need this slug never\n // awaits it, and an unawaited rejection takes the process down rather than\n // failing this one read. The real await below still sees the rejection.\n void pending.catch(() => undefined)\n prefetched[slug] = pending\n }\n return prefetched\n}\n\n/**\n * Expands Collection entries blocks (AGL-551) against their collections'\n * published entries, and Related posts blocks (AGL-582) against the routed\n * entry. Fetches lazily — screens without the blocks cost nothing — and\n * fails open on lookup errors like every other compose stage.\n */\nasync function expandCollectionEntryBlocks(\n hostId: string,\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n prefetched?: Record<string, Promise<PublishedCollectionSource>>,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Promise<Record<string, any>> {\n const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(\n nodes,\n collection,\n )\n if (!slugs.size) return nodes\n const sources: Record<string, Aglyn.CollectionEntriesSource> = {}\n await Promise.all(\n [...slugs].map(async (slug) => {\n // The routed collection rides its already-fetched entries +\n // categories (AGL-582); other collections fetch both on demand.\n if (slug === collection?.slug && collection.entries) {\n sources[slug] = {\n slug,\n entries: collection.entries,\n categories: collection.categories,\n // Only the ROUTED collection carries the URL's page (AGL-1321) — a\n // block bound to another collection must not inherit it.\n ...(collection.page ? { page: collection.page } : {}),\n // ...nor its bound (AGL-1516): this is a fact about the read the\n // route already performed, and it describes only that collection.\n ...(collection.entriesReachedBound ? { reachedBound: true } : {}),\n // ...nor where its window starts (AGL-3213), for the same reason:\n // it is a fact about the read this route performed.\n ...(collection.windowStart\n ? { windowStart: collection.windowStart }\n : {}),\n }\n return\n }\n // The prefetch when this slug was visible on the screen's own nodes;\n // a live read when it only appeared after layout/component grafting.\n const fetched = await (prefetched?.[slug] ??\n getPublishedCollectionSource({ hostId, collectionSlug: slug }))\n sources[slug] = {\n slug,\n entries: fetched.entries,\n categories:\n slug === collection?.slug && collection.categories\n ? collection.categories\n : fetched.categories,\n ...(fetched.reachedBound ? { reachedBound: true } : {}),\n }\n }),\n )\n const expanded = Aglyn.expandCollectionEntries(\n nodes,\n sources,\n collection?.slug,\n timeZone,\n )\n const withCategories = hasCategories\n ? Aglyn.expandCollectionCategories(\n expanded,\n sources,\n /*\n No DEFAULT collection for the pills on a routeless page (AGL-2524).\n\n Category pills are the one block that builds its links from the\n SOURCE's slug — `/{slug}/category/{x}` — because a category is a\n filter on one collection's own listing. On the author page that slug\n is synthetic, so an unbound pills block stamped links to a route that\n does not exist.\n\n Rendering nothing is the honest answer rather than a fallback. The\n page spans every collection, and there is no\n `/author/{slug}/category/{x}` for a pill to lead to; the only real\n destination would be some collection's listing, which drops the\n author the reader is looking at. A pills block that NAMES a\n collection is unaffected and still resolves that collection's own\n taxonomy — \"browse the blog by category\", which is a sensible thing\n to put beside an archive.\n */\n collection?.routeless ? undefined : collection?.slug,\n collection?.categorySlug,\n )\n : expanded\n const withSearch = hasSearch\n ? Aglyn.expandCollectionSearch(\n withCategories,\n sources,\n collection?.slug,\n timeZone,\n )\n : withCategories\n if (!hasRelated || !collection?.entry) return withSearch\n return Aglyn.expandCollectionRelated(\n withSearch,\n sources[collection.slug],\n collection.entry,\n timeZone,\n )\n}\n\n/**\n * Shared post-version composition (AGL-551, extracted from\n * `composeScreenNodes`): layout chrome, reusable components, repeatables,\n * collection entries, bindings, function definitions, plugin installs,\n * named tokens, denormalize, and last the current facts of each placed image\n * and film, and of the social card the page is shared as. The screen path and\n * the collection-fallback\n * path (which has no screen doc) build identical trees through this one\n * pipeline.\n */\n/**\n * The function bindings that apply to a composed tree (AGL-3393).\n *\n * First-party declarations are compiled in and cost nothing. A marketplace\n * plugin's live in its pinned version's manifest, which rides on the host's\n * realm installs — a read (render-cached, and the same one the page loader\n * issues for the bundles), so it is made only when it could change the\n * answer: the host has a function to hand out and the tree places an element\n * in a marketplace namespace. First-party declarations are listed first, so\n * no manifest can rebind a first-party element.\n *\n * A failed lookup binds first-party elements only. A marketplace element then\n * renders without its definition, which is its unconfigured placeholder —\n * the same fail-open the bundle lookup takes.\n */\nasync function functionBindingsFor(\n nodes: Record<string, { componentId?: unknown } | null | undefined>,\n hostId: string,\n functions: Record<string, unknown>,\n): Promise<Aglyn.FunctionBindings> {\n const firstParty = Aglyn.FIRST_PARTY_FUNCTION_BINDINGS\n if (!Object.keys(functions).length) return firstParty\n const placesMarketplaceElement = Object.values(nodes).some((node) =>\n Aglyn.isNamespacedComponentId(node?.componentId),\n )\n if (!placesMarketplaceElement) return firstParty\n // The dev loop's plugins have no install, so their manifests are read from\n // disk (AGL-3394). A production build never reaches the module.\n const dev =\n process.env.NODE_ENV === 'production'\n ? []\n : await (await import('./dev-plugin-manifests')).devPluginManifests()\n try {\n const { getRealmPluginInstalls } = await import('./realm-installs-seam')\n const installs = await getRealmPluginInstalls({ hostId })\n return Aglyn.mergeFunctionBindings(firstParty, ...installs, ...dev)\n } catch (error) {\n console.error('function bindings: realm install lookup failed:', error)\n return Aglyn.mergeFunctionBindings(firstParty, ...dev)\n }\n}\n\nexport async function composeNodesWithChrome(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /**\n * The layout binding, or a PROMISE of it.\n *\n * The unresolved form exists for the same reason `screenNodes` accepts one\n * (AGL-1428): the binding may live on the version document (key-present\n * wins over the screen's), and awaiting the version before this call would\n * put every host-scoped read back behind it. Only the layout-chain walk\n * consumes the binding, so it alone waits; the rest of the chrome bundle\n * still starts immediately.\n */\n layoutId?:\n | string\n | null\n | Promise<string | null | undefined>\n /**\n * The screen's values for the properties of the layouts it renders inside\n * (AGL-2893), keyed by layout id — or a PROMISE of them, for the reason\n * `layoutId` accepts one: they live on the version document beside the\n * binding. Absent, every layout renders its properties' defaults.\n */\n layoutPropValues?:\n | Aglyn.AglynScreenVersion['layoutPropValues']\n | null\n | Promise<Aglyn.AglynScreenVersion['layoutPropValues'] | null | undefined>\n /**\n * The screen's per-page restyling of its layouts' elements (AGL-3286),\n * keyed by layout id then layout node id — or a PROMISE of it, for the same\n * reason as `layoutPropValues`, beside which it is stored. Absent, every\n * layout renders with its own styling.\n */\n layoutStyleOverrides?:\n | Aglyn.AglynScreenVersion['layoutStyleOverrides']\n | null\n | Promise<\n Aglyn.AglynScreenVersion['layoutStyleOverrides'] | null | undefined\n >\n /**\n * The screen's own nodes, or a PROMISE of them (AGL-1428).\n *\n * Accepting the unresolved form is what lets `composeScreenNodes` hand the\n * version read over before it has finished, so the host-scoped reads below\n * — none of which look at these nodes — overlap it instead of queueing\n * behind it. Passing a resolved value behaves exactly as before, which is\n * what every other caller does.\n */\n screenNodes: Record<string, any> | Promise<Record<string, any>>\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * The host document, for `host.*` tokens (AGL-1022).\n *\n * Passed in rather than read here: every caller already holds it, and\n * `composeScreenNodes` was the largest single render phase before AGL-1225\n * cut it to one round trip — adding a read back would spend that win on\n * data we already have in hand.\n */\n host?: Aglyn.HostTokenSource | null\n /**\n * The social card the head shares this page as (AGL-2850). The documents of\n * the assets it names are read in the same batch as the images and films\n * the tree places.\n */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any>> {\n const { hostId, layoutId } = options\n\n /**\n * The layout chain, innermost first (AGL-703).\n *\n * A layout may itself render inside another layout, so this walks the\n * `layoutId` pointers rather than reading one. Fetching is sequential\n * because each step's parent is only known once the previous layout\n * document is in hand — but the walk is short by construction\n * (MAX_LAYOUT_CHAIN_DEPTH) and every layout is already a cached read.\n *\n * `seen` stops a cycle from looping forever. Stored data can hold one\n * even though both the console and `canNestLayout` refuse to create it:\n * the API, a script, or a restored backup can all write a layout\n * document directly, and a render must degrade rather than hang.\n */\n const walkLayoutChain = async () => {\n const chain: Aglyn.LayoutChainEntry[] = []\n const seen = new Set<string>()\n const boundLayoutId = await layoutId\n let currentLayoutId = boundLayoutId ? String(boundLayoutId) : undefined\n while (\n currentLayoutId &&\n !seen.has(currentLayoutId) &&\n chain.length < Aglyn.MAX_LAYOUT_CHAIN_DEPTH\n ) {\n seen.add(currentLayoutId)\n const layoutRes = await getPublishedLayoutVersion({\n hostId,\n layoutId: currentLayoutId,\n })\n // The version's declared properties travel with its nodes (AGL-2893):\n // they are applied to this layout alone, with the screen's values for\n // this layout, before the screen is grafted into its slot.\n chain.push({\n layoutId: currentLayoutId,\n nodes: layoutRes?.version?.nodes as any,\n props: (layoutRes?.version as Aglyn.AglynLayoutVersion | undefined)\n ?.props,\n })\n const parentId = (layoutRes?.layout as any)?.layoutId\n currentLayoutId = parentId ? String(parentId) : undefined\n }\n return chain\n }\n\n /**\n * ONE round-trip stage instead of three (AGL-1225).\n *\n * This used to be `await layout walk` → `await getComponents` → `await\n * Promise.all([five reads])`: three sequential waits, where only the first\n * has any reason to be sequential. Every one of the other six reads takes\n * `hostId` and nothing else — none of them consumes the layout chain — so\n * they were waiting on a walk whose result they never look at.\n *\n * The walk stays internally sequential because it genuinely is: each step's\n * parent id is only known once the previous layout document is in hand. It\n * just no longer gates anything else. The critical path becomes the walk\n * alone rather than walk + components + bulk.\n *\n * Measured budget that motivated this (production, `/product/besigner`):\n * `composeScreenNodes` was the largest single phase at 1577 ms cold and\n * consistently ~1.4-1.6 s warm too, so this is not cold-start cost. The\n * existing `AGL-1152:render` timing line reports `composeScreenNodes` as a\n * phase, so the effect of this shows up there directly — no new\n * instrumentation, and a regression would be visible in the same place.\n */\n /**\n * Does the SCREEN itself repeat (AGL-1440)?\n *\n * Asked as soon as the nodes are in hand — which since AGL-1428 is after\n * the fan-out has been ISSUED rather than before — so the overwhelmingly\n * common case still keeps the AGL-1225 shape: a page that repeats almost\n * always says so on its own document, and its rows read goes out\n * alongside the remaining chrome reads instead of after them.\n *\n * It is deliberately NOT the correctness gate — a repeatable can arrive from\n * a layout or a grafted reusable component, neither of which exists yet. The\n * gate is re-asked against the composed tree after grafting, which is the\n * exact input `expandRepeatables` reads.\n */\n /*\n * ISSUE THE HOST-SCOPED READS FIRST, THEN AWAIT THE NODES (AGL-1428).\n *\n * Every read in this bundle is keyed by `hostId` (and `layoutId`) alone —\n * none of them reads `screenNodes` — so starting them before the nodes are\n * in hand lets `composeScreenNodes`' version read overlap them rather than\n * run ahead of them. `Promise.all` is created BEFORE the `await` below on\n * purpose: that is the line that makes the two independent, and moving the\n * await above it silently gives the whole saving back.\n *\n * The repeat rows are the one read that genuinely depends on the nodes, so\n * they cannot join the bundle. It does not have to wait for the bundle either —\n * it is issued the moment the nodes resolve and awaited after, so it still\n * overlaps whatever is left of the chrome reads instead of costing the\n * extra serial round trip that dropping it from the batch would imply.\n */\n const chromeBundle = Promise.all([\n walkLayoutChain(),\n getComponents({ hostId }),\n // Host variable + function bindings (AGL-91/93): {{name}} and\n // {{fn:name(args)}} in string props resolve to values; unknown tokens\n // and failed runs stay literal. Whatever computes a variable's value\n // starts its reads here too.\n Promise.all([\n getVariables({ hostId }),\n getFunctions({ hostId }),\n prepareComputedVariables(hostId),\n getPluginInstalls({ hostId }),\n ]),\n ])\n const screenNodes = await options.screenNodes\n const screenRepeatKeys = Aglyn.repeatKeys(screenNodes)\n const screenRepeatRowsPromise = screenRepeatKeys.length\n ? readRepeatRows({ hostId, keys: screenRepeatKeys })\n : undefined\n // Does the SCREEN itself place a form entity? Gated and re-asked exactly\n // like the repeat rows read beside it (AGL-1440): most pages carry no form, and\n // the ones that do usually say so on their own document, so the read goes\n // out here alongside the chrome reads instead of as a serial tail. It is not\n // the correctness gate — a placed form can arrive from a layout or a grafted\n // component — so the composed tree is asked again below.\n const screenFormsPromise = Aglyn.placesFormEntity(screenNodes)\n ? getForms({ hostId })\n : undefined\n // Issued HERE, beside the repeat rows read and before the chrome bundle is\n // awaited, so the collection read overlaps it instead of trailing it.\n const prefetchedSources = prefetchCollectionSources(\n hostId,\n screenNodes,\n options.collection,\n )\n const [layoutChain, componentsRes, bulk] = await chromeBundle\n const [rawVariables, functions, computeVariables, pluginInstalls] = bulk\n const screenRepeatRows = await screenRepeatRowsPromise\n // Settled by now: it is read off the same version document the layout\n // binding the walk above waited on came from.\n const layoutPropValues = await options.layoutPropValues\n const layoutStyleOverrides = await options.layoutStyleOverrides\n\n const composedNodes = Aglyn.composeLayoutChainWithProps(\n layoutChain as any,\n screenNodes as any,\n layoutPropValues,\n layoutStyleOverrides,\n )\n const graftedComponents = Aglyn.composeReusableComponentNodes(\n composedNodes as any,\n componentsRes.definitions as any,\n )\n /*\n * PLACED FORMS RESOLVE AGAINST THEIR ENTITY (`docs/specs/reusable-forms.md`).\n *\n * A form node bound to `hosts/{hostId}/forms/{formId}` renders that entity's\n * published design, so a form is edited once and every page placing it\n * follows. Without this the entity's tree was written on every publish and\n * read by nothing: the fields had to be redrawn per page, and the two copies\n * diverged the moment either was touched.\n *\n * The gate is the COMPONENT-grafted tree, not the screen's own nodes, for\n * the reason the repeatables gate below states: a form placed inside a\n * layout or a shared component does not exist in `screenNodes`, and a page\n * that renders one would silently keep its stale inline copy.\n *\n * The second graft re-runs the component expansion deliberately. Instances\n * already expanded are skipped by their own prefix, so the repeat costs a\n * scan, and passing BOTH placement kinds is what expands a reusable\n * component nested inside a form's design — which the first pass could not\n * have seen, because that subtree was not in the tree yet.\n */\n const forms =\n (await screenFormsPromise)?.forms ??\n (Aglyn.placesFormEntity(graftedComponents as any)\n ? (await getForms({ hostId })).forms\n : undefined)\n const grafted = forms\n ? Aglyn.composeReusableComponentNodes(\n graftedComponents as any,\n componentsRes.definitions as any,\n [Aglyn.placedFormPlacement(forms as any)],\n )\n : graftedComponents\n // Computed variables (AGL-129): the plugin that computes a variable's\n // value fills it in once per compose; a failure keeps its stored fallback.\n const variables = computeVariables({ variables: rawVariables, functions })\n // Repeatables (AGL-103) expand after grafting (so they work inside\n // reusable components) and before bindings (so {{name}} tokens inside\n // cloned items still resolve).\n //\n // Only the rows this tree repeats over are read (AGL-1440), and the tree\n // asked is the composed one — after grafting — because that is the map the\n // expansion reads: a repeatable living in a layout or a reusable component\n // is invisible in `screenNodes`, and reading only the screen's keys would\n // silently render one template row where the author put a list. The\n // screen's own keys were issued beside the chrome reads above; a key only a\n // layout or a component adds is read here, for that key alone.\n //\n // The rows come from the plugin that keeps them, through the repeat-rows\n // contract; this composition names no collection. A declared source whose\n // reader is missing THROWS rather than answering \"no rows\", so a broken boot\n // keeps the last good render instead of replacing every list with one row.\n const composedRepeatKeys = Aglyn.repeatKeys(grafted as any)\n const unreadRepeatKeys = composedRepeatKeys.filter(\n (key) => !screenRepeatKeys.includes(key),\n )\n const repeatRows = unreadRepeatKeys.length\n ? {\n ...screenRepeatRows,\n ...(await readRepeatRows({ hostId, keys: unreadRepeatKeys })),\n }\n : screenRepeatRows\n const repeated = Aglyn.expandRepeatables(grafted as any, repeatRows)\n // Collection entries blocks (AGL-551) expand alongside repeatables:\n // per-entry {{entry.*}} tokens substitute inside the clones here, while\n // page-level tokens wait for resolveNamedTokens below.\n const withEntries = await expandCollectionEntryBlocks(\n hostId,\n repeated,\n options.collection,\n prefetchedSources,\n options.timeZone,\n )\n // Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/\n // tags. Needs no source fetch — the routed entry and its taxonomy are\n // already in hand — so it sits outside `expandCollectionEntryBlocks`, which\n // returns early when no block asks for a collection read. AFTER it, so the\n // per-entry clones it just produced already carry their own resolved values\n // and are skipped.\n const withEntryMeta = Aglyn.expandCollectionEntryMeta(\n withEntries as any,\n options.collection?.entry,\n options.collection?.categories,\n options.timeZone,\n )\n // Entry Author cards (AGL-2486): the same fill, one block over. Its values\n // come off the author RECORD the routed entry resolved to, which the\n // collection read has already attached, so this costs nothing either.\n const withEntryAuthor = Aglyn.expandCollectionEntryAuthor(\n withEntryMeta as any,\n options.collection?.entry,\n )\n const bound = Aglyn.resolveNodesBindings(\n withEntryAuthor as any,\n variables,\n functions,\n )\n // Host variables (AGL-1022): `{{host.*}}` resolves from the INSTALLING\n // site, late — at render, never at install — so a rebrand propagates to\n // every artifact that names the host instead of hard-coding it. Same\n // registry the email path uses, so a token means one thing in both.\n const withHostTokens = Aglyn.resolveNodesHostTokens(\n bound as any,\n options.host,\n )\n // Function-running elements run client-side: embed their definitions\n // (AGL-93), and beside each the site variables that function reads and no\n // others (AGL-3202). Which elements those are is declared by their plugins\n // (AGL-3393), never named here.\n const withFunctions = Aglyn.attachFunctionDefinitions(\n withHostTokens,\n functions,\n variables,\n await functionBindingsFor(withHostTokens, hostId, functions),\n )\n // Marketplace plugins (AGL-45): stamp each marketplacePlugin node with its\n // pinned install (version/sha256/capabilities) + kill-switch state.\n const nodes = Aglyn.attachPluginInstalls(withFunctions, pluginInstalls)\n // Entry-template tokens (AGL-105): {{entry.*}} from the rendered entry.\n const finalNodes = Aglyn.resolveNamedTokens(nodes as any, options.tokens)\n // The document's one `main` landmark (AGL-2486). LAST, so it reads the tree\n // the page actually ships — a slot grafted from a layout chain, an element\n // an author chose — rather than the screen as stored.\n const withLandmark = Aglyn.stampDocumentLandmark(finalNodes as any)\n // Each form the record target writes for, stamped with what that target\n // trusts at submit time (AGL-2773) — a signed binding, today. Read off THIS\n // tree, the one the page ships, so a form grafted from a layout, a component\n // or a form entity is stamped as it renders; and after every stage that\n // rewrites props, so nothing changes what the stamp covers. A declared\n // target that is missing THROWS here rather than shipping forms that\n // silently write nowhere.\n const withFormBindings = await stampFormRecordTargets(\n withLandmark as Record<string, unknown>,\n hostId,\n )\n const denormalized = Aglyn.canvas.processNodesToDenormalized(\n withFormBindings as any,\n )\n /*\n * WHAT EACH PLACED IMAGE AND FILM IS NOW (AGL-2807, AGL-2833).\n *\n * An image's pixel pair and a film's length, shape and poster come from the\n * asset's DAM document as it is now, not as it was when it was picked. LAST,\n * on the tree the page ships, because an asset can arrive from any stage\n * above: a layout, a component, a form, a repeated row, a collection entry,\n * a binding.\n *\n * ONE read for all of them, issued once the tree is final rather than beside\n * the chrome reads. A read issued there could only cover the screen's own\n * placements, and nearly every layout places an image the screen does not\n * (a logo, a footer mark), so it would buy a second read on almost every\n * page. The late read costs its own round trip after the chrome reads, paid\n * each time a page is composed, which for a published page is its ISR\n * regeneration. A tree with no library asset, on a page whose social card\n * names none, issues none.\n *\n * THE SOCIAL CARD'S ASSETS JOIN THE SAME READ (AGL-2850), ahead of the\n * placements. A placement the cap leaves out keeps its pick-time pair, which\n * for an image is a reservation the decoded picture corrects. Nothing\n * corrects a card's pair, because a crawler lays the card out from it before\n * fetching the image, and a card names at most three documents.\n */\n const socialImages = options.socialImages\n const cardRefs = socialImages ? socialImageRefs(socialImages.images) : []\n const refs = mediaAssetRefs(denormalized)\n if (!refs.length && !cardRefs.length) return denormalized\n const facts = await getMediaAssetFacts({\n hostId,\n refs: [...cardRefs, ...refs],\n })\n if (socialImages) {\n const cardFacts = socialImageAssetFacts(socialImages.images, facts)\n if (cardFacts) socialImages.onFacts(cardFacts)\n }\n return applyMediaAssetFacts(denormalized, facts)\n}\n\n/**\n * What the caller knows about the page it is composing, for the page review\n * to NAME it when it holds or flags it (AGL-3374). Never read to decide\n * whether the page serves.\n */\nexport interface ComposePageContext {\n /**\n * A template composed against a routed listing or entry: the route pattern\n * it serves (`/videos`, `/videos/:slug`), the collection it renders, the\n * address being shown, and what a visitor gets when it has no clean\n * version (`built-in-design` for a content collection, whose routes fall\n * back to it; `not-found` for a store template, which has none).\n */\n template?: {\n role: 'list' | 'entry'\n route: string\n collectionName?: string | null\n entryPath?: string | null\n fallback?: 'built-in-design' | 'not-found'\n } | null\n /** An experiment variant's version of the page. */\n variant?: { experimentId: string; variantId: string; name?: string | null } | null\n}\n\n/**\n * The page's own nodes and those of every layout and reusable component\n * composed into it, for the page review to say WHICH one carries flagged\n * content (AGL-3374). Read only when a review row is filed: the layout\n * versions and the component definitions are the cached reads the compose\n * already made.\n */\nasync function composedPageParts(options: {\n hostId: string\n screenId: string\n screenNodes: Record<string, any>\n layoutId: string | null | undefined\n}): Promise<HostedPagePart[]> {\n const parts: HostedPagePart[] = [\n { type: 'screen', id: options.screenId, nodes: options.screenNodes },\n ]\n const seen = new Set<string>()\n let layoutId = options.layoutId ? String(options.layoutId) : undefined\n while (layoutId && !seen.has(layoutId) && seen.size < Aglyn.MAX_LAYOUT_CHAIN_DEPTH) {\n seen.add(layoutId)\n const layoutRes = await getPublishedLayoutVersion({ hostId: options.hostId, layoutId })\n const layout = layoutRes?.layout as { displayName?: unknown; layoutId?: unknown } | undefined\n parts.push({\n type: 'layout',\n id: layoutId,\n name: typeof layout?.displayName === 'string' ? layout.displayName : null,\n nodes: layoutRes?.version?.nodes ?? {},\n })\n layoutId = layout?.layoutId ? String(layout.layoutId) : undefined\n }\n // Only the components this page actually renders, followed through\n // components that nest others, so an unused one is never blamed.\n const { definitions } = await getComponents({ hostId: options.hostId })\n const used = new Set<string>()\n let frontier = parts.map((part) => part.nodes as Record<string, any>)\n while (frontier.length) {\n const next: Record<string, any>[] = []\n for (const [id, definition] of Object.entries(definitions)) {\n if (used.has(id)) continue\n if (frontier.some((nodes) => Aglyn.nodesReferenceComponent(nodes, id))) {\n used.add(id)\n next.push(definition.nodes as Record<string, any>)\n parts.push({ type: 'component', id, nodes: definition.nodes })\n }\n }\n frontier = next\n }\n return parts\n}\n\n/**\n * Full published-render composition for one screen (extracted for AGL-87 so\n * the SSG path and the password-unlock API build identical trees): applies\n * a due publish schedule, loads the version, composes the shared layout\n * chrome, grafts reusable components, and denormalizes.\n */\nexport async function composeScreenNodes(options: {\n hostId: string\n screenId: string\n screen: Aglyn.AglynScreen\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * Compose a specific version instead of the published one (AGL-253):\n * experiment variants point at versions; schedules don't apply.\n */\n versionId?: string\n /** The host document, for `host.*` tokens (AGL-1022). */\n host?: Aglyn.HostTokenSource | null\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /** The social card the head shares this page as (AGL-2850). */\n socialImages?: ComposeSocialImages\n /**\n * Internal: `false` when this compose IS the fallback to a held page's\n * last clean version, so a fallback that holds too serves nothing rather\n * than falling back again.\n */\n pageReviewFallback?: boolean\n /** What this page is, for naming it if the page review holds it (AGL-3374). */\n page?: ComposePageContext\n}): Promise<Record<string, any> | null> {\n const { hostId, screenId, screen } = options\n\n const effectiveVersionId = options.versionId\n ? null\n : await applyDuePublishSchedule({\n hostId,\n collectionName: 'screens',\n docId: screenId,\n parent: screen,\n })\n const versionId = (options.versionId ??\n effectiveVersionId ??\n screen.versionId) as string\n\n /*\n * OVERLAP THE VERSION READ WITH THE CHROME BUNDLE (AGL-1428).\n *\n * `composeNodesWithChrome`'s reads are keyed by `hostId`/`layoutId`, both\n * of which are in hand here, so the version read no longer has to finish\n * before they start. Handing the promise over instead of the resolved\n * value is the whole change: it is `composeNodesWithChrome` that decides\n * when it actually needs the nodes.\n *\n * The `versionId`-less screen still exits BEFORE anything is issued, so a\n * screen that has never been published costs no reads. What remains is the\n * narrow case of a `versionId` that points at a missing or unreadable\n * version document — a data-integrity fault rather than a routing outcome\n * — and that one now pays for a chrome bundle it discards. That is the\n * deliberate trade: one wasted bundle on a broken screen, against the\n * version read hiding under the chrome reads on every render that works.\n */\n if (!versionId) return null\n\n const versionPromise = getScreenVersion({ hostId, screenId, versionId })\n\n /*\n * NEITHER DERIVED PROMISE MAY REJECT ON ITS OWN.\n *\n * `composed` is discarded on every failure path below, and a rejected\n * promise nobody awaited is an unhandled rejection — which in Node takes\n * the whole render process down instead of letting this 404. So the nodes\n * handed to the compose absorb the failure (an empty tree it will never be\n * asked for), and `composed` gets a rejection handler attached the moment\n * it exists rather than at the point we decide to drop it. The real error\n * still propagates, from the `await versionPromise` below, exactly where it\n * did when this function awaited the version directly.\n */\n const composed = composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n // Version-first (key-present wins, null = explicitly no layout), screen\n // fallback — resolved as a promise so only the layout-chain walk waits on\n // the version read; the rest of the chrome bundle keeps the AGL-1428\n // overlap. A failed version read falls back to the screen binding; the\n // whole compose is discarded on that path anyway.\n layoutId: versionPromise.then(\n (res) =>\n res.version && 'layoutId' in res.version\n ? ((res.version as Aglyn.AglynScreenVersion).layoutId as\n | string\n | null)\n : (screen.layoutId as string | undefined),\n () => screen.layoutId as string | undefined,\n ),\n // The screen's values for its layouts' properties (AGL-2893), beside the\n // binding on the same document; a failed read renders the defaults.\n layoutPropValues: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)?.layoutPropValues,\n () => undefined,\n ),\n // …and its per-page restyling of their elements (AGL-3286), from the same\n // document; a failed read renders the layouts' own styling.\n layoutStyleOverrides: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)\n ?.layoutStyleOverrides,\n () => undefined,\n ),\n screenNodes: versionPromise.then(\n (res) => (res.version?.nodes ?? {}) as any,\n () => ({}) as any,\n ),\n tokens: options.tokens,\n collection: options.collection,\n host: options.host,\n socialImages: options.socialImages,\n })\n void composed.catch(() => undefined)\n\n const versionRes = await versionPromise\n if (versionRes.error || !versionRes.version) return null\n\n /*\n * THE PAGE PHISHING REVIEW (AGL-3356).\n *\n * Every way a page goes live — a toolbar publish, a versions-panel\n * publish, a scheduled publish, a component or form promoted under it, an\n * experiment variant, an edit to the live version in place — meets here,\n * with the page exactly as a visitor will receive it. So this is where a\n * page that links to a brand's lookalike, asks for a password or a card\n * in a field of its own, or (for a new workspace) dresses a brand's\n * sign-in is held rather than served. See `hosted-page-review.ts`.\n *\n * A held version serves the last version this page served clean, screened\n * again, or nothing.\n */\n const nodes = await composed\n const version = versionRes.version as Aglyn.AglynScreenVersion\n const review = await reviewHostedPage({\n hostId,\n screenId,\n versionId,\n nodes,\n // Named by what it is, not only its id (AGL-3374).\n page: {\n screen: {\n displayName: (screen as { displayName?: unknown }).displayName,\n name: (screen as { name?: unknown }).name,\n kind: (screen as { kind?: unknown }).kind,\n },\n template: options.page?.template ?? null,\n variant: options.page?.variant ?? null,\n parts: () =>\n composedPageParts({\n hostId,\n screenId,\n screenNodes: (version?.nodes ?? {}) as Record<string, any>,\n layoutId:\n version && 'layoutId' in version\n ? (version.layoutId as string | null)\n : (screen.layoutId as string | undefined),\n }),\n },\n })\n if (review.outcome === 'serve') {\n // Only the PUBLISHED version is the page's last clean one; a variant or\n // a pinned version is not what the page serves.\n if (nodes && !options.versionId) {\n await recordServedPageVersion(hostId, screenId, versionId)\n }\n return nodes\n }\n if (options.pageReviewFallback === false) return null\n const previous = await servedPageVersion(hostId, screenId)\n if (!previous || previous === versionId) return null\n return composeScreenNodes({\n ...options,\n versionId: previous,\n pageReviewFallback: false,\n })\n}\n\nexport default composeScreenNodes\n"],"names":["Aglyn","applyMediaAssetFacts","mediaAssetRefs","recordServedPageVersion","reviewHostedPage","servedPageVersion","stampFormRecordTargets","readRepeatRows","prepareComputedVariables","applyDuePublishSchedule","getComponents","getForms","getMediaAssetFacts","getPublishedCollectionSource","getPluginInstalls","getVariables","getFunctions","getPublishedLayoutVersion","getScreenVersion","socialImageAssetFacts","socialImageRefs","scanCollectionBlocks","nodes","collection","slugs","Set","hasRelated","hasCategories","hasSearch","node","Object","values","componentId","COLLECTION_ENTRIES_COMPONENT_ID","slug","String","props","collectionSlug","trim","add","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","entry","prefetchCollectionSources","hostId","screenNodes","prefetched","entries","pending","catch","undefined","expandCollectionEntryBlocks","timeZone","size","sources","Promise","all","map","categories","page","entriesReachedBound","reachedBound","windowStart","fetched","expanded","expandCollectionEntries","withCategories","expandCollectionCategories","routeless","categorySlug","withSearch","expandCollectionSearch","expandCollectionRelated","functionBindingsFor","functions","firstParty","FIRST_PARTY_FUNCTION_BINDINGS","keys","length","placesMarketplaceElement","some","isNamespacedComponentId","dev","process","env","NODE_ENV","devPluginManifests","getRealmPluginInstalls","installs","mergeFunctionBindings","error","console","composeNodesWithChrome","options","layoutId","walkLayoutChain","chain","seen","boundLayoutId","currentLayoutId","has","MAX_LAYOUT_CHAIN_DEPTH","layoutRes","push","version","parentId","layout","chromeBundle","screenRepeatKeys","repeatKeys","screenRepeatRowsPromise","screenFormsPromise","placesFormEntity","prefetchedSources","layoutChain","componentsRes","bulk","rawVariables","computeVariables","pluginInstalls","screenRepeatRows","layoutPropValues","layoutStyleOverrides","composedNodes","composeLayoutChainWithProps","graftedComponents","composeReusableComponentNodes","definitions","forms","grafted","placedFormPlacement","variables","composedRepeatKeys","unreadRepeatKeys","filter","key","includes","repeatRows","repeated","expandRepeatables","withEntries","withEntryMeta","expandCollectionEntryMeta","withEntryAuthor","expandCollectionEntryAuthor","bound","resolveNodesBindings","withHostTokens","resolveNodesHostTokens","host","withFunctions","attachFunctionDefinitions","attachPluginInstalls","finalNodes","resolveNamedTokens","tokens","withLandmark","stampDocumentLandmark","withFormBindings","denormalized","canvas","processNodesToDenormalized","socialImages","cardRefs","images","refs","facts","cardFacts","onFacts","composedPageParts","parts","type","id","screenId","name","displayName","used","frontier","part","next","definition","nodesReferenceComponent","composeScreenNodes","screen","effectiveVersionId","versionId","collectionName","docId","parent","versionPromise","composed","then","res","versionRes","review","kind","template","variant","outcome","pageReviewFallback","previous"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/compose-screen-nodes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\n// By path: the overlay is server-only, and every `@aglyn/aglyn` barrel\n// re-exports `app-utils/server` into published pages.\nimport {\n applyMediaAssetFacts,\n mediaAssetRefs,\n} from '@aglyn/aglyn/app-utils/media-asset-facts'\nimport {\n recordServedPageVersion,\n reviewHostedPage,\n servedPageVersion,\n} from '@aglyn/tenant-data-admin/server/hosted-page-review'\nimport type { HostedPagePart } from '@aglyn/tenant-data-admin/server/held-page-subject'\n// By path: the contracts a repeat's rows and a form's record target are\n// asked through, both server-only.\nimport { stampFormRecordTargets } from '@aglyn/aglyn/plugin-manager/submission-record-target'\nimport { readRepeatRows } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { prepareComputedVariables } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport applyDuePublishSchedule from './apply-publish-schedule'\nimport getComponents from './get-components'\nimport getForms from './get-forms'\nimport getMediaAssetFacts from './get-media-asset-facts'\nimport {\n getPublishedCollectionSource,\n type PublishedCollectionSource,\n} from './get-collection-content'\nimport getPluginInstalls from './get-plugin-installs'\nimport getVariables, { getFunctions } from './get-variables'\nimport getPublishedLayoutVersion from './get-layout-version'\nimport getScreenVersion from './get-screen-version'\nimport {\n type ComposeSocialImages,\n socialImageAssetFacts,\n socialImageRefs,\n} from './social-image-facts'\n\n/**\n * Content-collection context for a compose (AGL-551): the collection the\n * route resolved (list/entry template screens). `entries` rides along when\n * the route already fetched them (list pages); blocks bound to other\n * collections — or to this one when `entries` is absent — fetch on demand.\n */\nexport interface ComposeCollectionContext {\n slug: string\n entries?: Aglyn.CollectionEntryRecord[]\n /**\n * The entry being rendered (AGL-582, entry-template screens / entry\n * fallback) — the Related posts block resolves against it.\n */\n entry?: Aglyn.CollectionEntryRecord | null\n /**\n * The routed collection's category taxonomy (AGL-582): entry\n * `categoryId`s resolve to display names against it during expansion.\n */\n categories?: Aglyn.CollectionCategory[]\n /**\n * The list page the URL asked for (AGL-1321). Fills in an entries block\n * that declares `perPage` but no `page` — design time cannot know which\n * page a visitor is on.\n */\n page?: number\n /**\n * The category segment the URL filtered on (AGL-1321); marks the current\n * pill in a Category Pills block. `entries` arrives already filtered.\n */\n categorySlug?: string\n /**\n * Whether the read behind `entries` stopped at its `.limit()` (AGL-1516).\n * Travels with the entries because the search boxes downstream have to say\n * what they actually searched, and — with the liveness gate and the route's\n * category filter both in between — `entries.length` no longer tells them.\n */\n entriesReachedBound?: boolean\n /**\n * Where `entries` begins in the collection's own order (AGL-3213).\n *\n * Zero for every listing served from the cached read. A page past that\n * read's bound carries only its own ten entries, and the block windows\n * `[(page - 1) * perPage, …)` — so without this it would slice from an\n * offset its own array never reaches and render an empty listing.\n */\n windowStart?: number\n /**\n * Whether {@link slug} is a cache KEY rather than an address (AGL-2524).\n *\n * The author page mixes collections, and the compose pipeline keys entry\n * sources by collection slug — so it hands over a synthetic one\n * (`AUTHOR_ENTRIES_SOURCE_SLUG`) with its entries already in hand. That is\n * harmless for the entries block, whose every row carries its OWN\n * `collectionSlug` and builds `entry.url` from it, and for the search box,\n * whose index is built the same way.\n *\n * It is NOT harmless for anything that builds a URL from the source's slug\n * itself. Set this and such a block resolves nothing rather than pointing\n * readers at `/{synthetic}/…`.\n */\n routeless?: boolean\n}\n\ninterface CollectionBlockScan {\n slugs: Set<string>\n hasRelated: boolean\n hasCategories: boolean\n hasSearch: boolean\n}\n\n/**\n * Which collections does this tree ask for (AGL-1152)?\n *\n * Extracted so the PREFETCH below and the expansion that consumes it read the\n * tree through one function rather than two copies of the same predicate. A\n * divergence between them is not a type error — it is a slug prefetched and\n * never awaited, or (worse) a slug the prefetch missed that then pays the full\n * serial read anyway — so there is deliberately no second implementation to\n * drift.\n */\nfunction scanCollectionBlocks(\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): CollectionBlockScan {\n const slugs = new Set<string>()\n let hasRelated = false\n let hasCategories = false\n let hasSearch = false\n for (const node of Object.values(nodes)) {\n if (node?.componentId === Aglyn.COLLECTION_ENTRIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) slugs.add(slug)\n }\n // Category pills (AGL-1321) need only the taxonomy, but it rides on the\n // same source, so they count as a reason to resolve one.\n if (node?.componentId === Aglyn.COLLECTION_CATEGORIES_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasCategories = true\n slugs.add(slug)\n }\n }\n // The standalone toolbar search box (AGL-1516, Figma 494:1220) needs the\n // collection's entries to index, and rides the same source the listing\n // beside it was built from — one read, one answer.\n if (node?.componentId === Aglyn.COLLECTION_SEARCH_COMPONENT_ID) {\n const slug =\n String(node?.props?.collectionSlug ?? '').trim() || collection?.slug\n if (slug) {\n hasSearch = true\n slugs.add(slug)\n }\n }\n // Related posts (AGL-582) always resolve against the ROUTED collection\n // — they only mean something with a current entry in context.\n if (\n node?.componentId === Aglyn.COLLECTION_RELATED_COMPONENT_ID &&\n collection?.slug &&\n collection.entry\n ) {\n hasRelated = true\n slugs.add(collection.slug)\n }\n }\n return { slugs, hasRelated, hasCategories, hasSearch }\n}\n\n/**\n * Issue the collection reads AS SOON AS THE SCREEN NODES EXIST (AGL-1152).\n *\n * `getPublishedCollectionSource` is three SEQUENTIAL round trips —\n * `findContentCollection`, then `listLiveEntries`, then `attachEntryAuthors`\n * — and until this existed it ran after the chrome bundle had already been\n * awaited, so a page carrying a Collection entries block paid the whole thing\n * as a serial tail on the compose phase. Measured on the tenant: a page with\n * no collection block composes in ~20 ms, one with a block in ~572 ms, and the\n * tree work itself accounts for under 2 ms of that at 50 entries. The gap is\n * this read, waiting for reads it shares nothing with.\n *\n * Same shape as `screenRepeatRowsPromise` directly below, and the same caveat\n * applies: the SCREEN's own nodes are a fast path, NOT the correctness gate. A\n * collection block can arrive from a layout or a grafted reusable component,\n * neither of which exists yet at this point, so the real scan still runs\n * against the composed tree and still fetches anything this missed.\n *\n * The ROUTED collection is deliberately excluded when its entries are already\n * in hand: the expansion below answers that slug from `collection.entries`\n * without reading at all, so prefetching it would buy a read nobody awaits.\n */\nfunction prefetchCollectionSources(\n hostId: string,\n screenNodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n): Record<string, Promise<PublishedCollectionSource>> {\n const prefetched: Record<\n string,\n Promise<PublishedCollectionSource>\n > = {}\n for (const slug of scanCollectionBlocks(screenNodes, collection).slugs) {\n if (slug === collection?.slug && collection.entries) continue\n const pending = getPublishedCollectionSource({\n hostId,\n collectionSlug: slug,\n })\n // Marked handled the moment it exists, for the reason `composed` is in\n // `composeScreenNodes`: a tree that turns out not to need this slug never\n // awaits it, and an unawaited rejection takes the process down rather than\n // failing this one read. The real await below still sees the rejection.\n void pending.catch(() => undefined)\n prefetched[slug] = pending\n }\n return prefetched\n}\n\n/**\n * Expands Collection entries blocks (AGL-551) against their collections'\n * published entries, and Related posts blocks (AGL-582) against the routed\n * entry. Fetches lazily — screens without the blocks cost nothing — and\n * fails open on lookup errors like every other compose stage.\n */\nasync function expandCollectionEntryBlocks(\n hostId: string,\n nodes: Record<string, any>,\n collection?: ComposeCollectionContext,\n prefetched?: Record<string, Promise<PublishedCollectionSource>>,\n /** The site's zone (AGL-3237); UTC when a site has not named one. */\n timeZone?: string,\n): Promise<Record<string, any>> {\n const { slugs, hasRelated, hasCategories, hasSearch } = scanCollectionBlocks(\n nodes,\n collection,\n )\n if (!slugs.size) return nodes\n const sources: Record<string, Aglyn.CollectionEntriesSource> = {}\n await Promise.all(\n [...slugs].map(async (slug) => {\n // The routed collection rides its already-fetched entries +\n // categories (AGL-582); other collections fetch both on demand.\n if (slug === collection?.slug && collection.entries) {\n sources[slug] = {\n slug,\n entries: collection.entries,\n categories: collection.categories,\n // Only the ROUTED collection carries the URL's page (AGL-1321) — a\n // block bound to another collection must not inherit it.\n ...(collection.page ? { page: collection.page } : {}),\n // ...nor its bound (AGL-1516): this is a fact about the read the\n // route already performed, and it describes only that collection.\n ...(collection.entriesReachedBound ? { reachedBound: true } : {}),\n // ...nor where its window starts (AGL-3213), for the same reason:\n // it is a fact about the read this route performed.\n ...(collection.windowStart\n ? { windowStart: collection.windowStart }\n : {}),\n }\n return\n }\n // The prefetch when this slug was visible on the screen's own nodes;\n // a live read when it only appeared after layout/component grafting.\n const fetched = await (prefetched?.[slug] ??\n getPublishedCollectionSource({ hostId, collectionSlug: slug }))\n sources[slug] = {\n slug,\n entries: fetched.entries,\n categories:\n slug === collection?.slug && collection.categories\n ? collection.categories\n : fetched.categories,\n ...(fetched.reachedBound ? { reachedBound: true } : {}),\n }\n }),\n )\n const expanded = Aglyn.expandCollectionEntries(\n nodes,\n sources,\n collection?.slug,\n timeZone,\n )\n const withCategories = hasCategories\n ? Aglyn.expandCollectionCategories(\n expanded,\n sources,\n /*\n No DEFAULT collection for the pills on a routeless page (AGL-2524).\n\n Category pills are the one block that builds its links from the\n SOURCE's slug — `/{slug}/category/{x}` — because a category is a\n filter on one collection's own listing. On the author page that slug\n is synthetic, so an unbound pills block stamped links to a route that\n does not exist.\n\n Rendering nothing is the honest answer rather than a fallback. The\n page spans every collection, and there is no\n `/author/{slug}/category/{x}` for a pill to lead to; the only real\n destination would be some collection's listing, which drops the\n author the reader is looking at. A pills block that NAMES a\n collection is unaffected and still resolves that collection's own\n taxonomy — \"browse the blog by category\", which is a sensible thing\n to put beside an archive.\n */\n collection?.routeless ? undefined : collection?.slug,\n collection?.categorySlug,\n )\n : expanded\n const withSearch = hasSearch\n ? Aglyn.expandCollectionSearch(\n withCategories,\n sources,\n collection?.slug,\n timeZone,\n )\n : withCategories\n if (!hasRelated || !collection?.entry) return withSearch\n return Aglyn.expandCollectionRelated(\n withSearch,\n sources[collection.slug],\n collection.entry,\n timeZone,\n )\n}\n\n/**\n * Shared post-version composition (AGL-551, extracted from\n * `composeScreenNodes`): layout chrome, reusable components, repeatables,\n * collection entries, bindings, function definitions, plugin installs,\n * named tokens, denormalize, and last the current facts of each placed image\n * and film, and of the social card the page is shared as. The screen path and\n * the collection-fallback\n * path (which has no screen doc) build identical trees through this one\n * pipeline.\n */\n/**\n * The function bindings that apply to a composed tree (AGL-3393).\n *\n * First-party declarations are compiled in and cost nothing. A marketplace\n * plugin's live in its pinned version's manifest, which rides on the host's\n * realm installs — a read (render-cached, and the same one the page loader\n * issues for the bundles), so it is made only when it could change the\n * answer: the host has a function to hand out and the tree places an element\n * in a marketplace namespace. First-party declarations are listed first, so\n * no manifest can rebind a first-party element.\n *\n * A failed lookup binds first-party elements only. A marketplace element then\n * renders without its definition, which is its unconfigured placeholder —\n * the same fail-open the bundle lookup takes.\n */\nasync function functionBindingsFor(\n nodes: Record<string, { componentId?: unknown } | null | undefined>,\n hostId: string,\n functions: Record<string, unknown>,\n): Promise<Aglyn.FunctionBindings> {\n const firstParty = Aglyn.FIRST_PARTY_FUNCTION_BINDINGS\n if (!Object.keys(functions).length) return firstParty\n const placesMarketplaceElement = Object.values(nodes).some((node) =>\n Aglyn.isNamespacedComponentId(node?.componentId),\n )\n if (!placesMarketplaceElement) return firstParty\n // The dev loop's plugins have no install, so their manifests are read from\n // disk (AGL-3394). A production build never reaches the module.\n const dev =\n process.env.NODE_ENV === 'production'\n ? []\n : await (await import('./dev-plugin-manifests')).devPluginManifests()\n try {\n const { getRealmPluginInstalls } = await import('./realm-installs-seam')\n const installs = await getRealmPluginInstalls({ hostId })\n return Aglyn.mergeFunctionBindings(firstParty, ...installs, ...dev)\n } catch (error) {\n console.error('function bindings: realm install lookup failed:', error)\n return Aglyn.mergeFunctionBindings(firstParty, ...dev)\n }\n}\n\nexport async function composeNodesWithChrome(options: {\n hostId: string\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /**\n * The layout binding, or a PROMISE of it.\n *\n * The unresolved form exists for the same reason `screenNodes` accepts one\n * (AGL-1428): the binding may live on the version document (key-present\n * wins over the screen's), and awaiting the version before this call would\n * put every host-scoped read back behind it. Only the layout-chain walk\n * consumes the binding, so it alone waits; the rest of the chrome bundle\n * still starts immediately.\n */\n layoutId?:\n | string\n | null\n | Promise<string | null | undefined>\n /**\n * The screen's values for the properties of the layouts it renders inside\n * (AGL-2893), keyed by layout id — or a PROMISE of them, for the reason\n * `layoutId` accepts one: they live on the version document beside the\n * binding. Absent, every layout renders its properties' defaults.\n */\n layoutPropValues?:\n | Aglyn.AglynScreenVersion['layoutPropValues']\n | null\n | Promise<Aglyn.AglynScreenVersion['layoutPropValues'] | null | undefined>\n /**\n * The screen's per-page restyling of its layouts' elements (AGL-3286),\n * keyed by layout id then layout node id — or a PROMISE of it, for the same\n * reason as `layoutPropValues`, beside which it is stored. Absent, every\n * layout renders with its own styling.\n */\n layoutStyleOverrides?:\n | Aglyn.AglynScreenVersion['layoutStyleOverrides']\n | null\n | Promise<\n Aglyn.AglynScreenVersion['layoutStyleOverrides'] | null | undefined\n >\n /**\n * The screen's own nodes, or a PROMISE of them (AGL-1428).\n *\n * Accepting the unresolved form is what lets `composeScreenNodes` hand the\n * version read over before it has finished, so the host-scoped reads below\n * — none of which look at these nodes — overlap it instead of queueing\n * behind it. Passing a resolved value behaves exactly as before, which is\n * what every other caller does.\n */\n screenNodes: Record<string, any> | Promise<Record<string, any>>\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * The host document, for `host.*` tokens (AGL-1022).\n *\n * Passed in rather than read here: every caller already holds it, and\n * `composeScreenNodes` was the largest single render phase before AGL-1225\n * cut it to one round trip — adding a read back would spend that win on\n * data we already have in hand.\n */\n host?: Aglyn.HostTokenSource | null\n /**\n * The social card the head shares this page as (AGL-2850). The documents of\n * the assets it names are read in the same batch as the images and films\n * the tree places.\n */\n socialImages?: ComposeSocialImages\n}): Promise<Record<string, any>> {\n const { hostId, layoutId } = options\n\n /**\n * The layout chain, innermost first (AGL-703).\n *\n * A layout may itself render inside another layout, so this walks the\n * `layoutId` pointers rather than reading one. Fetching is sequential\n * because each step's parent is only known once the previous layout\n * document is in hand — but the walk is short by construction\n * (MAX_LAYOUT_CHAIN_DEPTH) and every layout is already a cached read.\n *\n * `seen` stops a cycle from looping forever. Stored data can hold one\n * even though both the console and `canNestLayout` refuse to create it:\n * the API, a script, or a restored backup can all write a layout\n * document directly, and a render must degrade rather than hang.\n */\n const walkLayoutChain = async () => {\n const chain: Aglyn.LayoutChainEntry[] = []\n const seen = new Set<string>()\n const boundLayoutId = await layoutId\n let currentLayoutId = boundLayoutId ? String(boundLayoutId) : undefined\n while (\n currentLayoutId &&\n !seen.has(currentLayoutId) &&\n chain.length < Aglyn.MAX_LAYOUT_CHAIN_DEPTH\n ) {\n seen.add(currentLayoutId)\n const layoutRes = await getPublishedLayoutVersion({\n hostId,\n layoutId: currentLayoutId,\n })\n // The version's declared properties travel with its nodes (AGL-2893):\n // they are applied to this layout alone, with the screen's values for\n // this layout, before the screen is grafted into its slot.\n chain.push({\n layoutId: currentLayoutId,\n nodes: layoutRes?.version?.nodes as any,\n props: (layoutRes?.version as Aglyn.AglynLayoutVersion | undefined)\n ?.props,\n })\n const parentId = (layoutRes?.layout as any)?.layoutId\n currentLayoutId = parentId ? String(parentId) : undefined\n }\n return chain\n }\n\n /**\n * ONE round-trip stage instead of three (AGL-1225).\n *\n * This used to be `await layout walk` → `await getComponents` → `await\n * Promise.all([five reads])`: three sequential waits, where only the first\n * has any reason to be sequential. Every one of the other six reads takes\n * `hostId` and nothing else — none of them consumes the layout chain — so\n * they were waiting on a walk whose result they never look at.\n *\n * The walk stays internally sequential because it genuinely is: each step's\n * parent id is only known once the previous layout document is in hand. It\n * just no longer gates anything else. The critical path becomes the walk\n * alone rather than walk + components + bulk.\n *\n * Measured budget that motivated this (production, `/product/besigner`):\n * `composeScreenNodes` was the largest single phase at 1577 ms cold and\n * consistently ~1.4-1.6 s warm too, so this is not cold-start cost. The\n * existing `AGL-1152:render` timing line reports `composeScreenNodes` as a\n * phase, so the effect of this shows up there directly — no new\n * instrumentation, and a regression would be visible in the same place.\n */\n /**\n * Does the SCREEN itself repeat (AGL-1440)?\n *\n * Asked as soon as the nodes are in hand — which since AGL-1428 is after\n * the fan-out has been ISSUED rather than before — so the overwhelmingly\n * common case still keeps the AGL-1225 shape: a page that repeats almost\n * always says so on its own document, and its rows read goes out\n * alongside the remaining chrome reads instead of after them.\n *\n * It is deliberately NOT the correctness gate — a repeatable can arrive from\n * a layout or a grafted reusable component, neither of which exists yet. The\n * gate is re-asked against the composed tree after grafting, which is the\n * exact input `expandRepeatables` reads.\n */\n /*\n * ISSUE THE HOST-SCOPED READS FIRST, THEN AWAIT THE NODES (AGL-1428).\n *\n * Every read in this bundle is keyed by `hostId` (and `layoutId`) alone —\n * none of them reads `screenNodes` — so starting them before the nodes are\n * in hand lets `composeScreenNodes`' version read overlap them rather than\n * run ahead of them. `Promise.all` is created BEFORE the `await` below on\n * purpose: that is the line that makes the two independent, and moving the\n * await above it silently gives the whole saving back.\n *\n * The repeat rows are the one read that genuinely depends on the nodes, so\n * they cannot join the bundle. It does not have to wait for the bundle either —\n * it is issued the moment the nodes resolve and awaited after, so it still\n * overlaps whatever is left of the chrome reads instead of costing the\n * extra serial round trip that dropping it from the batch would imply.\n */\n const chromeBundle = Promise.all([\n walkLayoutChain(),\n getComponents({ hostId }),\n // Host variable + function bindings (AGL-91/93): {{name}} and\n // {{fn:name(args)}} in string props resolve to values; unknown tokens\n // and failed runs stay literal. Whatever computes a variable's value\n // starts its reads here too.\n Promise.all([\n getVariables({ hostId }),\n getFunctions({ hostId }),\n prepareComputedVariables(hostId),\n getPluginInstalls({ hostId }),\n ]),\n ])\n const screenNodes = await options.screenNodes\n const screenRepeatKeys = Aglyn.repeatKeys(screenNodes)\n const screenRepeatRowsPromise = screenRepeatKeys.length\n ? readRepeatRows({ hostId, keys: screenRepeatKeys })\n : undefined\n // Does the SCREEN itself place a form entity? Gated and re-asked exactly\n // like the repeat rows read beside it (AGL-1440): most pages carry no form, and\n // the ones that do usually say so on their own document, so the read goes\n // out here alongside the chrome reads instead of as a serial tail. It is not\n // the correctness gate — a placed form can arrive from a layout or a grafted\n // component — so the composed tree is asked again below.\n const screenFormsPromise = Aglyn.placesFormEntity(screenNodes)\n ? getForms({ hostId })\n : undefined\n // Issued HERE, beside the repeat rows read and before the chrome bundle is\n // awaited, so the collection read overlaps it instead of trailing it.\n const prefetchedSources = prefetchCollectionSources(\n hostId,\n screenNodes,\n options.collection,\n )\n const [layoutChain, componentsRes, bulk] = await chromeBundle\n const [rawVariables, functions, computeVariables, pluginInstalls] = bulk\n const screenRepeatRows = await screenRepeatRowsPromise\n // Settled by now: it is read off the same version document the layout\n // binding the walk above waited on came from.\n const layoutPropValues = await options.layoutPropValues\n const layoutStyleOverrides = await options.layoutStyleOverrides\n\n const composedNodes = Aglyn.composeLayoutChainWithProps(\n layoutChain as any,\n screenNodes as any,\n layoutPropValues,\n layoutStyleOverrides,\n )\n const graftedComponents = Aglyn.composeReusableComponentNodes(\n composedNodes as any,\n componentsRes.definitions as any,\n )\n /*\n * PLACED FORMS RESOLVE AGAINST THEIR ENTITY (`docs/specs/reusable-forms.md`).\n *\n * A form node bound to `hosts/{hostId}/forms/{formId}` renders that entity's\n * published design, so a form is edited once and every page placing it\n * follows. Without this the entity's tree was written on every publish and\n * read by nothing: the fields had to be redrawn per page, and the two copies\n * diverged the moment either was touched.\n *\n * The gate is the COMPONENT-grafted tree, not the screen's own nodes, for\n * the reason the repeatables gate below states: a form placed inside a\n * layout or a shared component does not exist in `screenNodes`, and a page\n * that renders one would silently keep its stale inline copy.\n *\n * The second graft re-runs the component expansion deliberately. Instances\n * already expanded are skipped by their own prefix, so the repeat costs a\n * scan, and passing BOTH placement kinds is what expands a reusable\n * component nested inside a form's design — which the first pass could not\n * have seen, because that subtree was not in the tree yet.\n */\n const forms =\n (await screenFormsPromise)?.forms ??\n (Aglyn.placesFormEntity(graftedComponents as any)\n ? (await getForms({ hostId })).forms\n : undefined)\n const grafted = forms\n ? Aglyn.composeReusableComponentNodes(\n graftedComponents as any,\n componentsRes.definitions as any,\n [Aglyn.placedFormPlacement(forms as any)],\n )\n : graftedComponents\n // Computed variables (AGL-129): the plugin that computes a variable's\n // value fills it in once per compose; a failure keeps its stored fallback.\n const variables = computeVariables({ variables: rawVariables, functions })\n // Repeatables (AGL-103) expand after grafting (so they work inside\n // reusable components) and before bindings (so {{name}} tokens inside\n // cloned items still resolve).\n //\n // Only the rows this tree repeats over are read (AGL-1440), and the tree\n // asked is the composed one — after grafting — because that is the map the\n // expansion reads: a repeatable living in a layout or a reusable component\n // is invisible in `screenNodes`, and reading only the screen's keys would\n // silently render one template row where the author put a list. The\n // screen's own keys were issued beside the chrome reads above; a key only a\n // layout or a component adds is read here, for that key alone.\n //\n // The rows come from the plugin that keeps them, through the repeat-rows\n // contract; this composition names no collection. A declared source whose\n // reader is missing THROWS rather than answering \"no rows\", so a broken boot\n // keeps the last good render instead of replacing every list with one row.\n const composedRepeatKeys = Aglyn.repeatKeys(grafted as any)\n const unreadRepeatKeys = composedRepeatKeys.filter(\n (key) => !screenRepeatKeys.includes(key),\n )\n const repeatRows = unreadRepeatKeys.length\n ? {\n ...screenRepeatRows,\n ...(await readRepeatRows({ hostId, keys: unreadRepeatKeys })),\n }\n : screenRepeatRows\n const repeated = Aglyn.expandRepeatables(grafted as any, repeatRows)\n // Collection entries blocks (AGL-551) expand alongside repeatables:\n // per-entry {{entry.*}} tokens substitute inside the clones here, while\n // page-level tokens wait for resolveNamedTokens below.\n const withEntries = await expandCollectionEntryBlocks(\n hostId,\n repeated,\n options.collection,\n prefetchedSources,\n options.timeZone,\n )\n // Entry Meta blocks (AGL-1385): fill in the routed entry's date/category/\n // tags. Needs no source fetch — the routed entry and its taxonomy are\n // already in hand — so it sits outside `expandCollectionEntryBlocks`, which\n // returns early when no block asks for a collection read. AFTER it, so the\n // per-entry clones it just produced already carry their own resolved values\n // and are skipped.\n const withEntryMeta = Aglyn.expandCollectionEntryMeta(\n withEntries as any,\n options.collection?.entry,\n options.collection?.categories,\n options.timeZone,\n )\n // Entry Author cards (AGL-2486): the same fill, one block over. Its values\n // come off the author RECORD the routed entry resolved to, which the\n // collection read has already attached, so this costs nothing either.\n const withEntryAuthor = Aglyn.expandCollectionEntryAuthor(\n withEntryMeta as any,\n options.collection?.entry,\n )\n const bound = Aglyn.resolveNodesBindings(\n withEntryAuthor as any,\n variables,\n functions,\n )\n // Host variables (AGL-1022): `{{host.*}}` resolves from the INSTALLING\n // site, late — at render, never at install — so a rebrand propagates to\n // every artifact that names the host instead of hard-coding it. Same\n // registry the email path uses, so a token means one thing in both.\n const withHostTokens = Aglyn.resolveNodesHostTokens(\n bound as any,\n options.host,\n )\n // Function-running elements run client-side: embed their definitions\n // (AGL-93), and beside each the site variables that function reads and no\n // others (AGL-3202). Which elements those are is declared by their plugins\n // (AGL-3393), never named here.\n const withFunctions = Aglyn.attachFunctionDefinitions(\n withHostTokens,\n functions,\n variables,\n await functionBindingsFor(withHostTokens, hostId, functions),\n )\n // Marketplace plugins (AGL-45): stamp each marketplacePlugin node with its\n // pinned install (version/sha256/capabilities) + kill-switch state.\n const nodes = Aglyn.attachPluginInstalls(withFunctions, pluginInstalls)\n // Entry-template tokens (AGL-105): {{entry.*}} from the rendered entry.\n const finalNodes = Aglyn.resolveNamedTokens(nodes as any, options.tokens)\n // The document's one `main` landmark (AGL-2486). LAST, so it reads the tree\n // the page actually ships — a slot grafted from a layout chain, an element\n // an author chose — rather than the screen as stored.\n const withLandmark = Aglyn.stampDocumentLandmark(finalNodes as any)\n // Each form the record target writes for, stamped with what that target\n // trusts at submit time (AGL-2773) — a signed binding, today. Read off THIS\n // tree, the one the page ships, so a form grafted from a layout, a component\n // or a form entity is stamped as it renders; and after every stage that\n // rewrites props, so nothing changes what the stamp covers. A declared\n // target that is missing THROWS here rather than shipping forms that\n // silently write nowhere.\n const withFormBindings = await stampFormRecordTargets(\n withLandmark as Record<string, unknown>,\n hostId,\n )\n const denormalized = Aglyn.canvas.processNodesToDenormalized(\n withFormBindings as any,\n )\n /*\n * WHAT EACH PLACED IMAGE AND FILM IS NOW (AGL-2807, AGL-2833).\n *\n * An image's pixel pair and a film's length, shape and poster come from the\n * asset's DAM document as it is now, not as it was when it was picked. LAST,\n * on the tree the page ships, because an asset can arrive from any stage\n * above: a layout, a component, a form, a repeated row, a collection entry,\n * a binding.\n *\n * ONE read for all of them, issued once the tree is final rather than beside\n * the chrome reads. A read issued there could only cover the screen's own\n * placements, and nearly every layout places an image the screen does not\n * (a logo, a footer mark), so it would buy a second read on almost every\n * page. The late read costs its own round trip after the chrome reads, paid\n * each time a page is composed, which for a published page is its ISR\n * regeneration. A tree with no library asset, on a page whose social card\n * names none, issues none.\n *\n * THE SOCIAL CARD'S ASSETS JOIN THE SAME READ (AGL-2850), ahead of the\n * placements. A placement the cap leaves out keeps its pick-time pair, which\n * for an image is a reservation the decoded picture corrects. Nothing\n * corrects a card's pair, because a crawler lays the card out from it before\n * fetching the image, and a card names at most three documents.\n */\n const socialImages = options.socialImages\n const cardRefs = socialImages ? socialImageRefs(socialImages.images) : []\n const refs = mediaAssetRefs(denormalized)\n if (!refs.length && !cardRefs.length) return denormalized\n const facts = await getMediaAssetFacts({\n hostId,\n refs: [...cardRefs, ...refs],\n })\n if (socialImages) {\n const cardFacts = socialImageAssetFacts(socialImages.images, facts)\n if (cardFacts) socialImages.onFacts(cardFacts)\n }\n return applyMediaAssetFacts(denormalized, facts)\n}\n\n/**\n * What the caller knows about the page it is composing, for the page review\n * to NAME it when it holds or flags it (AGL-3374). Never read to decide\n * whether the page serves.\n */\nexport interface ComposePageContext {\n /**\n * A template composed against a routed listing or entry: the route pattern\n * it serves (`/videos`, `/videos/:slug`), the collection it renders, the\n * address being shown, and what a visitor gets when it has no clean\n * version (`built-in-design` for a content collection, whose routes fall\n * back to it; `not-found` for a store template, which has none).\n */\n template?: {\n role: 'list' | 'entry'\n route: string\n collectionName?: string | null\n entryPath?: string | null\n fallback?: 'built-in-design' | 'not-found'\n } | null\n /** An experiment variant's version of the page. */\n variant?: { experimentId: string; variantId: string; name?: string | null } | null\n}\n\n/**\n * The page's own nodes and those of every layout and reusable component\n * composed into it, for the page review to say WHICH one carries flagged\n * content (AGL-3374). Read only when a review row is filed: the layout\n * versions and the component definitions are the cached reads the compose\n * already made.\n */\nasync function composedPageParts(options: {\n hostId: string\n screenId: string\n screenNodes: Record<string, any>\n layoutId: string | null | undefined\n}): Promise<HostedPagePart[]> {\n const parts: HostedPagePart[] = [\n { type: 'screen', id: options.screenId, nodes: options.screenNodes },\n ]\n const seen = new Set<string>()\n let layoutId = options.layoutId ? String(options.layoutId) : undefined\n while (layoutId && !seen.has(layoutId) && seen.size < Aglyn.MAX_LAYOUT_CHAIN_DEPTH) {\n seen.add(layoutId)\n const layoutRes = await getPublishedLayoutVersion({ hostId: options.hostId, layoutId })\n const layout = layoutRes?.layout as { displayName?: unknown; layoutId?: unknown } | undefined\n parts.push({\n type: 'layout',\n id: layoutId,\n name: typeof layout?.displayName === 'string' ? layout.displayName : null,\n nodes: layoutRes?.version?.nodes ?? {},\n })\n layoutId = layout?.layoutId ? String(layout.layoutId) : undefined\n }\n // Only the components this page actually renders, followed through\n // components that nest others, so an unused one is never blamed.\n const { definitions } = await getComponents({ hostId: options.hostId })\n const used = new Set<string>()\n let frontier = parts.map((part) => part.nodes as Record<string, any>)\n while (frontier.length) {\n const next: Record<string, any>[] = []\n for (const [id, definition] of Object.entries(definitions)) {\n if (used.has(id)) continue\n if (frontier.some((nodes) => Aglyn.nodesReferenceComponent(nodes, id))) {\n used.add(id)\n next.push(definition.nodes as Record<string, any>)\n parts.push({ type: 'component', id, nodes: definition.nodes })\n }\n }\n frontier = next\n }\n return parts\n}\n\n/**\n * Full published-render composition for one screen (extracted for AGL-87 so\n * the SSG path and the password-unlock API build identical trees): applies\n * a due publish schedule, loads the version, composes the shared layout\n * chrome, grafts reusable components, and denormalizes.\n */\nexport async function composeScreenNodes(options: {\n hostId: string\n screenId: string\n screen: Aglyn.AglynScreen\n /** Entry-template tokens (AGL-105) substituted before denormalize. */\n tokens?: Record<string, string>\n /** Routed content collection (AGL-551) for Collection entries blocks. */\n collection?: ComposeCollectionContext\n /**\n * Compose a specific version instead of the published one (AGL-253):\n * experiment variants point at versions; schedules don't apply.\n */\n versionId?: string\n /** The host document, for `host.*` tokens (AGL-1022). */\n host?: Aglyn.HostTokenSource | null\n /**\n * The zone this site's dates read in (AGL-3237) — `resolveSiteTimeZone` of\n * the org and the host, resolved ONCE by the caller that holds both.\n *\n * A string rather than the two documents, because the value has to be\n * identical on the server render and the client re-render, and the surest\n * way to guarantee that is for only one place to decide it. Absent is UTC,\n * which is what every site rendered before this existed.\n */\n timeZone?: string\n /** The social card the head shares this page as (AGL-2850). */\n socialImages?: ComposeSocialImages\n /**\n * Internal: `false` when this compose IS the fallback to a held page's\n * last clean version, so a fallback that holds too serves nothing rather\n * than falling back again.\n */\n pageReviewFallback?: boolean\n /** What this page is, for naming it if the page review holds it (AGL-3374). */\n page?: ComposePageContext\n}): Promise<Record<string, any> | null> {\n const { hostId, screenId, screen } = options\n\n const effectiveVersionId = options.versionId\n ? null\n : await applyDuePublishSchedule({\n hostId,\n collectionName: 'screens',\n docId: screenId,\n parent: screen,\n })\n const versionId = (options.versionId ??\n effectiveVersionId ??\n screen.versionId) as string\n\n /*\n * OVERLAP THE VERSION READ WITH THE CHROME BUNDLE (AGL-1428).\n *\n * `composeNodesWithChrome`'s reads are keyed by `hostId`/`layoutId`, both\n * of which are in hand here, so the version read no longer has to finish\n * before they start. Handing the promise over instead of the resolved\n * value is the whole change: it is `composeNodesWithChrome` that decides\n * when it actually needs the nodes.\n *\n * The `versionId`-less screen still exits BEFORE anything is issued, so a\n * screen that has never been published costs no reads. What remains is the\n * narrow case of a `versionId` that points at a missing or unreadable\n * version document — a data-integrity fault rather than a routing outcome\n * — and that one now pays for a chrome bundle it discards. That is the\n * deliberate trade: one wasted bundle on a broken screen, against the\n * version read hiding under the chrome reads on every render that works.\n */\n if (!versionId) return null\n\n const versionPromise = getScreenVersion({ hostId, screenId, versionId })\n\n /*\n * NEITHER DERIVED PROMISE MAY REJECT ON ITS OWN.\n *\n * `composed` is discarded on every failure path below, and a rejected\n * promise nobody awaited is an unhandled rejection — which in Node takes\n * the whole render process down instead of letting this 404. So the nodes\n * handed to the compose absorb the failure (an empty tree it will never be\n * asked for), and `composed` gets a rejection handler attached the moment\n * it exists rather than at the point we decide to drop it. The real error\n * still propagates, from the `await versionPromise` below, exactly where it\n * did when this function awaited the version directly.\n */\n const composed = composeNodesWithChrome({\n ...(options.timeZone ? { timeZone: options.timeZone } : {}),\n hostId,\n // Version-first (key-present wins, null = explicitly no layout), screen\n // fallback — resolved as a promise so only the layout-chain walk waits on\n // the version read; the rest of the chrome bundle keeps the AGL-1428\n // overlap. A failed version read falls back to the screen binding; the\n // whole compose is discarded on that path anyway.\n layoutId: versionPromise.then(\n (res) =>\n res.version && 'layoutId' in res.version\n ? ((res.version as Aglyn.AglynScreenVersion).layoutId as\n | string\n | null)\n : (screen.layoutId as string | undefined),\n () => screen.layoutId as string | undefined,\n ),\n // The screen's values for its layouts' properties (AGL-2893), beside the\n // binding on the same document; a failed read renders the defaults.\n layoutPropValues: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)?.layoutPropValues,\n () => undefined,\n ),\n // …and its per-page restyling of their elements (AGL-3286), from the same\n // document; a failed read renders the layouts' own styling.\n layoutStyleOverrides: versionPromise.then(\n (res) =>\n (res.version as Aglyn.AglynScreenVersion | undefined)\n ?.layoutStyleOverrides,\n () => undefined,\n ),\n screenNodes: versionPromise.then(\n (res) => (res.version?.nodes ?? {}) as any,\n () => ({}) as any,\n ),\n tokens: options.tokens,\n collection: options.collection,\n host: options.host,\n socialImages: options.socialImages,\n })\n void composed.catch(() => undefined)\n\n const versionRes = await versionPromise\n if (versionRes.error || !versionRes.version) return null\n\n /*\n * THE PAGE PHISHING REVIEW (AGL-3356).\n *\n * Every way a page goes live — a toolbar publish, a versions-panel\n * publish, a scheduled publish, a component or form promoted under it, an\n * experiment variant, an edit to the live version in place — meets here,\n * with the page exactly as a visitor will receive it. So this is where a\n * page that links to a brand's lookalike, asks for a password or a card\n * in a field of its own, or (for a new workspace) dresses a brand's\n * sign-in or sends its call to action off the site beside a document or\n * account lure is held rather than served. See `hosted-page-review.ts`.\n *\n * A held version serves the last version this page served clean, screened\n * again, or nothing — and so does a flagged page whose review could not\n * finish, which is held rather than served.\n */\n const nodes = await composed\n const version = versionRes.version as Aglyn.AglynScreenVersion\n const review = await reviewHostedPage({\n hostId,\n screenId,\n versionId,\n nodes,\n // Named by what it is, not only its id (AGL-3374).\n page: {\n screen: {\n displayName: (screen as { displayName?: unknown }).displayName,\n name: (screen as { name?: unknown }).name,\n kind: (screen as { kind?: unknown }).kind,\n },\n template: options.page?.template ?? null,\n variant: options.page?.variant ?? null,\n parts: () =>\n composedPageParts({\n hostId,\n screenId,\n screenNodes: (version?.nodes ?? {}) as Record<string, any>,\n layoutId:\n version && 'layoutId' in version\n ? (version.layoutId as string | null)\n : (screen.layoutId as string | undefined),\n }),\n },\n })\n if (review.outcome === 'serve') {\n // Only the PUBLISHED version is the page's last clean one; a variant or\n // a pinned version is not what the page serves.\n if (nodes && !options.versionId) {\n await recordServedPageVersion(hostId, screenId, versionId)\n }\n return nodes\n }\n if (options.pageReviewFallback === false) return null\n const previous = await servedPageVersion(hostId, screenId)\n if (!previous || previous === versionId) return null\n return composeScreenNodes({\n ...options,\n versionId: previous,\n pageReviewFallback: false,\n })\n}\n\nexport default composeScreenNodes\n"],"names":["Aglyn","applyMediaAssetFacts","mediaAssetRefs","recordServedPageVersion","reviewHostedPage","servedPageVersion","stampFormRecordTargets","readRepeatRows","prepareComputedVariables","applyDuePublishSchedule","getComponents","getForms","getMediaAssetFacts","getPublishedCollectionSource","getPluginInstalls","getVariables","getFunctions","getPublishedLayoutVersion","getScreenVersion","socialImageAssetFacts","socialImageRefs","scanCollectionBlocks","nodes","collection","slugs","Set","hasRelated","hasCategories","hasSearch","node","Object","values","componentId","COLLECTION_ENTRIES_COMPONENT_ID","slug","String","props","collectionSlug","trim","add","COLLECTION_CATEGORIES_COMPONENT_ID","COLLECTION_SEARCH_COMPONENT_ID","COLLECTION_RELATED_COMPONENT_ID","entry","prefetchCollectionSources","hostId","screenNodes","prefetched","entries","pending","catch","undefined","expandCollectionEntryBlocks","timeZone","size","sources","Promise","all","map","categories","page","entriesReachedBound","reachedBound","windowStart","fetched","expanded","expandCollectionEntries","withCategories","expandCollectionCategories","routeless","categorySlug","withSearch","expandCollectionSearch","expandCollectionRelated","functionBindingsFor","functions","firstParty","FIRST_PARTY_FUNCTION_BINDINGS","keys","length","placesMarketplaceElement","some","isNamespacedComponentId","dev","process","env","NODE_ENV","devPluginManifests","getRealmPluginInstalls","installs","mergeFunctionBindings","error","console","composeNodesWithChrome","options","layoutId","walkLayoutChain","chain","seen","boundLayoutId","currentLayoutId","has","MAX_LAYOUT_CHAIN_DEPTH","layoutRes","push","version","parentId","layout","chromeBundle","screenRepeatKeys","repeatKeys","screenRepeatRowsPromise","screenFormsPromise","placesFormEntity","prefetchedSources","layoutChain","componentsRes","bulk","rawVariables","computeVariables","pluginInstalls","screenRepeatRows","layoutPropValues","layoutStyleOverrides","composedNodes","composeLayoutChainWithProps","graftedComponents","composeReusableComponentNodes","definitions","forms","grafted","placedFormPlacement","variables","composedRepeatKeys","unreadRepeatKeys","filter","key","includes","repeatRows","repeated","expandRepeatables","withEntries","withEntryMeta","expandCollectionEntryMeta","withEntryAuthor","expandCollectionEntryAuthor","bound","resolveNodesBindings","withHostTokens","resolveNodesHostTokens","host","withFunctions","attachFunctionDefinitions","attachPluginInstalls","finalNodes","resolveNamedTokens","tokens","withLandmark","stampDocumentLandmark","withFormBindings","denormalized","canvas","processNodesToDenormalized","socialImages","cardRefs","images","refs","facts","cardFacts","onFacts","composedPageParts","parts","type","id","screenId","name","displayName","used","frontier","part","next","definition","nodesReferenceComponent","composeScreenNodes","screen","effectiveVersionId","versionId","collectionName","docId","parent","versionPromise","composed","then","res","versionRes","review","kind","template","variant","outcome","pageReviewFallback","previous"],"mappings":"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"}
|