@aglyn/tenant-data-admin 1.0.0-beta.221 → 1.0.0-beta.223

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.
Files changed (121) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/api-keys.js +3 -3
  3. package/src/lib/server/api-keys.js.map +1 -1
  4. package/src/lib/server/api-v1-resources.d.ts +44 -3
  5. package/src/lib/server/api-v1-resources.js +82 -5
  6. package/src/lib/server/api-v1-resources.js.map +1 -1
  7. package/src/lib/server/collection-live-pages.d.ts +2 -2
  8. package/src/lib/server/collection-live-pages.js +1 -1
  9. package/src/lib/server/collection-live-pages.js.map +1 -1
  10. package/src/lib/server/email-delivery-log.d.ts +17 -73
  11. package/src/lib/server/email-delivery-log.js +44 -222
  12. package/src/lib/server/email-delivery-log.js.map +1 -1
  13. package/src/lib/server/email-flow-gate.js +27 -38
  14. package/src/lib/server/email-flow-gate.js.map +1 -1
  15. package/src/lib/server/erase-person.d.ts +34 -54
  16. package/src/lib/server/erase-person.js +44 -239
  17. package/src/lib/server/erase-person.js.map +1 -1
  18. package/src/lib/server/ga4-measurement-protocol.js +41 -28
  19. package/src/lib/server/ga4-measurement-protocol.js.map +1 -1
  20. package/src/lib/server/host-visitor-records.d.ts +25 -4
  21. package/src/lib/server/host-visitor-records.js +39 -11
  22. package/src/lib/server/host-visitor-records.js.map +1 -1
  23. package/src/lib/server/hosted-page-review.js +52 -36
  24. package/src/lib/server/hosted-page-review.js.map +1 -1
  25. package/src/lib/server/link-reputation-review.d.ts +6 -2
  26. package/src/lib/server/link-reputation-review.js +61 -20
  27. package/src/lib/server/link-reputation-review.js.map +1 -1
  28. package/src/lib/server/live-page-drops.d.ts +67 -0
  29. package/src/lib/server/live-page-drops.js +57 -0
  30. package/src/lib/server/live-page-drops.js.map +1 -0
  31. package/src/lib/server/media-bandwidth-cap.d.ts +78 -0
  32. package/src/lib/server/media-bandwidth-cap.js +180 -0
  33. package/src/lib/server/media-bandwidth-cap.js.map +1 -0
  34. package/src/lib/server/media-cdn-icon.d.ts +77 -0
  35. package/src/lib/server/media-cdn-icon.js +140 -0
  36. package/src/lib/server/media-cdn-icon.js.map +1 -0
  37. package/src/lib/server/media-delivery-regeneration.d.ts +132 -0
  38. package/src/lib/server/media-delivery-regeneration.js +216 -0
  39. package/src/lib/server/media-delivery-regeneration.js.map +1 -0
  40. package/src/lib/server/media-delivery.d.ts +2 -0
  41. package/src/lib/server/media-delivery.js +2 -1
  42. package/src/lib/server/media-delivery.js.map +1 -1
  43. package/src/lib/server/media-serve-count.d.ts +50 -0
  44. package/src/lib/server/media-serve-count.js +62 -0
  45. package/src/lib/server/media-serve-count.js.map +1 -0
  46. package/src/lib/server/media-signing.d.ts +13 -2
  47. package/src/lib/server/media-signing.js +12 -8
  48. package/src/lib/server/media-signing.js.map +1 -1
  49. package/src/lib/server/media-tombstone.js +7 -5
  50. package/src/lib/server/media-tombstone.js.map +1 -1
  51. package/src/lib/server/media-variants.d.ts +151 -20
  52. package/src/lib/server/media-variants.js +204 -27
  53. package/src/lib/server/media-variants.js.map +1 -1
  54. package/src/lib/server/organizations.d.ts +102 -2
  55. package/src/lib/server/organizations.js +388 -58
  56. package/src/lib/server/organizations.js.map +1 -1
  57. package/src/lib/server/outbound-send-review.js +11 -7
  58. package/src/lib/server/outbound-send-review.js.map +1 -1
  59. package/src/lib/server/paged-sweeps.d.ts +51 -0
  60. package/src/lib/server/paged-sweeps.js +79 -0
  61. package/src/lib/server/paged-sweeps.js.map +1 -0
  62. package/src/lib/server/payment-fraud-signal.d.ts +49 -9
  63. package/src/lib/server/payment-fraud-signal.js +192 -48
  64. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  65. package/src/lib/server/platform-marketing-consent.d.ts +0 -49
  66. package/src/lib/server/platform-marketing-consent.js +27 -11
  67. package/src/lib/server/platform-marketing-consent.js.map +1 -1
  68. package/src/lib/server/render-monitor.d.ts +166 -0
  69. package/src/lib/server/render-monitor.js +412 -0
  70. package/src/lib/server/render-monitor.js.map +1 -0
  71. package/src/lib/server/risk-notice.d.ts +7 -0
  72. package/src/lib/server/risk-notice.js +6 -0
  73. package/src/lib/server/risk-notice.js.map +1 -1
  74. package/src/lib/server/serve-media-cdn.d.ts +69 -1
  75. package/src/lib/server/serve-media-cdn.js +424 -70
  76. package/src/lib/server/serve-media-cdn.js.map +1 -1
  77. package/src/lib/server/site-icon-render.d.ts +68 -0
  78. package/src/lib/server/site-icon-render.js +157 -0
  79. package/src/lib/server/site-icon-render.js.map +1 -0
  80. package/src/lib/server/staff-seat.d.ts +50 -0
  81. package/src/lib/server/staff-seat.js +118 -0
  82. package/src/lib/server/staff-seat.js.map +1 -0
  83. package/src/lib/server/transfer-export.d.ts +46 -0
  84. package/src/lib/server/transfer-export.js +221 -0
  85. package/src/lib/server/transfer-export.js.map +1 -0
  86. package/src/lib/server/transfer-jobs.d.ts +385 -0
  87. package/src/lib/server/transfer-jobs.js +2368 -0
  88. package/src/lib/server/transfer-jobs.js.map +1 -0
  89. package/src/lib/server/transfer-packages.d.ts +127 -0
  90. package/src/lib/server/transfer-packages.js +1036 -0
  91. package/src/lib/server/transfer-packages.js.map +1 -0
  92. package/src/lib/server/upgrade-proposal.d.ts +97 -0
  93. package/src/lib/server/upgrade-proposal.js +312 -0
  94. package/src/lib/server/upgrade-proposal.js.map +1 -0
  95. package/src/lib/server/upsert-contact.d.ts +17 -8
  96. package/src/lib/server/upsert-contact.js +82 -16
  97. package/src/lib/server/upsert-contact.js.map +1 -1
  98. package/src/lib/server/web-risk.d.ts +50 -9
  99. package/src/lib/server/web-risk.js +290 -155
  100. package/src/lib/server/web-risk.js.map +1 -1
  101. package/src/tenancy.d.ts +3 -3
  102. package/src/tenancy.js +5 -3
  103. package/src/tenancy.js.map +1 -1
  104. package/src/lib/server/campaign-attribution-store.d.ts +0 -55
  105. package/src/lib/server/campaign-attribution-store.js +0 -95
  106. package/src/lib/server/campaign-attribution-store.js.map +0 -1
  107. package/src/lib/server/campaign-conversion-attribution.d.ts +0 -283
  108. package/src/lib/server/campaign-conversion-attribution.js +0 -450
  109. package/src/lib/server/campaign-conversion-attribution.js.map +0 -1
  110. package/src/lib/server/crm-email-activity.d.ts +0 -118
  111. package/src/lib/server/crm-email-activity.js +0 -148
  112. package/src/lib/server/crm-email-activity.js.map +0 -1
  113. package/src/lib/server/crm-next-activity.d.ts +0 -74
  114. package/src/lib/server/crm-next-activity.js +0 -194
  115. package/src/lib/server/crm-next-activity.js.map +0 -1
  116. package/src/lib/server/dataset-live-pages.d.ts +0 -179
  117. package/src/lib/server/dataset-live-pages.js +0 -421
  118. package/src/lib/server/dataset-live-pages.js.map +0 -1
  119. package/src/lib/server/email-revenue-attribution.d.ts +0 -185
  120. package/src/lib/server/email-revenue-attribution.js +0 -321
  121. package/src/lib/server/email-revenue-attribution.js.map +0 -1
@@ -1,148 +0,0 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
- /**
3
- * @license
4
- * Copyright 2026 Aglyn LLC
5
- *
6
- * Licensed under the Apache License, Version 2.0 (the "License");
7
- * you may not use this file except in compliance with the License.
8
- * You may obtain a copy of the License at
9
- *
10
- * http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing, software
13
- * distributed under the License is distributed on an "AS IS" BASIS,
14
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing permissions and
16
- * limitations under the License.
17
- */ import { CRM_COLLECTIONS, isCrmEmailDeliveryState, nextCrmEmailDeliveryState } from "@aglyn/aglyn/server";
18
- import { createHash } from "crypto";
19
- import { FieldValue } from "firebase-admin/firestore";
20
- /**
21
- * The activity row a one-to-one email is logged as, on the server side
22
- * (AGL-2615): where it lives, how it is written, and how the delivery
23
- * webhook moves its state.
24
- *
25
- * Two writers log a sent email — the console's `crm/email-send` route and
26
- * the `sendEmail` automation step — and one reader advances it, the Resend
27
- * event webhook. The reader has to find the row from nothing but the tags
28
- * on the message, so the id is minted BEFORE the send and stamped on it,
29
- * and the row is written only after the provider accepted the message. A
30
- * row written first would be an "email" on the timeline for a message that
31
- * never left.
32
- *
33
- * Firestore is a parameter throughout, as in `crm-records.ts`: the callers
34
- * hold one, and a module that reached for the Admin app itself would drag
35
- * it into every spec that only wants the arithmetic.
36
- */ /** `orgs/{orgId}/crmActivities/{activityId}`. */ export function crmActivityRef(firestore, orgId, activityId) {
37
- return firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities).doc(activityId);
38
- }
39
- /**
40
- * A fresh reference for the row a send is about to earn — `doc()` with no
41
- * id mints one locally and reads nothing, so the id can ride the message
42
- * as a tag before anything is written.
43
- */ export function newCrmActivityRef(firestore, orgId) {
44
- return firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities).doc();
45
- }
46
- /**
47
- * Writes the row a sent email is logged as, stamped with the server clock.
48
- *
49
- * `set` rather than `add`, because the id was minted ahead of the send and
50
- * is already on the message; a second id here would be the one the webhook
51
- * could not find.
52
- */ export async function writeCrmEmailActivity(ref, activity) {
53
- await ref.set(_extends({}, activity, {
54
- createdAt: FieldValue.serverTimestamp(),
55
- updatedAt: FieldValue.serverTimestamp()
56
- }));
57
- }
58
- /**
59
- * Which delivery state one normalized delivery event moves a row to, or
60
- * `null` for an event that moves nothing.
61
- *
62
- * Takes the log's own vocabulary (`EmailDeliveryEventType`) rather than the
63
- * provider's wire strings: the mail provider's `deliveryEvents` is the one place
64
- * that reads those, and this module must not become a second. The five
65
- * states the timeline shows share the log's names exactly, so the mapping
66
- * is membership; `delayed` and `failed` are the log's own and the row keeps
67
- * whatever it held.
68
- */ export function crmEmailDeliveryStateForEvent(type) {
69
- return isCrmEmailDeliveryState(type) ? type : null;
70
- }
71
- /**
72
- * One delivery event, onto the activity row it names (AGL-2615).
73
- *
74
- * A TRANSACTION, because the rule is "keep the later state" and two events
75
- * for one message routinely arrive together — `delivered` and `opened`
76
- * within the same second from a mail client that fetches images on
77
- * receipt. Two plain read-then-writes interleaved would let the earlier
78
- * event land last and print "Delivered" over an open that already
79
- * happened; inside a transaction the second read sees the first write and
80
- * `nextCrmEmailDeliveryState` answers "unchanged".
81
- *
82
- * A row that is not an email activity is left alone: the tag named it, but
83
- * a delivery state on a call log would be a chip the timeline has no way to
84
- * explain. **Never throws** — the webhook answers the provider 200 whatever
85
- * happens here, so a failure is logged and reported rather than raised.
86
- */ export async function recordCrmEmailDelivery(firestore, input) {
87
- var _input_detail;
88
- const { orgId, activityId, state } = input;
89
- if (!orgId || !activityId) return 'missing';
90
- const ref = crmActivityRef(firestore, orgId, activityId);
91
- const detail = String((_input_detail = input.detail) != null ? _input_detail : '').replace(/\s+/g, ' ').trim().slice(0, 500);
92
- try {
93
- return await firestore.runTransaction(async (transaction)=>{
94
- const snapshot = await transaction.get(ref);
95
- if (!snapshot.exists || snapshot.get('kind') !== 'email') return 'missing';
96
- const current = snapshot.get('deliveryState');
97
- const next = nextCrmEmailDeliveryState(current, state);
98
- if (next === current) return 'unchanged';
99
- transaction.update(ref, _extends({
100
- deliveryState: next,
101
- deliveryAtMs: Number.isFinite(input.atMs) ? input.atMs : Date.now()
102
- }, detail ? {
103
- deliveryDetail: detail
104
- } : {}, {
105
- updatedAt: FieldValue.serverTimestamp()
106
- }));
107
- return 'advanced';
108
- });
109
- } catch (error) {
110
- console.error('[crm] email delivery state write failed', orgId, activityId, error);
111
- return 'failed';
112
- }
113
- }
114
- /**
115
- * `orgs/{orgId}/crmActivities/{id}` for a CAPTURED message (AGL-2657), with
116
- * the id derived from the message's key — its `Message-ID`, else the
117
- * provider's id — so a second delivery of one message addresses the row
118
- * the first delivery wrote. A hash rather than the key itself, because a
119
- * `Message-ID` carries `<`, `@` and `/`, which a document id may not.
120
- */ export function crmCapturedEmailActivityRef(firestore, orgId, key) {
121
- const digest = createHash('sha256').update(key).digest('hex').slice(0, 28);
122
- return crmActivityRef(firestore, orgId, `cap_${digest}`);
123
- }
124
- const isAlreadyExists = (error)=>{
125
- var _ref;
126
- const failure = error;
127
- return (failure == null ? void 0 : failure.code) === 6 || (failure == null ? void 0 : failure.code) === 'already-exists' || /ALREADY_EXISTS/i.test(String((_ref = failure == null ? void 0 : failure.message) != null ? _ref : ''));
128
- };
129
- /**
130
- * Writes a captured message's row ONCE: `create()`, which refuses a
131
- * document that already exists, is what makes two deliveries of one
132
- * message one row even when they arrive together — the second create is
133
- * the one Firestore refuses, and this answers `duplicate` for it. Any
134
- * other failure is the caller's.
135
- */ export async function createCrmEmailActivity(ref, activity) {
136
- try {
137
- await ref.create(_extends({}, activity, {
138
- createdAt: FieldValue.serverTimestamp(),
139
- updatedAt: FieldValue.serverTimestamp()
140
- }));
141
- return 'created';
142
- } catch (error) {
143
- if (isAlreadyExists(error)) return 'duplicate';
144
- throw error;
145
- }
146
- }
147
-
148
- //# sourceMappingURL=crm-email-activity.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-email-activity.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 CRM_COLLECTIONS,\n type CrmActivity,\n type CrmEmailDeliveryState,\n isCrmEmailDeliveryState,\n nextCrmEmailDeliveryState,\n} from '@aglyn/aglyn/server'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * The activity row a one-to-one email is logged as, on the server side\n * (AGL-2615): where it lives, how it is written, and how the delivery\n * webhook moves its state.\n *\n * Two writers log a sent email — the console's `crm/email-send` route and\n * the `sendEmail` automation step — and one reader advances it, the Resend\n * event webhook. The reader has to find the row from nothing but the tags\n * on the message, so the id is minted BEFORE the send and stamped on it,\n * and the row is written only after the provider accepted the message. A\n * row written first would be an \"email\" on the timeline for a message that\n * never left.\n *\n * Firestore is a parameter throughout, as in `crm-records.ts`: the callers\n * hold one, and a module that reached for the Admin app itself would drag\n * it into every spec that only wants the arithmetic.\n */\n\n/** `orgs/{orgId}/crmActivities/{activityId}`. */\nexport function crmActivityRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n activityId: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_COLLECTIONS.activities)\n .doc(activityId)\n}\n\n/**\n * A fresh reference for the row a send is about to earn — `doc()` with no\n * id mints one locally and reads nothing, so the id can ride the message\n * as a tag before anything is written.\n */\nexport function newCrmActivityRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_COLLECTIONS.activities)\n .doc()\n}\n\n/**\n * Writes the row a sent email is logged as, stamped with the server clock.\n *\n * `set` rather than `add`, because the id was minted ahead of the send and\n * is already on the message; a second id here would be the one the webhook\n * could not find.\n */\nexport async function writeCrmEmailActivity(\n ref: FirebaseFirestore.DocumentReference,\n activity: CrmActivity,\n): Promise<void> {\n await ref.set({\n ...activity,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n}\n\n/**\n * Which delivery state one normalized delivery event moves a row to, or\n * `null` for an event that moves nothing.\n *\n * Takes the log's own vocabulary (`EmailDeliveryEventType`) rather than the\n * provider's wire strings: the mail provider's `deliveryEvents` is the one place\n * that reads those, and this module must not become a second. The five\n * states the timeline shows share the log's names exactly, so the mapping\n * is membership; `delayed` and `failed` are the log's own and the row keeps\n * whatever it held.\n */\nexport function crmEmailDeliveryStateForEvent(\n type: string | null | undefined,\n): CrmEmailDeliveryState | null {\n return isCrmEmailDeliveryState(type) ? type : null\n}\n\nexport type CrmEmailDeliveryOutcome =\n /** The row moved to the incoming state. */\n | 'advanced'\n /** The row already held this state or a later one; nothing written. */\n | 'unchanged'\n /** No email activity by that id in that org; nothing written. */\n | 'missing'\n /** The read or the write threw; logged, and nothing is promised. */\n | 'failed'\n\n/**\n * One delivery event, onto the activity row it names (AGL-2615).\n *\n * A TRANSACTION, because the rule is \"keep the later state\" and two events\n * for one message routinely arrive together — `delivered` and `opened`\n * within the same second from a mail client that fetches images on\n * receipt. Two plain read-then-writes interleaved would let the earlier\n * event land last and print \"Delivered\" over an open that already\n * happened; inside a transaction the second read sees the first write and\n * `nextCrmEmailDeliveryState` answers \"unchanged\".\n *\n * A row that is not an email activity is left alone: the tag named it, but\n * a delivery state on a call log would be a chip the timeline has no way to\n * explain. **Never throws** — the webhook answers the provider 200 whatever\n * happens here, so a failure is logged and reported rather than raised.\n */\nexport async function recordCrmEmailDelivery(\n firestore: FirebaseFirestore.Firestore,\n input: {\n orgId: string\n activityId: string\n state: CrmEmailDeliveryState\n /** When the provider says it happened, epoch ms. */\n atMs: number\n /**\n * What the receiving server said (AGL-3245), for a failure: the bounce's\n * diagnostic, scrubbed of the address. Kept beside the state as\n * `deliveryDetail`, so the timeline can say why the message did not land.\n */\n detail?: string | null\n },\n): Promise<CrmEmailDeliveryOutcome> {\n const { orgId, activityId, state } = input\n if (!orgId || !activityId) return 'missing'\n const ref = crmActivityRef(firestore, orgId, activityId)\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim().slice(0, 500)\n try {\n return await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists || snapshot.get('kind') !== 'email') return 'missing'\n const current = snapshot.get('deliveryState')\n const next = nextCrmEmailDeliveryState(current, state)\n if (next === current) return 'unchanged'\n transaction.update(ref, {\n deliveryState: next,\n deliveryAtMs: Number.isFinite(input.atMs) ? input.atMs : Date.now(),\n ...(detail ? { deliveryDetail: detail } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return 'advanced'\n })\n } catch (error) {\n console.error('[crm] email delivery state write failed', orgId, activityId, error)\n return 'failed'\n }\n}\n\n/**\n * `orgs/{orgId}/crmActivities/{id}` for a CAPTURED message (AGL-2657), with\n * the id derived from the message's key — its `Message-ID`, else the\n * provider's id — so a second delivery of one message addresses the row\n * the first delivery wrote. A hash rather than the key itself, because a\n * `Message-ID` carries `<`, `@` and `/`, which a document id may not.\n */\nexport function crmCapturedEmailActivityRef(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n key: string,\n): FirebaseFirestore.DocumentReference {\n const digest = createHash('sha256').update(key).digest('hex').slice(0, 28)\n return crmActivityRef(firestore, orgId, `cap_${digest}`)\n}\n\n/** What a create answered: the row is new, or it was already there. */\nexport type CrmEmailActivityCreateOutcome = 'created' | 'duplicate'\n\nconst isAlreadyExists = (error: unknown): boolean => {\n const failure = error as { code?: unknown; message?: unknown } | null\n return (\n failure?.code === 6 ||\n failure?.code === 'already-exists' ||\n /ALREADY_EXISTS/i.test(String(failure?.message ?? ''))\n )\n}\n\n/**\n * Writes a captured message's row ONCE: `create()`, which refuses a\n * document that already exists, is what makes two deliveries of one\n * message one row even when they arrive together — the second create is\n * the one Firestore refuses, and this answers `duplicate` for it. Any\n * other failure is the caller's.\n */\nexport async function createCrmEmailActivity(\n ref: FirebaseFirestore.DocumentReference,\n activity: CrmActivity,\n): Promise<CrmEmailActivityCreateOutcome> {\n try {\n await ref.create({\n ...activity,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return 'created'\n } catch (error) {\n if (isAlreadyExists(error)) return 'duplicate'\n throw error\n }\n}\n"],"names":["CRM_COLLECTIONS","isCrmEmailDeliveryState","nextCrmEmailDeliveryState","createHash","FieldValue","crmActivityRef","firestore","orgId","activityId","collection","doc","activities","newCrmActivityRef","writeCrmEmailActivity","ref","activity","set","createdAt","serverTimestamp","updatedAt","crmEmailDeliveryStateForEvent","type","recordCrmEmailDelivery","input","state","detail","String","replace","trim","slice","runTransaction","transaction","snapshot","get","exists","current","next","update","deliveryState","deliveryAtMs","Number","isFinite","atMs","Date","now","deliveryDetail","error","console","crmCapturedEmailActivityRef","key","digest","isAlreadyExists","failure","code","test","message","createCrmEmailActivity","create"],"mappings":"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"}
@@ -1,74 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Aglyn LLC
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
- import { type CrmNextActivityLink, type CrmNextActivityLinkField } from '@aglyn/aglyn/app-utils/crm-next-activity';
18
- /** The collection each link field's record lives in, under `orgs/{orgId}/`. */
19
- export declare const CRM_NEXT_ACTIVITY_COLLECTIONS: Readonly<Record<CrmNextActivityLinkField, string>>;
20
- export interface CrmNextActivityRecompute {
21
- /** Records whose stored value was written (or rewritten to the same figure). */
22
- records: number;
23
- /** Records that no longer exist — a task can outlive what it was filed against. */
24
- missing: number;
25
- }
26
- /**
27
- * Recompute `nextTaskAtMs` for every record the links name, once each.
28
- *
29
- * Hand in BOTH sides of a change — the links a task carried before an
30
- * update and the ones it carries after — so a task moved from one deal to
31
- * another leaves neither stale. A record the task named that has since been
32
- * deleted is counted in `missing` and skipped; nothing here creates one.
33
- */
34
- export declare function recomputeCrmNextTaskAt(firestore: FirebaseFirestore.Firestore, orgId: string, links: readonly (CrmNextActivityLink | null | undefined)[]): Promise<CrmNextActivityRecompute>;
35
- /**
36
- * The same, from a task document rather than a link — what a writer holds
37
- * after it has read the task it is about to change or has just changed.
38
- */
39
- export declare function crmNextActivityLinksOf(task: Record<string, unknown> | null | undefined): CrmNextActivityLink;
40
- /** The most open tasks one org-wide recompute walks. */
41
- export declare const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20000;
42
- export interface CrmNextActivitySweep {
43
- /** Open tasks read. */
44
- tasks: number;
45
- /** Records that now carry a time. */
46
- scheduled: number;
47
- /** Records that carried a time and now carry `null`. */
48
- cleared: number;
49
- /** Whether the org held more open tasks than the sweep reads. */
50
- truncated: boolean;
51
- }
52
- /**
53
- * The one-off recompute for a whole organization — the Fields section's
54
- * "Recompute next activity" (AGL-2661), for the records written before the
55
- * field existed and for whatever a client-direct write left behind.
56
- *
57
- * Two passes and no per-record task query: every OPEN task once, grouped
58
- * by the records it names, so each record with something scheduled is
59
- * written its earliest due time; then every record in the three
60
- * collections that CARRIES a time and was not in that group, cleared to
61
- * `null`. A record that was never written is left alone — every reader
62
- * already treats absent as `null`, and writing a null onto every contact
63
- * an org holds would cost a write per person to say nothing.
64
- *
65
- * The clearing pass runs only when the first pass read EVERY open task.
66
- * "No open task names this record" is a statement about all of them, and
67
- * an organization holding more than the cap has open tasks the sweep never
68
- * saw — clearing on a partial read would blank records that are perfectly
69
- * scheduled. Past the cap the sweep is the writing half alone, and
70
- * `truncated` says so.
71
- */
72
- export declare function sweepCrmNextTaskAt(firestore: FirebaseFirestore.Firestore, orgId: string, options?: {
73
- max?: number;
74
- }): Promise<CrmNextActivitySweep>;
@@ -1,194 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Aglyn LLC
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */ /**
17
- * The `nextTaskAtMs` a record carries, as the Admin SDK writes it (AGL-2661).
18
- *
19
- * A contact, a company and a deal each denormalize when their earliest OPEN
20
- * task is due, so a list can draw a "Next activity" column and answer
21
- * "which deals have nothing scheduled" without a task query per row. This
22
- * is the writer: given the records a task names, read that record's tasks
23
- * back and store the answer `nextTaskAtMsOf` gives. Every server-side task
24
- * writer — the console's task routes, the REST resource, the workflow step
25
- * that files a task, the merge that repoints one — calls it after its own
26
- * write; a client-direct task write asks the console's `crm/next-activity`
27
- * route, which calls it too.
28
- *
29
- * Imported by module path rather than from the `@aglyn/aglyn/server`
30
- * barrel, for the reason `contact-company-link.ts` gives.
31
- *
32
- * The task query is ONE equality on the link field, the query the record
33
- * cards already run, and the status is judged in memory — no composite
34
- * index, and a record with a hundred done tasks reads a hundred small
35
- * documents once per write against it, which is the price of never being
36
- * wrong. The record write is a bare `update()` of the one field, so
37
- * `updatedAt` stays where the person's last edit left it: a task moving is
38
- * not the record being edited, and the lists order by `updatedAt`.
39
- */ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
40
- import { CRM_NEXT_ACTIVITY_FIELD, mergeNextActivityLinks, nextActivityLinksOfTask, nextTaskAtMsOf, readNextTaskAtMs } from "@aglyn/aglyn/app-utils/crm-next-activity";
41
- /** The collection each link field's record lives in, under `orgs/{orgId}/`. */ export const CRM_NEXT_ACTIVITY_COLLECTIONS = {
42
- contactId: 'contacts',
43
- companyId: CRM_COLLECTIONS.companies,
44
- dealId: CRM_COLLECTIONS.deals
45
- };
46
- const isNotFound = (error)=>{
47
- const { code, message } = error != null ? error : {};
48
- return code === 5 || code === 'not-found' || code === 'NOT_FOUND' || typeof message === 'string' && message.includes('NOT_FOUND');
49
- };
50
- /**
51
- * Store one record's `nextTaskAtMs` from its tasks. `null` for a record
52
- * that no longer exists, else the value written.
53
- */ async function recomputeOne(orgRef, field, id) {
54
- const tasks = await orgRef.collection(CRM_COLLECTIONS.tasks).where(field, '==', id).get();
55
- const value = nextTaskAtMsOf(tasks.docs.map((doc)=>({
56
- status: doc.get('status'),
57
- dueAtMs: doc.get('dueAtMs')
58
- })));
59
- try {
60
- await orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field]).doc(id).update({
61
- [CRM_NEXT_ACTIVITY_FIELD]: value
62
- });
63
- } catch (error) {
64
- if (isNotFound(error)) return {
65
- written: false
66
- };
67
- throw error;
68
- }
69
- return {
70
- written: true,
71
- value
72
- };
73
- }
74
- /**
75
- * Recompute `nextTaskAtMs` for every record the links name, once each.
76
- *
77
- * Hand in BOTH sides of a change — the links a task carried before an
78
- * update and the ones it carries after — so a task moved from one deal to
79
- * another leaves neither stale. A record the task named that has since been
80
- * deleted is counted in `missing` and skipped; nothing here creates one.
81
- */ export async function recomputeCrmNextTaskAt(firestore, orgId, links) {
82
- const orgRef = firestore.collection('orgs').doc(orgId);
83
- const result = {
84
- records: 0,
85
- missing: 0
86
- };
87
- for (const { field, id } of mergeNextActivityLinks(links)){
88
- const outcome = await recomputeOne(orgRef, field, id);
89
- if (outcome.written) result.records += 1;
90
- else result.missing += 1;
91
- }
92
- return result;
93
- }
94
- /**
95
- * The same, from a task document rather than a link — what a writer holds
96
- * after it has read the task it is about to change or has just changed.
97
- */ export function crmNextActivityLinksOf(task) {
98
- return nextActivityLinksOfTask(task);
99
- }
100
- /** The most open tasks one org-wide recompute walks. */ export const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20000;
101
- /**
102
- * The one-off recompute for a whole organization — the Fields section's
103
- * "Recompute next activity" (AGL-2661), for the records written before the
104
- * field existed and for whatever a client-direct write left behind.
105
- *
106
- * Two passes and no per-record task query: every OPEN task once, grouped
107
- * by the records it names, so each record with something scheduled is
108
- * written its earliest due time; then every record in the three
109
- * collections that CARRIES a time and was not in that group, cleared to
110
- * `null`. A record that was never written is left alone — every reader
111
- * already treats absent as `null`, and writing a null onto every contact
112
- * an org holds would cost a write per person to say nothing.
113
- *
114
- * The clearing pass runs only when the first pass read EVERY open task.
115
- * "No open task names this record" is a statement about all of them, and
116
- * an organization holding more than the cap has open tasks the sweep never
117
- * saw — clearing on a partial read would blank records that are perfectly
118
- * scheduled. Past the cap the sweep is the writing half alone, and
119
- * `truncated` says so.
120
- */ export async function sweepCrmNextTaskAt(firestore, orgId, options = {}) {
121
- var _options_max;
122
- const max = (_options_max = options.max) != null ? _options_max : CRM_NEXT_ACTIVITY_SWEEP_MAX;
123
- const orgRef = firestore.collection('orgs').doc(orgId);
124
- const open = await orgRef.collection(CRM_COLLECTIONS.tasks).where('status', '==', 'open').limit(max + 1).get();
125
- const truncated = open.docs.length > max;
126
- const docs = open.docs.slice(0, max);
127
- const earliest = new Map();
128
- for (const field of Object.keys(CRM_NEXT_ACTIVITY_COLLECTIONS)){
129
- earliest.set(field, new Map());
130
- }
131
- for (const doc of docs){
132
- const task = {
133
- status: doc.get('status'),
134
- dueAtMs: doc.get('dueAtMs')
135
- };
136
- const links = nextActivityLinksOfTask({
137
- contactId: doc.get('contactId'),
138
- companyId: doc.get('companyId'),
139
- dealId: doc.get('dealId')
140
- });
141
- for (const { field, id } of mergeNextActivityLinks([
142
- links
143
- ])){
144
- var _byId_get;
145
- const byId = earliest.get(field);
146
- byId.set(id, nextTaskAtMsOf([
147
- task,
148
- {
149
- status: 'open',
150
- dueAtMs: (_byId_get = byId.get(id)) != null ? _byId_get : null
151
- }
152
- ]));
153
- }
154
- }
155
- const sweep = {
156
- tasks: docs.length,
157
- scheduled: 0,
158
- cleared: 0,
159
- truncated
160
- };
161
- for (const [field, byId] of earliest){
162
- const records = orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field]);
163
- for (const [id, value] of byId){
164
- try {
165
- await records.doc(id).update({
166
- [CRM_NEXT_ACTIVITY_FIELD]: value
167
- });
168
- sweep.scheduled += 1;
169
- } catch (error) {
170
- if (!isNotFound(error)) throw error;
171
- }
172
- }
173
- /*
174
- * The stale half: a record that says something is scheduled when no
175
- * open task names it any more. `> 0` is the one query a stored time
176
- * answers without an index of its own, and matches what
177
- * `readNextTaskAtMs` calls a usable value.
178
- */ if (truncated) continue;
179
- const carrying = await records.where(CRM_NEXT_ACTIVITY_FIELD, '>', 0).get();
180
- for (const doc of carrying.docs){
181
- if (byId.has(doc.id)) continue;
182
- if (readNextTaskAtMs({
183
- nextTaskAtMs: doc.get(CRM_NEXT_ACTIVITY_FIELD)
184
- }) === null) continue;
185
- await doc.ref.update({
186
- [CRM_NEXT_ACTIVITY_FIELD]: null
187
- });
188
- sweep.cleared += 1;
189
- }
190
- }
191
- return sweep;
192
- }
193
-
194
- //# sourceMappingURL=crm-next-activity.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-next-activity.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\n/**\n * The `nextTaskAtMs` a record carries, as the Admin SDK writes it (AGL-2661).\n *\n * A contact, a company and a deal each denormalize when their earliest OPEN\n * task is due, so a list can draw a \"Next activity\" column and answer\n * \"which deals have nothing scheduled\" without a task query per row. This\n * is the writer: given the records a task names, read that record's tasks\n * back and store the answer `nextTaskAtMsOf` gives. Every server-side task\n * writer — the console's task routes, the REST resource, the workflow step\n * that files a task, the merge that repoints one — calls it after its own\n * write; a client-direct task write asks the console's `crm/next-activity`\n * route, which calls it too.\n *\n * Imported by module path rather than from the `@aglyn/aglyn/server`\n * barrel, for the reason `contact-company-link.ts` gives.\n *\n * The task query is ONE equality on the link field, the query the record\n * cards already run, and the status is judged in memory — no composite\n * index, and a record with a hundred done tasks reads a hundred small\n * documents once per write against it, which is the price of never being\n * wrong. The record write is a bare `update()` of the one field, so\n * `updatedAt` stays where the person's last edit left it: a task moving is\n * not the record being edited, and the lists order by `updatedAt`.\n */\n\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n CRM_NEXT_ACTIVITY_FIELD,\n type CrmNextActivityLink,\n type CrmNextActivityLinkField,\n mergeNextActivityLinks,\n nextActivityLinksOfTask,\n nextTaskAtMsOf,\n readNextTaskAtMs,\n} from '@aglyn/aglyn/app-utils/crm-next-activity'\n\n/** The collection each link field's record lives in, under `orgs/{orgId}/`. */\nexport const CRM_NEXT_ACTIVITY_COLLECTIONS: Readonly<Record<CrmNextActivityLinkField, string>> = {\n contactId: 'contacts',\n companyId: CRM_COLLECTIONS.companies,\n dealId: CRM_COLLECTIONS.deals,\n}\n\nexport interface CrmNextActivityRecompute {\n /** Records whose stored value was written (or rewritten to the same figure). */\n records: number\n /** Records that no longer exist — a task can outlive what it was filed against. */\n missing: number\n}\n\nconst isNotFound = (error: unknown): boolean => {\n const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown }\n return (\n code === 5 ||\n code === 'not-found' ||\n code === 'NOT_FOUND' ||\n (typeof message === 'string' && message.includes('NOT_FOUND'))\n )\n}\n\n/**\n * Store one record's `nextTaskAtMs` from its tasks. `null` for a record\n * that no longer exists, else the value written.\n */\nasync function recomputeOne(\n orgRef: FirebaseFirestore.DocumentReference,\n field: CrmNextActivityLinkField,\n id: string,\n): Promise<{ written: true; value: number | null } | { written: false }> {\n const tasks = await orgRef\n .collection(CRM_COLLECTIONS.tasks)\n .where(field, '==', id)\n .get()\n const value = nextTaskAtMsOf(\n tasks.docs.map((doc) => ({\n status: doc.get('status'),\n dueAtMs: doc.get('dueAtMs'),\n })),\n )\n try {\n await orgRef\n .collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field])\n .doc(id)\n .update({ [CRM_NEXT_ACTIVITY_FIELD]: value })\n } catch (error) {\n if (isNotFound(error)) return { written: false }\n throw error\n }\n return { written: true, value }\n}\n\n/**\n * Recompute `nextTaskAtMs` for every record the links name, once each.\n *\n * Hand in BOTH sides of a change — the links a task carried before an\n * update and the ones it carries after — so a task moved from one deal to\n * another leaves neither stale. A record the task named that has since been\n * deleted is counted in `missing` and skipped; nothing here creates one.\n */\nexport async function recomputeCrmNextTaskAt(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n links: readonly (CrmNextActivityLink | null | undefined)[],\n): Promise<CrmNextActivityRecompute> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const result: CrmNextActivityRecompute = { records: 0, missing: 0 }\n for (const { field, id } of mergeNextActivityLinks(links)) {\n const outcome = await recomputeOne(orgRef, field, id)\n if (outcome.written) result.records += 1\n else result.missing += 1\n }\n return result\n}\n\n/**\n * The same, from a task document rather than a link — what a writer holds\n * after it has read the task it is about to change or has just changed.\n */\nexport function crmNextActivityLinksOf(\n task: Record<string, unknown> | null | undefined,\n): CrmNextActivityLink {\n return nextActivityLinksOfTask(task as Parameters<typeof nextActivityLinksOfTask>[0])\n}\n\n/** The most open tasks one org-wide recompute walks. */\nexport const CRM_NEXT_ACTIVITY_SWEEP_MAX = 20_000\n\nexport interface CrmNextActivitySweep {\n /** Open tasks read. */\n tasks: number\n /** Records that now carry a time. */\n scheduled: number\n /** Records that carried a time and now carry `null`. */\n cleared: number\n /** Whether the org held more open tasks than the sweep reads. */\n truncated: boolean\n}\n\n/**\n * The one-off recompute for a whole organization — the Fields section's\n * \"Recompute next activity\" (AGL-2661), for the records written before the\n * field existed and for whatever a client-direct write left behind.\n *\n * Two passes and no per-record task query: every OPEN task once, grouped\n * by the records it names, so each record with something scheduled is\n * written its earliest due time; then every record in the three\n * collections that CARRIES a time and was not in that group, cleared to\n * `null`. A record that was never written is left alone — every reader\n * already treats absent as `null`, and writing a null onto every contact\n * an org holds would cost a write per person to say nothing.\n *\n * The clearing pass runs only when the first pass read EVERY open task.\n * \"No open task names this record\" is a statement about all of them, and\n * an organization holding more than the cap has open tasks the sweep never\n * saw — clearing on a partial read would blank records that are perfectly\n * scheduled. Past the cap the sweep is the writing half alone, and\n * `truncated` says so.\n */\nexport async function sweepCrmNextTaskAt(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n options: { max?: number } = {},\n): Promise<CrmNextActivitySweep> {\n const max = options.max ?? CRM_NEXT_ACTIVITY_SWEEP_MAX\n const orgRef = firestore.collection('orgs').doc(orgId)\n const open = await orgRef\n .collection(CRM_COLLECTIONS.tasks)\n .where('status', '==', 'open')\n .limit(max + 1)\n .get()\n const truncated = open.docs.length > max\n const docs = open.docs.slice(0, max)\n\n const earliest = new Map<CrmNextActivityLinkField, Map<string, number | null>>()\n for (const field of Object.keys(CRM_NEXT_ACTIVITY_COLLECTIONS) as CrmNextActivityLinkField[]) {\n earliest.set(field, new Map())\n }\n for (const doc of docs) {\n const task = { status: doc.get('status'), dueAtMs: doc.get('dueAtMs') }\n const links = nextActivityLinksOfTask({\n contactId: doc.get('contactId'),\n companyId: doc.get('companyId'),\n dealId: doc.get('dealId'),\n })\n for (const { field, id } of mergeNextActivityLinks([links])) {\n const byId = earliest.get(field) as Map<string, number | null>\n byId.set(id, nextTaskAtMsOf([task, { status: 'open', dueAtMs: byId.get(id) ?? null }]))\n }\n }\n\n const sweep: CrmNextActivitySweep = { tasks: docs.length, scheduled: 0, cleared: 0, truncated }\n for (const [field, byId] of earliest) {\n const records = orgRef.collection(CRM_NEXT_ACTIVITY_COLLECTIONS[field])\n for (const [id, value] of byId) {\n try {\n await records.doc(id).update({ [CRM_NEXT_ACTIVITY_FIELD]: value })\n sweep.scheduled += 1\n } catch (error) {\n if (!isNotFound(error)) throw error\n }\n }\n /*\n * The stale half: a record that says something is scheduled when no\n * open task names it any more. `> 0` is the one query a stored time\n * answers without an index of its own, and matches what\n * `readNextTaskAtMs` calls a usable value.\n */\n if (truncated) continue\n const carrying = await records.where(CRM_NEXT_ACTIVITY_FIELD, '>', 0).get()\n for (const doc of carrying.docs) {\n if (byId.has(doc.id)) continue\n if (readNextTaskAtMs({ nextTaskAtMs: doc.get(CRM_NEXT_ACTIVITY_FIELD) }) === null) continue\n await doc.ref.update({ [CRM_NEXT_ACTIVITY_FIELD]: null })\n sweep.cleared += 1\n }\n }\n return sweep\n}\n"],"names":["CRM_COLLECTIONS","CRM_NEXT_ACTIVITY_FIELD","mergeNextActivityLinks","nextActivityLinksOfTask","nextTaskAtMsOf","readNextTaskAtMs","CRM_NEXT_ACTIVITY_COLLECTIONS","contactId","companyId","companies","dealId","deals","isNotFound","error","code","message","includes","recomputeOne","orgRef","field","id","tasks","collection","where","get","value","docs","map","doc","status","dueAtMs","update","written","recomputeCrmNextTaskAt","firestore","orgId","links","result","records","missing","outcome","crmNextActivityLinksOf","task","CRM_NEXT_ACTIVITY_SWEEP_MAX","sweepCrmNextTaskAt","options","max","open","limit","truncated","length","slice","earliest","Map","Object","keys","set","byId","sweep","scheduled","cleared","carrying","has","nextTaskAtMs","ref"],"mappings":"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"}
@@ -1,179 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2026 Aglyn LLC
4
- *
5
- * Licensed under the Apache License, Version 2.0 (the "License");
6
- * you may not use this file except in compliance with the License.
7
- * You may obtain a copy of the License at
8
- *
9
- * http://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in writing, software
12
- * distributed under the License is distributed on an "AS IS" BASIS,
13
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- * See the License for the specific language governing permissions and
15
- * limitations under the License.
16
- */
17
- import type { Firestore } from 'firebase-admin/firestore';
18
- import { type UsageSources } from './live-page-usage';
19
- /**
20
- * How many documents per collection the per-site scan reads.
21
- *
22
- * The console's publish scan uses the same number for the same reason
23
- * (AGL-1161): this decides which caches get dropped, so a prefix scan would
24
- * report a refreshed site and leave real pages serving the old rows. Bounded
25
- * all the same, and when the bound bites it is SAID rather than absorbed.
26
- */
27
- export declare const DATASET_SCAN_LIMIT = 2000;
28
- /**
29
- * How many sites one record write fans out to.
30
- *
31
- * `MAX_SCOPE_HOSTS` is the ceiling on a NAMED selection, so only an org-wide
32
- * dataset can reach this — an org with more sites than this pays a scan per
33
- * site on every write otherwise, which is a cost the person writing one row
34
- * did not ask for. Sites past the cap keep the TTL they had before this
35
- * existed, and the shortfall is returned and logged rather than swallowed.
36
- */
37
- export declare const MAX_DATASET_FANOUT_HOSTS = 30;
38
- /**
39
- * How long one site-and-dataset pair waits before announcing again.
40
- *
41
- * A 1,000-row import is one announce because the importer announces after its
42
- * loop, not per row. This covers the other burst — many small writes arriving
43
- * separately, a form under load or an automation fanning out — where each one
44
- * would otherwise pay a full scan per site and hand the tenant the same path
45
- * list again.
46
- *
47
- * Leading edge, so the FIRST write of a burst refreshes immediately; what is
48
- * skipped is a repeat inside the window, and the next write after it announces
49
- * again. The cost of the skip is bounded by the window: a row written during
50
- * one is visible no later than the next announce.
51
- */
52
- export declare const DATASET_ANNOUNCE_WINDOW_MS = 2000;
53
- /** Test seam: the window is process-wide state, so a spec has to reset it. */
54
- export declare function resetDatasetAnnounceThrottle(): void;
55
- /** One site's share of a dataset change. */
56
- export interface DatasetLivePageTarget {
57
- hostId: string;
58
- /** The tenant keys its cache on this, never on `hostId`. */
59
- subdomain: string;
60
- /** The attached custom domain, whose pages live under a second cache key. */
61
- cname?: string;
62
- /** Site-absolute addresses (`/`, `/team`) that repeat over the dataset. */
63
- paths: string[];
64
- /** The site held more documents than the scan read, so `paths` is partial. */
65
- truncated: boolean;
66
- }
67
- export interface DatasetLivePageScope {
68
- targets: DatasetLivePageTarget[];
69
- /**
70
- * Sites the fan-out cap left out. Zero in the ordinary case, and reported
71
- * rather than logged because a caller that says "refreshed" while some sites
72
- * were skipped is the failure this whole arc exists to remove.
73
- */
74
- hostsDropped: number;
75
- /**
76
- * Why there is nothing to announce, or `'ok'`.
77
- *
78
- * `'not-rendered'` is a success: a dataset no page repeats over has no live
79
- * page to refresh. It is kept apart from `'error'` for the same reason the
80
- * revalidate route keeps `not-routed` apart from a refusal — one means there
81
- * was nothing to do, the other means something was left stale.
82
- */
83
- reason: 'ok' | 'no-dataset' | 'no-sites' | 'not-rendered' | 'error';
84
- }
85
- /**
86
- * Every live screen whose rendered output repeats over the dataset, however
87
- * indirectly.
88
- *
89
- * The same three-bucket closure a placed form's pages are found by, for the
90
- * same reason: a binding is found by SEARCHING node trees rather than by
91
- * matching a pointer, so it can sit on the screen, in the chrome of the layout
92
- * the screen renders inside, or inside a reusable component a third component
93
- * nests. After the first level the fan-out is identical, so it is delegated
94
- * rather than restated.
95
- *
96
- * Pure, and separate from the Firestore read, so the closure is testable
97
- * without a database.
98
- */
99
- export declare function screenIdsRepeatingDataset(keys: ReadonlySet<string>, sources: UsageSources): string[];
100
- /**
101
- * Every live page a change to this dataset's records makes stale, per site.
102
- *
103
- * Reads only; announcing is the caller's, because the two runtimes announce
104
- * differently — the console posts to the tenant over the shared secret, and
105
- * the tenant drops its own caches in process. Splitting it here is what lets
106
- * both use one answer to the question of WHICH pages.
107
- *
108
- * Never throws. A scope that could not be computed is a scope of nothing, and
109
- * the TTL underneath is unaffected.
110
- */
111
- export declare function datasetLivePageScope(options: {
112
- firestore: Firestore;
113
- orgId: string;
114
- datasetId: string;
115
- }): Promise<DatasetLivePageScope>;
116
- /**
117
- * How one runtime drops a site's cached pages.
118
- *
119
- * The generic seam, registered by the deployment that owns the caches. The
120
- * tenant registers an in-process drop at boot; the console announces over the
121
- * shared secret and passes its own dropper at the call site. Core names
122
- * neither, exactly as it names no repeat source.
123
- *
124
- * Returns whether the drop landed. It must never throw: everything above it
125
- * has already been written.
126
- */
127
- export type LivePageDropper = (target: DatasetLivePageTarget) => Promise<boolean>;
128
- /**
129
- * Registers the dropper for this process, replacing any previous one. Returns
130
- * the unregister, which removes it only if it is still the one registered.
131
- *
132
- * Called from a function the platform calls BY NAME at boot, never from a
133
- * module's top level: a bundler deletes a module imported only for its side
134
- * effect when its package says it has none (AGL-3025).
135
- */
136
- export declare function registerLivePageDropper(drop: LivePageDropper): () => void;
137
- export interface DatasetAnnounceResult {
138
- /** Sites whose cached pages were dropped. */
139
- sitesRefreshed: number;
140
- /** Pages named across those sites. */
141
- pathsRefreshed: number;
142
- /** Sites the drop was attempted on and refused. */
143
- sitesRefused: number;
144
- /** Sites past the fan-out cap, and sites whose scan was truncated. */
145
- hostsDropped: number;
146
- scanTruncated: boolean;
147
- /** `'throttled'` when a drop for this pair already went out just now. */
148
- reason: DatasetLivePageScope['reason'] | 'throttled' | 'no-dropper';
149
- }
150
- /**
151
- * Refresh every live page that shows this dataset's records.
152
- *
153
- * The one call a write path makes. BEST EFFORT, ALWAYS: it never throws and
154
- * never rejects, so a caller can `await` it inside the try that already guards
155
- * the write without any chance of turning a stored record into a failed one.
156
- *
157
- * `drop` overrides the registered dropper for callers that hold their own —
158
- * the console, whose announce is an authenticated request rather than a local
159
- * cache call.
160
- */
161
- export declare function announceDatasetRecordChange(options: {
162
- firestore: Firestore;
163
- orgId: string;
164
- datasetId: string;
165
- drop?: LivePageDropper;
166
- }): Promise<DatasetAnnounceResult>;
167
- /**
168
- * One sentence for a refresh that did not cover every page, or `null` when it
169
- * did.
170
- *
171
- * Beside the call rather than at each surface, so the console cannot word this
172
- * differently in two places — the same reason `describeRevalidateShortfall`
173
- * lives beside `revalidateLivePages`. `lead` names the act that succeeded,
174
- * because only that word differs between surfaces: a record is SAVED, and
175
- * telling someone who fixed a typo that they published it is a worse error
176
- * than the staleness the sentence is about.
177
- */
178
- export declare function describeDatasetAnnounceShortfall(result: DatasetAnnounceResult | null, lead?: string): string | null;
179
- export default announceDatasetRecordChange;