@aglyn/plugins-marketing 1.0.0-beta.244 → 1.0.0-beta.247

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/plugins-marketing",
3
- "version": "1.0.0-beta.244",
3
+ "version": "1.0.0-beta.247",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,21 +25,21 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.244",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.244",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.244",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.244",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.244",
33
- "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.244",
34
- "@aglyn/shared-ui-next": "1.0.0-beta.244",
35
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.244",
36
- "@aglyn/shared-util-email": "1.0.0-beta.244",
37
- "@aglyn/shared-util-http": "1.0.0-beta.244",
38
- "@aglyn/shared-util-timestamp": "1.0.0-beta.244",
39
- "@aglyn/shared-util-tools": "1.0.0-beta.244",
40
- "@aglyn/tenant-data-admin": "1.0.0-beta.244",
41
- "@aglyn/tenant-feature-instance": "1.0.0-beta.244",
42
- "@aglyn/tenant-runtime": "1.0.0-beta.244",
28
+ "@aglyn/aglyn": "1.0.0-beta.247",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.247",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.247",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.247",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.247",
33
+ "@aglyn/shared-ui-jsx-forms": "1.0.0-beta.247",
34
+ "@aglyn/shared-ui-next": "1.0.0-beta.247",
35
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.247",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.247",
37
+ "@aglyn/shared-util-http": "1.0.0-beta.247",
38
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.247",
39
+ "@aglyn/shared-util-tools": "1.0.0-beta.247",
40
+ "@aglyn/tenant-data-admin": "1.0.0-beta.247",
41
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.247",
42
+ "@aglyn/tenant-runtime": "1.0.0-beta.247",
43
43
  "@swc/helpers": "0.5.23"
44
44
  },
45
45
  "peerDependencies": {
@@ -15,4 +15,13 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /** How long an evaluated, unedited list is left alone. */
19
+ export declare const REEVALUATE_AFTER_MS: number;
20
+ /** Whether one list document is due for evaluation at `nowMs`. */
21
+ export declare function dynamicListIsDue(listDoc: {
22
+ get(field: string): unknown;
23
+ updateTime?: {
24
+ toMillis(): number;
25
+ } | null;
26
+ }, nowMs: number): boolean;
18
27
  export declare const listsMaterializeHandler: PluginApiHandler;
@@ -24,8 +24,17 @@ import { firebaseAdmin, materializeDynamicList } from "@aglyn/tenant-data-admin"
24
24
  * subcollection; without something re-running the rule, "dynamic" means
25
25
  * "evaluated once at creation". This is that something: it rides the existing
26
26
  * fifteen-minute Cloud Scheduler fast-cron beside the scheduled-campaign
27
- * processor, so a dynamic list is never more than about fifteen minutes stale
28
- * and no new scheduler job is provisioned.
27
+ * processor, so no new scheduler job is provisioned.
28
+ *
29
+ * ## A list is re-evaluated hourly, not every tick
30
+ *
31
+ * Every evaluation scans the WHOLE of the rule's silos on the list's site —
32
+ * every form submission, contact, lead and member — whether or not anything
33
+ * changed, so a fifteen-minute cadence costs four full scans an hour per
34
+ * list. A list evaluated within {@link REEVALUATE_AFTER_MS} is skipped unless
35
+ * it is mid-scan (`evaluationCursor`) or was edited since it was evaluated:
36
+ * the sweep's own write stamps `lastEvaluatedAt` with the server time, so a
37
+ * document `updateTime` past it is somebody else's write — a changed rule.
29
38
  *
30
39
  * ## It chunks, because `sweepConsoleCron` will follow a cursor
31
40
  *
@@ -44,6 +53,21 @@ import { firebaseAdmin, materializeDynamicList } from "@aglyn/tenant-data-admin"
44
53
  * document order from ever being evaluated — the failure would be invisible
45
54
  * and would land on whichever customers sort last.
46
55
  */ /** Dynamic lists evaluated per invocation. */ const LISTS_PER_CHUNK = 20;
56
+ /** How long an evaluated, unedited list is left alone. */ export const REEVALUATE_AFTER_MS = 60 * 60 * 1000;
57
+ /**
58
+ * The slack between the sweep's `lastEvaluatedAt` server timestamp and the
59
+ * `updateTime` of the same write, which Firestore commits together.
60
+ */ const OWN_WRITE_SLACK_MS = 2000;
61
+ /** Whether one list document is due for evaluation at `nowMs`. */ export function dynamicListIsDue(listDoc, nowMs) {
62
+ var _evaluated_toMillis, _listDoc_updateTime;
63
+ if (listDoc.get('evaluationCursor')) return true;
64
+ const evaluated = listDoc.get('lastEvaluatedAt');
65
+ const evaluatedMs = evaluated == null ? void 0 : (_evaluated_toMillis = evaluated.toMillis) == null ? void 0 : _evaluated_toMillis.call(evaluated);
66
+ if (typeof evaluatedMs !== 'number') return true;
67
+ if (nowMs - evaluatedMs >= REEVALUATE_AFTER_MS) return true;
68
+ const updatedMs = (_listDoc_updateTime = listDoc.updateTime) == null ? void 0 : _listDoc_updateTime.toMillis();
69
+ return typeof updatedMs === 'number' && updatedMs - evaluatedMs > OWN_WRITE_SLACK_MS;
70
+ }
47
71
  export const listsMaterializeHandler = async (req, res)=>{
48
72
  var _ref;
49
73
  var _req_body;
@@ -82,9 +106,15 @@ export const listsMaterializeHandler = async (req, res)=>{
82
106
  const lists = await query.get();
83
107
  const results = [];
84
108
  let lastPath = '';
109
+ let skipped = 0;
110
+ const nowMs = Date.now();
85
111
  for (const listDoc of lists.docs){
86
112
  var _listDoc_get;
87
113
  lastPath = listDoc.ref.path;
114
+ if (!dynamicListIsDue(listDoc, nowMs)) {
115
+ skipped += 1;
116
+ continue;
117
+ }
88
118
  const hostId = String((_listDoc_get = listDoc.get('hostId')) != null ? _listDoc_get : '');
89
119
  if (!hostId) {
90
120
  // A dynamic list with no host has no silos to draw from. Reported
@@ -119,6 +149,7 @@ export const listsMaterializeHandler = async (req, res)=>{
119
149
  return res.status(200).json({
120
150
  ok: true,
121
151
  evaluated: results.length,
152
+ skipped,
122
153
  results,
123
154
  done,
124
155
  // The caller refuses to loop on `done:false` with no cursor, which is the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/lists-materialize.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 { writeCronBeat, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n materializeDynamicList,\n type DynamicListCursor,\n} from '@aglyn/tenant-data-admin'\n\n/**\n * The scheduled sweep that keeps dynamic lists fresh\n * (`docs/specs/email-overhaul.md` §3e).\n *\n * A dynamic list stores a rule and materializes into its `members`\n * subcollection; without something re-running the rule, \"dynamic\" means\n * \"evaluated once at creation\". This is that something: it rides the existing\n * fifteen-minute Cloud Scheduler fast-cron beside the scheduled-campaign\n * processor, so a dynamic list is never more than about fifteen minutes stale\n * and no new scheduler job is provisioned.\n *\n * ## It chunks, because `sweepConsoleCron` will follow a cursor\n *\n * The caller in `cloud/functions` drives a route to completion by following\n * `nextCursor`, so this answers `{ done, nextCursor }` rather than trying to\n * sweep every list in one invocation. Two cursors are in play and they are\n * different things: `nextCursor` here is a position in the LIST of lists, and\n * the `evaluationCursor` stored on each list document is a position inside\n * one list's own scan. Both resume; neither is allowed to shorten a\n * membership.\n *\n * ## A failing list must not stop the sweep\n *\n * Each list is evaluated in its own try. One org's malformed rule, deleted\n * segment or missing host would otherwise stop every list after it in\n * document order from ever being evaluated — the failure would be invisible\n * and would land on whichever customers sort last.\n */\n\n/** Dynamic lists evaluated per invocation. */\nconst LISTS_PER_CHUNK = 20\n\nexport const listsMaterializeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res\n .status(501)\n .json({ error: 'Dynamic lists are not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n\n const firestore = firebaseAdmin.app().firestore()\n /*\n * The mark `/api/health/crons` reads to notice this job going away\n * (AGL-1955). Nothing downstream ages when a dynamic list stops being\n * re-evaluated — a stale membership looks exactly like a fresh one whose\n * population happens not to have changed — so the invocation itself is the\n * only honest thing to watch.\n */\n await writeCronBeat(firestore, 'lists-materialize')\n\n const cursor = String((req.body as Record<string, unknown>)?.['cursor'] ?? '')\n let query = firestore\n .collectionGroup('lists')\n .where('kind', '==', 'dynamic')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(LISTS_PER_CHUNK)\n /*\n * A DocumentReference, not the raw string. A collection-group cursor on\n * `documentId()` is compared against the FULL resource path, and a bare id\n * would either be rejected or silently compared against the wrong thing —\n * so the cursor travels as a path and is turned back into a reference here.\n */\n if (cursor) query = query.startAfter(firestore.doc(cursor))\n\n const lists = await query.get()\n const results: Array<Record<string, unknown>> = []\n let lastPath = ''\n for (const listDoc of lists.docs) {\n lastPath = listDoc.ref.path\n const hostId = String(listDoc.get('hostId') ?? '')\n if (!hostId) {\n // A dynamic list with no host has no silos to draw from. Reported\n // rather than thrown: it is a data problem on one list, and the sweep\n // has other lists to get to.\n results.push({ list: listDoc.ref.path, error: 'no hostId' })\n continue\n }\n try {\n const outcome = await materializeDynamicList({\n listRef: listDoc.ref,\n hostId,\n rule: listDoc.get('rule'),\n resume: (listDoc.get('evaluationCursor') as DynamicListCursor) ?? null,\n })\n results.push({ list: listDoc.ref.path, ...outcome })\n } catch (error) {\n console.error('dynamic list materialize failed', listDoc.ref.path, error)\n results.push({ list: listDoc.ref.path, error: 'evaluation failed' })\n }\n }\n\n const done = lists.size < LISTS_PER_CHUNK\n return res.status(200).json({\n ok: true,\n evaluated: results.length,\n results,\n done,\n // The caller refuses to loop on `done:false` with no cursor, which is the\n // one shape that would re-read the same chunk for ever.\n nextCursor: done ? null : lastPath,\n })\n}\n"],"names":["writeCronBeat","firebaseAdmin","materializeDynamicList","LISTS_PER_CHUNK","listsMaterializeHandler","req","res","method","status","json","error","cronSecret","process","env","CRON_SECRET","headers","firestore","app","cursor","String","body","query","collectionGroup","where","orderBy","FieldPath","documentId","limit","startAfter","doc","lists","get","results","lastPath","listDoc","docs","ref","path","hostId","push","list","outcome","listRef","rule","resume","console","done","size","ok","evaluated","length","nextCursor"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAA+B,sBAAqB;AAC1E,SACEC,aAAa,EACbC,sBAAsB,QAEjB,2BAA0B;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,4CAA4C,GAC5C,MAAMC,kBAAkB;AAExB,OAAO,MAAMC,0BAA4C,OAAOC,KAAKC;;QAwB5CD;IAvBvB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,aAAaC,QAAQC,GAAG,CAACC,WAAW;IAC1C,IAAI,CAACH,YAAY;QACf,OAAOL,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEC,OAAO;QAAkD;IACrE;IACA,IAAIL,IAAIU,OAAO,CAAC,gBAAgB,KAAKJ,YAAY;QAC/C,OAAOL,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAkB;IACzD;IAEA,MAAMM,YAAYf,cAAcgB,GAAG,GAAGD,SAAS;IAC/C;;;;;;GAMC,GACD,MAAMhB,cAAcgB,WAAW;IAE/B,MAAME,SAASC,gBAAQd,YAAAA,IAAIe,IAAI,qBAAT,AAACf,SAAsC,CAAC,SAAS,mBAAI;IAC3E,IAAIgB,QAAQL,UACTM,eAAe,CAAC,SAChBC,KAAK,CAAC,QAAQ,MAAM,WACpBC,OAAO,CAACvB,cAAce,SAAS,CAACS,SAAS,CAACC,UAAU,IACpDC,KAAK,CAACxB;IACT;;;;;GAKC,GACD,IAAIe,QAAQG,QAAQA,MAAMO,UAAU,CAACZ,UAAUa,GAAG,CAACX;IAEnD,MAAMY,QAAQ,MAAMT,MAAMU,GAAG;IAC7B,MAAMC,UAA0C,EAAE;IAClD,IAAIC,WAAW;IACf,KAAK,MAAMC,WAAWJ,MAAMK,IAAI,CAAE;YAEVD;QADtBD,WAAWC,QAAQE,GAAG,CAACC,IAAI;QAC3B,MAAMC,SAASnB,QAAOe,eAAAA,QAAQH,GAAG,CAAC,qBAAZG,eAAyB;QAC/C,IAAI,CAACI,QAAQ;YACX,kEAAkE;YAClE,sEAAsE;YACtE,6BAA6B;YAC7BN,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;gBAAE3B,OAAO;YAAY;YAC1D;QACF;QACA,IAAI;gBAKSwB;YAJX,MAAMO,UAAU,MAAMvC,uBAAuB;gBAC3CwC,SAASR,QAAQE,GAAG;gBACpBE;gBACAK,MAAMT,QAAQH,GAAG,CAAC;gBAClBa,MAAM,GAAGV,gBAAAA,QAAQH,GAAG,CAAC,+BAAZG,gBAAyD;YACpE;YACAF,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;eAAKI;QAC5C,EAAE,OAAO/B,OAAO;YACdmC,QAAQnC,KAAK,CAAC,mCAAmCwB,QAAQE,GAAG,CAACC,IAAI,EAAE3B;YACnEsB,QAAQO,IAAI,CAAC;gBAAEC,MAAMN,QAAQE,GAAG,CAACC,IAAI;gBAAE3B,OAAO;YAAoB;QACpE;IACF;IAEA,MAAMoC,OAAOhB,MAAMiB,IAAI,GAAG5C;IAC1B,OAAOG,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAC1BuC,IAAI;QACJC,WAAWjB,QAAQkB,MAAM;QACzBlB;QACAc;QACA,0EAA0E;QAC1E,wDAAwD;QACxDK,YAAYL,OAAO,OAAOb;IAC5B;AACF,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketing/src/lib/server/lists-materialize.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 { writeCronBeat, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n materializeDynamicList,\n type DynamicListCursor,\n} from '@aglyn/tenant-data-admin'\n\n/**\n * The scheduled sweep that keeps dynamic lists fresh\n * (`docs/specs/email-overhaul.md` §3e).\n *\n * A dynamic list stores a rule and materializes into its `members`\n * subcollection; without something re-running the rule, \"dynamic\" means\n * \"evaluated once at creation\". This is that something: it rides the existing\n * fifteen-minute Cloud Scheduler fast-cron beside the scheduled-campaign\n * processor, so no new scheduler job is provisioned.\n *\n * ## A list is re-evaluated hourly, not every tick\n *\n * Every evaluation scans the WHOLE of the rule's silos on the list's site —\n * every form submission, contact, lead and member — whether or not anything\n * changed, so a fifteen-minute cadence costs four full scans an hour per\n * list. A list evaluated within {@link REEVALUATE_AFTER_MS} is skipped unless\n * it is mid-scan (`evaluationCursor`) or was edited since it was evaluated:\n * the sweep's own write stamps `lastEvaluatedAt` with the server time, so a\n * document `updateTime` past it is somebody else's write — a changed rule.\n *\n * ## It chunks, because `sweepConsoleCron` will follow a cursor\n *\n * The caller in `cloud/functions` drives a route to completion by following\n * `nextCursor`, so this answers `{ done, nextCursor }` rather than trying to\n * sweep every list in one invocation. Two cursors are in play and they are\n * different things: `nextCursor` here is a position in the LIST of lists, and\n * the `evaluationCursor` stored on each list document is a position inside\n * one list's own scan. Both resume; neither is allowed to shorten a\n * membership.\n *\n * ## A failing list must not stop the sweep\n *\n * Each list is evaluated in its own try. One org's malformed rule, deleted\n * segment or missing host would otherwise stop every list after it in\n * document order from ever being evaluated — the failure would be invisible\n * and would land on whichever customers sort last.\n */\n\n/** Dynamic lists evaluated per invocation. */\nconst LISTS_PER_CHUNK = 20\n\n/** How long an evaluated, unedited list is left alone. */\nexport const REEVALUATE_AFTER_MS = 60 * 60 * 1000\n\n/**\n * The slack between the sweep's `lastEvaluatedAt` server timestamp and the\n * `updateTime` of the same write, which Firestore commits together.\n */\nconst OWN_WRITE_SLACK_MS = 2_000\n\n/** Whether one list document is due for evaluation at `nowMs`. */\nexport function dynamicListIsDue(\n listDoc: {\n get(field: string): unknown\n updateTime?: { toMillis(): number } | null\n },\n nowMs: number,\n): boolean {\n if (listDoc.get('evaluationCursor')) return true\n const evaluated = listDoc.get('lastEvaluatedAt') as\n | { toMillis?: () => number }\n | null\n | undefined\n const evaluatedMs = evaluated?.toMillis?.()\n if (typeof evaluatedMs !== 'number') return true\n if (nowMs - evaluatedMs >= REEVALUATE_AFTER_MS) return true\n const updatedMs = listDoc.updateTime?.toMillis()\n return typeof updatedMs === 'number' && updatedMs - evaluatedMs > OWN_WRITE_SLACK_MS\n}\n\nexport const listsMaterializeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res\n .status(501)\n .json({ error: 'Dynamic lists are not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n\n const firestore = firebaseAdmin.app().firestore()\n /*\n * The mark `/api/health/crons` reads to notice this job going away\n * (AGL-1955). Nothing downstream ages when a dynamic list stops being\n * re-evaluated — a stale membership looks exactly like a fresh one whose\n * population happens not to have changed — so the invocation itself is the\n * only honest thing to watch.\n */\n await writeCronBeat(firestore, 'lists-materialize')\n\n const cursor = String((req.body as Record<string, unknown>)?.['cursor'] ?? '')\n let query = firestore\n .collectionGroup('lists')\n .where('kind', '==', 'dynamic')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(LISTS_PER_CHUNK)\n /*\n * A DocumentReference, not the raw string. A collection-group cursor on\n * `documentId()` is compared against the FULL resource path, and a bare id\n * would either be rejected or silently compared against the wrong thing —\n * so the cursor travels as a path and is turned back into a reference here.\n */\n if (cursor) query = query.startAfter(firestore.doc(cursor))\n\n const lists = await query.get()\n const results: Array<Record<string, unknown>> = []\n let lastPath = ''\n let skipped = 0\n const nowMs = Date.now()\n for (const listDoc of lists.docs) {\n lastPath = listDoc.ref.path\n if (!dynamicListIsDue(listDoc, nowMs)) {\n skipped += 1\n continue\n }\n const hostId = String(listDoc.get('hostId') ?? '')\n if (!hostId) {\n // A dynamic list with no host has no silos to draw from. Reported\n // rather than thrown: it is a data problem on one list, and the sweep\n // has other lists to get to.\n results.push({ list: listDoc.ref.path, error: 'no hostId' })\n continue\n }\n try {\n const outcome = await materializeDynamicList({\n listRef: listDoc.ref,\n hostId,\n rule: listDoc.get('rule'),\n resume: (listDoc.get('evaluationCursor') as DynamicListCursor) ?? null,\n })\n results.push({ list: listDoc.ref.path, ...outcome })\n } catch (error) {\n console.error('dynamic list materialize failed', listDoc.ref.path, error)\n results.push({ list: listDoc.ref.path, error: 'evaluation failed' })\n }\n }\n\n const done = lists.size < LISTS_PER_CHUNK\n return res.status(200).json({\n ok: true,\n evaluated: results.length,\n skipped,\n results,\n done,\n // The caller refuses to loop on `done:false` with no cursor, which is the\n // one shape that would re-read the same chunk for ever.\n nextCursor: done ? null : lastPath,\n })\n}\n"],"names":["writeCronBeat","firebaseAdmin","materializeDynamicList","LISTS_PER_CHUNK","REEVALUATE_AFTER_MS","OWN_WRITE_SLACK_MS","dynamicListIsDue","listDoc","nowMs","evaluated","get","evaluatedMs","toMillis","updatedMs","updateTime","listsMaterializeHandler","req","res","method","status","json","error","cronSecret","process","env","CRON_SECRET","headers","firestore","app","cursor","String","body","query","collectionGroup","where","orderBy","FieldPath","documentId","limit","startAfter","doc","lists","results","lastPath","skipped","Date","now","docs","ref","path","hostId","push","list","outcome","listRef","rule","resume","console","done","size","ok","length","nextCursor"],"mappings":"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"}