@aglyn/aglyn 1.0.0-beta.239 → 1.0.0-beta.240
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 +11 -11
- package/src/lib/app-utils/error-beacon.d.ts +39 -0
- package/src/lib/app-utils/error-beacon.js +66 -9
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +8 -3
- package/src/lib/app-utils/expand-repeatables.js +46 -17
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.240",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
46
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
47
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
48
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
49
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
40
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.240",
|
|
41
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.240",
|
|
42
|
+
"@aglyn/shared-data-types": "1.0.0-beta.240",
|
|
43
|
+
"@aglyn/shared-util-email": "1.0.0-beta.240",
|
|
44
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.240",
|
|
45
|
+
"@aglyn/shared-util-http": "1.0.0-beta.240",
|
|
46
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.240",
|
|
47
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.240",
|
|
48
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.240",
|
|
49
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.240",
|
|
50
50
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
51
51
|
"@msgpack/msgpack": "^3.1.3",
|
|
52
52
|
"@types/unist": "^3.0.3",
|
|
@@ -32,6 +32,15 @@ export interface ErrorBeaconEvent {
|
|
|
32
32
|
col?: number;
|
|
33
33
|
/** Page URL, scrubbed to origin + pathname. */
|
|
34
34
|
url: string;
|
|
35
|
+
/**
|
|
36
|
+
* On a `hydration` event only: the page translator whose rewrite is
|
|
37
|
+
* visible in the document when React gave up — see
|
|
38
|
+
* {@link pageTranslationSignal}. Absent when nothing visibly translated
|
|
39
|
+
* the page, which is the case that is worth reading.
|
|
40
|
+
*/
|
|
41
|
+
translated?: string;
|
|
42
|
+
/** On a `hydration` event only: `navigator.language`, clamped. */
|
|
43
|
+
language?: string;
|
|
35
44
|
}
|
|
36
45
|
export interface ErrorBeaconOptions {
|
|
37
46
|
/** POST target; same-origin. Default `/api/errors`. */
|
|
@@ -125,6 +134,36 @@ export declare const CHUNK_LOAD_KIND = "chunk-load";
|
|
|
125
134
|
*/
|
|
126
135
|
export declare function recoveredErrorKind(error: unknown): string | undefined;
|
|
127
136
|
export declare function isHydrationMismatch(message: string): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* WHICH TRANSLATOR rewrote this page's text, read off the document, or
|
|
139
|
+
* undefined when none visibly did.
|
|
140
|
+
*
|
|
141
|
+
* A hydration report on its own cannot say whether React disagreed with our
|
|
142
|
+
* server render or with a translator that rewrote the server's text before
|
|
143
|
+
* React reached it, and the two look identical in `client-errors`: the
|
|
144
|
+
* minified #418, a stack inside the react-dom chunk, the page URL, nothing
|
|
145
|
+
* else. aglyn.com/ carried eight of them between 2026-09-22 and 2026-10-10.
|
|
146
|
+
* The visitor at 2026-10-09 22:28Z went on to sign up two minutes later with
|
|
147
|
+
* Chrome Translate rewriting the console (the `<font>` crash #1342 fixed); the
|
|
148
|
+
* two on 2026-09-22/23 were desktop Chrome OS in Hyderabad, while every other
|
|
149
|
+
* Indian, Pakistani and Turkish home-page visit in GA that fortnight hydrated
|
|
150
|
+
* without a report — so neither the build nor the time zone was the variable,
|
|
151
|
+
* and the beacon had nothing to say which visitor it was.
|
|
152
|
+
*
|
|
153
|
+
* The signatures are the ones each translator leaves on the DOM it rewrote:
|
|
154
|
+
*
|
|
155
|
+
* - Chrome (and every Chromium that ships Google Translate) sets
|
|
156
|
+
* `translated-ltr` / `translated-rtl` on `<html>` and wraps each rewritten
|
|
157
|
+
* run in `<font style="vertical-align: inherit;">`.
|
|
158
|
+
* - Microsoft Translator (Edge) stamps `_msttexthash` / `_msthash` on every
|
|
159
|
+
* element whose text it replaced.
|
|
160
|
+
* - Any translator that does the job properly rewrites `<html lang>`, which is
|
|
161
|
+
* compared against the `servedLang` the beacon read at install, before
|
|
162
|
+
* hydration began.
|
|
163
|
+
*
|
|
164
|
+
* Never throws: it runs inside the error handler.
|
|
165
|
+
*/
|
|
166
|
+
export declare function pageTranslationSignal(doc: Document, servedLang: string | null | undefined): string | undefined;
|
|
128
167
|
/**
|
|
129
168
|
* WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).
|
|
130
169
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -229,6 +230,51 @@ const HYDRATION_TEXT = /Hydration failed because|Text content does not match ser
|
|
|
229
230
|
export function isHydrationMismatch(message) {
|
|
230
231
|
return HYDRATION_MINIFIED.test(message) || HYDRATION_TEXT.test(message);
|
|
231
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* WHICH TRANSLATOR rewrote this page's text, read off the document, or
|
|
235
|
+
* undefined when none visibly did.
|
|
236
|
+
*
|
|
237
|
+
* A hydration report on its own cannot say whether React disagreed with our
|
|
238
|
+
* server render or with a translator that rewrote the server's text before
|
|
239
|
+
* React reached it, and the two look identical in `client-errors`: the
|
|
240
|
+
* minified #418, a stack inside the react-dom chunk, the page URL, nothing
|
|
241
|
+
* else. aglyn.com/ carried eight of them between 2026-09-22 and 2026-10-10.
|
|
242
|
+
* The visitor at 2026-10-09 22:28Z went on to sign up two minutes later with
|
|
243
|
+
* Chrome Translate rewriting the console (the `<font>` crash #1342 fixed); the
|
|
244
|
+
* two on 2026-09-22/23 were desktop Chrome OS in Hyderabad, while every other
|
|
245
|
+
* Indian, Pakistani and Turkish home-page visit in GA that fortnight hydrated
|
|
246
|
+
* without a report — so neither the build nor the time zone was the variable,
|
|
247
|
+
* and the beacon had nothing to say which visitor it was.
|
|
248
|
+
*
|
|
249
|
+
* The signatures are the ones each translator leaves on the DOM it rewrote:
|
|
250
|
+
*
|
|
251
|
+
* - Chrome (and every Chromium that ships Google Translate) sets
|
|
252
|
+
* `translated-ltr` / `translated-rtl` on `<html>` and wraps each rewritten
|
|
253
|
+
* run in `<font style="vertical-align: inherit;">`.
|
|
254
|
+
* - Microsoft Translator (Edge) stamps `_msttexthash` / `_msthash` on every
|
|
255
|
+
* element whose text it replaced.
|
|
256
|
+
* - Any translator that does the job properly rewrites `<html lang>`, which is
|
|
257
|
+
* compared against the `servedLang` the beacon read at install, before
|
|
258
|
+
* hydration began.
|
|
259
|
+
*
|
|
260
|
+
* Never throws: it runs inside the error handler.
|
|
261
|
+
*/ export function pageTranslationSignal(doc, servedLang) {
|
|
262
|
+
try {
|
|
263
|
+
const root = doc.documentElement;
|
|
264
|
+
if (!root) return undefined;
|
|
265
|
+
if (root.classList.contains('translated-ltr') || root.classList.contains('translated-rtl') || doc.querySelector('font[style*="vertical-align: inherit"]')) {
|
|
266
|
+
return 'chrome';
|
|
267
|
+
}
|
|
268
|
+
if (doc.querySelector('[_msttexthash], [_msthash]')) return 'microsoft';
|
|
269
|
+
const lang = root.getAttribute('lang');
|
|
270
|
+
if (servedLang && lang && lang.toLowerCase() !== servedLang.toLowerCase()) {
|
|
271
|
+
return `lang:${clamp(lang, 16)}`;
|
|
272
|
+
}
|
|
273
|
+
} catch (unused) {
|
|
274
|
+
// An unreadable document leaves the event unlabeled, never unreported.
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
232
278
|
/**
|
|
233
279
|
* WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).
|
|
234
280
|
*
|
|
@@ -351,7 +397,8 @@ let installed = false;
|
|
|
351
397
|
* Installs the handlers once per page. Safe to call from module scope of a
|
|
352
398
|
* client bundle: it no-ops during SSR and on repeat calls.
|
|
353
399
|
*/ export function installErrorBeacon(options) {
|
|
354
|
-
var _ref, _ref1, _ref2;
|
|
400
|
+
var _ref, _ref1, _ref2, _ref3;
|
|
401
|
+
var _document_documentElement;
|
|
355
402
|
if (typeof window === 'undefined' || installed) return;
|
|
356
403
|
installed = true;
|
|
357
404
|
const endpoint = (_ref = options == null ? void 0 : options.endpoint) != null ? _ref : '/api/errors';
|
|
@@ -361,6 +408,9 @@ let installed = false;
|
|
|
361
408
|
// (the rejection handler below), because a tab left on a dead deploy is a
|
|
362
409
|
// visitor's problem whether or not its errors are counted.
|
|
363
410
|
const sampled = Math.random() < sampleRate;
|
|
411
|
+
// The document's language as SERVED, read before hydration and before any
|
|
412
|
+
// translator has had a chance to rewrite it (`pageTranslationSignal`).
|
|
413
|
+
const servedLang = (_ref3 = (_document_documentElement = document.documentElement) == null ? void 0 : _document_documentElement.getAttribute('lang')) != null ? _ref3 : null;
|
|
364
414
|
if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
|
|
365
415
|
const seen = new Set();
|
|
366
416
|
let queued = [];
|
|
@@ -424,20 +474,27 @@ let installed = false;
|
|
|
424
474
|
// clamp still makes the error ours.
|
|
425
475
|
if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return;
|
|
426
476
|
const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
477
|
+
// MARKED, not dropped: a translator causes a hydration mismatch and so
|
|
478
|
+
// does a real render divergence, and a recovered fault is still a
|
|
479
|
+
// fault. Only a rate tells either apart from noise — and the
|
|
480
|
+
// translator's own mark on the DOM, when it left one.
|
|
481
|
+
const kind = (_recoveredErrorKind = recoveredErrorKind(error != null ? error : {
|
|
482
|
+
message
|
|
483
|
+
})) != null ? _recoveredErrorKind : isHydrationMismatch(message) ? 'hydration' : 'error';
|
|
484
|
+
const translated = kind === 'hydration' ? pageTranslationSignal(document, servedLang) : undefined;
|
|
485
|
+
enqueue(_extends({
|
|
486
|
+
kind,
|
|
434
487
|
message,
|
|
435
488
|
stack,
|
|
436
489
|
source: scrubUrl(event.filename) || undefined,
|
|
437
490
|
line: typeof event.lineno === 'number' ? event.lineno : undefined,
|
|
438
491
|
col: typeof event.colno === 'number' ? event.colno : undefined,
|
|
439
492
|
url: pageUrl
|
|
440
|
-
}
|
|
493
|
+
}, translated ? {
|
|
494
|
+
translated
|
|
495
|
+
} : {}, kind === 'hydration' && navigator.language ? {
|
|
496
|
+
language: clamp(navigator.language, 16)
|
|
497
|
+
} : {}));
|
|
441
498
|
} catch (unused) {
|
|
442
499
|
// Never rethrow from an error handler.
|
|
443
500
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/error-beacon.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 * First-party browser error beacon (AGL-1538).\n *\n * Crashlytics does not exist for web, and adding a third-party error vendor\n * is a subprocessor-list decision that was explicitly deferred. This is the\n * first-party alternative: `window.onerror` / `unhandledrejection` handlers\n * that batch uncaught failures to the app's own `/api/errors` route, which\n * forwards them to Google Cloud Error Reporting — a processor already on the\n * list, alerting through the GCP stack that already pages (AGL-1502).\n *\n * Design constraints, in order:\n *\n * - **It must never become the outage.** Everything is wrapped; a failure to\n * report is silence, not a thrown error. Batches are capped per page so an\n * error thrown in a loop cannot DDoS our own API — after `maxPerPage`\n * events the beacon disarms for the rest of the pageview.\n * - **No PII.** URLs are scrubbed to origin + pathname (query strings and\n * fragments carry tokens and search terms), no user id, no cookies —\n * the payload is the error text and where in the CODE it happened.\n * Cross-origin scripts surface as the browser's opaque \"Script error.\"\n * with no stack; those are dropped since they cannot be acted on.\n * - **Dedupe.** One page throwing the same error on every frame reports it\n * once.\n *\n * Framework-free on purpose: the console mounts it from its root layout, the\n * tenant runtime from its own, and neither needs React here. Deliberately NO\n * 'use client' directive — inside this shared lib the directive forks the\n * module graph (AGL-52); every importer is already a client module.\n */\n\nimport { isCrossPoolDesync } from './cross-pool-desync'\nimport { isForeignScriptUrl, stackFrameUrls } from './foreign-script'\nimport { isStaleBuildError, recoverStaleBuildRejection } from './stale-build-error'\n\nexport interface ErrorBeaconEvent {\n /**\n * Which handler caught it — 'error' | 'unhandledrejection' — or the\n * family it was labeled with: 'hydration', {@link AUTH_DESYNC_KIND},\n * {@link CHUNK_LOAD_KIND}, or a caller's own kind for a handled error.\n * Lands at `jsonPayload.kind`, where the alert policies filter on it.\n */\n kind: string\n /** Error message, clamped. */\n message: string\n /** Stack trace when the thrown value carried one, clamped. */\n stack?: string\n /** Script URL for stackless `window.onerror` events, scrubbed. */\n source?: string\n line?: number\n col?: number\n /** Page URL, scrubbed to origin + pathname. */\n url: string\n}\n\nexport interface ErrorBeaconOptions {\n /** POST target; same-origin. Default `/api/errors`. */\n endpoint?: string\n /** 0..1 — fraction of pageviews that report at all. Default 1. */\n sampleRate?: number\n /** Max events reported per pageview before the beacon disarms. */\n maxPerPage?: number\n}\n\nconst MAX_MESSAGE = 1_024\nconst MAX_STACK = 8_192\nconst FLUSH_DELAY_MS = 2_000\n\n/** Origin + pathname only — query strings and fragments never leave. */\nfunction scrubUrl(raw: string | null | undefined): string {\n if (!raw) return ''\n try {\n const url = new URL(String(raw), window.location.href)\n return `${url.origin}${url.pathname}`\n } catch {\n return ''\n }\n}\n\nfunction clamp(value: unknown, max: number): string {\n return String(value ?? '').slice(0, max)\n}\n\n/**\n * Was this thrown by a script somebody ELSE evaluated into our page?\n *\n * Three tells, all read off the frames' positions and none off a function\n * name — a name would be a denylist needing a new entry per vendor.\n *\n * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on\n * aglyn.com/pricing, reported as an error of ours:\n *\n * sendDataToNative@https://aglyn.com/pricing:1:1325\n * sendPageHideMessage@https://aglyn.com/pricing:1:4139\n * @https://aglyn.com/pricing:1:6257\n *\n * `sendDataToNative` is the Meta in-app browser's native bridge, injected by\n * the Facebook/Instagram webview. In WebKit's format the code was evaluated\n * inline, so every frame is the document rather than a script we served.\n *\n * **Every frame is under a FOREIGN scheme.** Measured 2026-09-10 (AGL-2786) on\n * aglyn.com, the same bridge in the Android webview:\n *\n * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)\n *\n * That webview names its scripts under its own `iabjs://` scheme, and\n * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.\n *\n * **Every frame is a script with NO URL** (AGL-3423). Measured on the\n * sign-up page and on aglyn.com/solutions/agencies:\n *\n * at eval (<anonymous>)\n * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)\n *\n * at <anonymous>:1:54\n * at <anonymous>:1:64\n *\n * An automation driver's `evaluate` and a snippet pasted into devtools. Every\n * script we serve has a URL — a chunk, a CDN asset, or the document for an\n * inline one — so a frame whose position is `<anonymous>:line:col`, or code\n * `eval`'d by such a frame, was not served by us. See\n * {@link anonymousScriptFrameCount} for exactly which frames count.\n *\n * ⚑ Deliberately `every`, and deliberately compared against the document\n * rather than against our asset path. A rule like \"no frame under\n * `/_next/static/`\" would delete EVERY error from a self-hosted deployment\n * that serves its assets from a CDN, because none of its frames would match.\n * Comparing to the document cannot fail that way: a CDN frame is not the\n * document either, so it keeps the report.\n *\n * A stack may mix the tells frame by frame. Nothing we ship can be fixed in\n * response to any of them, which is the same test the opaque `Script error.`\n * cut already applies.\n *\n * The cost is honest and small: a throw from one of our own inline\n * bootstrap scripts looks the same and is dropped with it. That trade buys a\n * rule that needs no maintenance as new webviews and extensions appear.\n */\nexport function isInjectedThirdPartyFrame(\n stack: string,\n documentUrl: string,\n): boolean {\n const urls = stackFrameUrls(stack)\n // No frames parsed is not evidence of anything — keep the report. A stack\n // with neither a URL nor an anonymous-script frame is exactly that: an\n // empty frame list must never read as \"every frame is injected\".\n if (!urls.length && !anonymousScriptFrameCount(stack)) return false\n // Anonymous frames need no test of their own — they are injected by\n // definition — so `every` over the URLs is `every` over the whole stack.\n // One URL of ours, even as the origin of an `eval`, keeps the report.\n return urls.every(\n (url) => isForeignScriptUrl(url) || scrubUrl(url) === documentUrl,\n )\n}\n\n/** A frame line, in V8's `at …` format or WebKit/Firefox's `fn@location`. */\nconst STACK_FRAME_LINE = /^\\s*at\\s|@/\n\n/**\n * A position in a script that has no URL.\n *\n * - `<anonymous>:line:col` — V8's name for such a script, both for a top-level\n * snippet and as the position of code run through `eval`/`new Function`.\n * - `eval at …` — V8's origin of `eval`'d code. When OUR script ran the eval\n * its URL is inside this origin, `stackFrameUrls` reads it, and the report\n * is kept; that is how the marketing plugin's `runJs` step stays reported.\n * - `debugger eval code:line:col` — Firefox's devtools console, in the\n * `fn@location` format WebKit also uses.\n *\n * ⚑ A BARE `(<anonymous>)` with no line and column is NOT one of these. It is\n * how V8 prints a builtin — `at Array.forEach (<anonymous>)`,\n * `at JSON.parse (<anonymous>)` — and a builtin is called by whoever called\n * it, including us.\n */\nconst ANONYMOUS_SCRIPT_POSITION =\n /<anonymous>:\\d+:\\d+|\\beval at\\s|\\bdebugger eval code:\\d+:\\d+/\n\n/** How many frames of this stack are in a script with no URL at all. */\nexport function anonymousScriptFrameCount(stack: string): number {\n let count = 0\n for (const line of stack.split('\\n')) {\n if (STACK_FRAME_LINE.test(line) && ANONYMOUS_SCRIPT_POSITION.test(line)) {\n count += 1\n }\n }\n return count\n}\n\n/**\n * Browser notices that are not failures, and that no fix of ours could act on.\n *\n * `ResizeObserver loop completed with undelivered notifications` (and its older\n * wording, `ResizeObserver loop limit exceeded`) is the browser saying it\n * deferred some resize callbacks to the next frame. Nothing threw: the event\n * carries no error object and no stack, and every observer is still\n * delivered. It arrived from the Besigner on 2026-09-26 and paged. Dropped, on\n * the browser's exact wording.\n */\nconst BENIGN_BROWSER_NOTICE =\n /^(?:Uncaught )?(?:Error: )?ResizeObserver loop (?:completed with undelivered notifications|limit exceeded)/\n\nexport function isBenignBrowserNotice(message: string): boolean {\n return BENIGN_BROWSER_NOTICE.test(message)\n}\n\n/**\n * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to\n * another account pool (AGL-3280). The console recovers the tab by reloading\n * it once it is hidden and deliberately leaves the rejection unhandled so it\n * is still seen; this label is what lets it be seen by RATE instead of paging\n * on each of the several rejections one storage event raises.\n */\nexport const AUTH_DESYNC_KIND = 'auth-desync'\n\n/**\n * `kind` for a chunk the document names and the origin no longer serves\n * (AGL-3279). Every boundary reloads once for it; what still arrives is the\n * failure the reload did not cure. One of those is a visitor on a bad\n * connection; a burst of them is a broken deploy, which only a rate can tell.\n */\nexport const CHUNK_LOAD_KIND = 'chunk-load'\n\n/**\n * The label for a fault we RECOVER from and still want to see, or undefined.\n *\n * These could be ours, so unlike an injected stack they are never dropped:\n * they are written at full severity under their own `kind`, the per-entry\n * policy excludes that kind, and a rate policy watches it — the same split\n * `hydration` already has.\n */\nexport function recoveredErrorKind(error: unknown): string | undefined {\n try {\n if (isCrossPoolDesync(error)) return AUTH_DESYNC_KIND\n if (isStaleBuildError(error)) return CHUNK_LOAD_KIND\n } catch {\n // A getter that throws leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * React's hydration-mismatch family, which a page TRANSLATOR causes and a\n * real render divergence also causes.\n *\n * These are `onRecoverableError` reports, not crashes: React has already\n * re-rendered on the client and the visitor has a working page. They arrive\n * here as uncaught errors only because React re-throws them to `reportError`.\n *\n * They are marked rather than dropped, and the distinction is the whole\n * design. Dropping would hide a real hydration regression, which is a genuine\n * and expensive bug; the entries stay in `client-errors` at full severity and\n * still group in Error Reporting. What the mark buys is that the log-match\n * policy can stop paging on a single report, while a rate-based policy on the\n * same mark still catches a systemic one.\n *\n * ⚠️ THE MARK IS NOT A VERDICT, and the one cluster this repo has measured is\n * why. Eight #418 reports arrived inside a nine-minute window on 2026-09-01,\n * on one build, and the three pages they came from were exactly the three\n * carrying a form — `/`, `/pricing` and the solutions pages reported nothing\n * in the same window, which no browser-side rewrite would respect. A placed\n * form was expanding inside its own grafted subtree, so every page carrying\n * one shipped six nested `<form>` elements; nested forms are invalid HTML, the\n * parser dropped the inner ones, and the client tree disagreed with the server\n * tree. The visible cost was a submission the route never received, because\n * the native submit came from an unhandled ancestor and the browser fell back\n * to a GET with the visitor's name, email and message in the address bar. The\n * reports stopped because the nesting was fixed, roughly a quarter of an hour\n * after the last one.\n *\n * So a rate low enough not to page is a reason to READ the entry — the page\n * it names, and whether the set of pages has a shape — never a reason to\n * assume somebody's extension.\n */\nconst HYDRATION_MINIFIED = /Minified React error #(?:418|423|425)\\b/\nconst HYDRATION_TEXT =\n /Hydration failed because|Text content does not match server-rendered HTML|There was an error while hydrating/\n\nexport function isHydrationMismatch(message: string): boolean {\n return HYDRATION_MINIFIED.test(message) || HYDRATION_TEXT.test(message)\n}\n\n/**\n * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).\n *\n * `unhandledrejection` hands over whatever was passed to `reject`, and only\n * an `Error` brings a message and a stack. A rejection with anything else —\n * an API's `{ code, message }` object, a `DOMException` from an aborted\n * fetch, a bare string, a `Response` — used to be reported as the literal\n * words \"Unhandled promise rejection\" with no stack and no other field,\n * which names the handler rather than the failure. One such report arrived\n * on 2026-09-21 from the Besigner editor and there is nothing in it to act\n * on: no message, no stack, no code, nothing but the page.\n *\n * So the value is DESCRIBED rather than defaulted. In order of how much a\n * reader can do with it: the error's own message; a string as itself; a\n * `name`/`code`/`message` shape assembled from whatever of those it has —\n * the shape every Firebase, DOM and fetch rejection takes; the value's own\n * `toString` when it says more than `[object Object]`; and last its own\n * keys, which at least name what kind of thing rejected.\n *\n * Nothing here may throw: a getter on the reason can, `toString` can, and\n * a describer that throws inside the rejection handler loses the report it\n * was called to improve.\n */\nexport function describeRejectionReason(reason: unknown): string {\n try {\n if (reason instanceof Error && reason.message) return reason.message\n if (typeof reason === 'string') return reason\n if (reason === null || reason === undefined) {\n return `Unhandled promise rejection (${String(reason)})`\n }\n if (typeof reason !== 'object') {\n return `Unhandled promise rejection (${typeof reason}: ${String(reason)})`\n }\n const shape = reason as Record<string, unknown>\n const named = [\n shape['name'] ?? shape['code'] ?? '',\n shape['message'] ?? shape['statusText'] ?? '',\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n if (named.length) return named.join(': ')\n const printed = String(reason)\n if (printed && printed !== '[object Object]') return printed\n const keys = Object.keys(shape).slice(0, 12).join(', ')\n return keys\n ? `Unhandled promise rejection (object with ${keys})`\n : 'Unhandled promise rejection (empty object)'\n } catch {\n return 'Unhandled promise rejection (undescribable)'\n }\n}\n\n/**\n * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).\n *\n * V8 keeps ten by default, and ten is not enough to find where a render loop\n * STARTED. The Besigner's one React #185 report (2026-09-24) was a\n * react-final-form field's `setState` inside final-form's `notify` loop, and\n * every one of its ten frames was inside those two libraries: the component\n * whose update fed the loop was cut off below them. Fifty reaches past a\n * form library's own frames to ours, and still fits the {@link MAX_STACK}\n * clamp at the length a minified frame line has.\n */\nexport const BEACON_STACK_TRACE_LIMIT = 50\n\n/**\n * Raises the engine's frame limit to `limit`, never lowers it. V8 and\n * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no\n * such number, and is left without one rather than handed a property it\n * ignores. It applies to errors created after the call, which is why the\n * beacon sets it at install, at module scope of the app's first client chunk.\n */\nexport function raiseStackTraceLimit(limit: number): void {\n try {\n const engine = Error as ErrorConstructor & { stackTraceLimit?: number }\n const current = engine.stackTraceLimit\n if (typeof current !== 'number' || current >= limit) return\n engine.stackTraceLimit = limit\n } catch {\n // A frozen or exotic `Error` keeps its own limit.\n }\n}\n\nlet installed = false\n\n/**\n * The live beacon's `enqueue`, published for {@link reportHandledError}.\n *\n * Null until `installErrorBeacon` runs, and null forever on a surface that\n * never installs one — so a caught error reported from a page with no beacon\n * is silently dropped rather than throwing inside somebody's error handler.\n */\nlet publishEvent: ((event: ErrorBeaconEvent) => void) | null = null\n\n/**\n * Re-export so `error-beacon` stays the one name a caller has to know for\n * browser error reporting. The definition lives in its own module because a\n * boundary that imports it must not pull the installer below into a chunk\n * every visitor downloads.\n */\nexport { redispatchCaughtError } from './redispatch-caught-error'\n\n/**\n * Report an error the code ALREADY CAUGHT.\n *\n * The two window handlers below see only what nothing caught. A `catch` that\n * swallows its error is invisible to them by construction, which is the whole\n * failure mode this exists for: a background write that can only fail\n * silently is one that stays broken for as long as nobody thinks to look.\n *\n * It rides the same queue as an uncaught error, which is the point — the\n * dedupe collapses a failure that repeats on every edit into one report, and\n * `maxPerPage` bounds it, so a call site in a loop cannot turn a broken\n * feature into a flood. Reporting is never worth an exception of its own, so\n * every path here returns rather than throws.\n */\nexport function reportHandledError(\n error: unknown,\n options?: { kind?: string },\n): void {\n try {\n if (!publishEvent) return\n const thrown = error as Error | undefined\n const message = clamp(\n thrown?.message ?? (typeof error === 'string' ? error : ''),\n MAX_MESSAGE,\n )\n if (!message) return\n publishEvent({\n kind: clamp(options?.kind ?? 'handled', 32),\n message,\n stack: thrown?.stack ? clamp(thrown.stack, MAX_STACK) : undefined,\n url: scrubUrl(window.location.href),\n })\n } catch {\n // An observer that throws is worse than one that misses an event.\n }\n}\n\n/**\n * Installs the handlers once per page. Safe to call from module scope of a\n * client bundle: it no-ops during SSR and on repeat calls.\n */\nexport function installErrorBeacon(options?: ErrorBeaconOptions): void {\n if (typeof window === 'undefined' || installed) return\n installed = true\n\n const endpoint = options?.endpoint ?? '/api/errors'\n const sampleRate = options?.sampleRate ?? 1\n const maxPerPage = options?.maxPerPage ?? 10\n // Only a sampled pageview REPORTS. Every pageview recovers a stale build\n // (the rejection handler below), because a tab left on a dead deploy is a\n // visitor's problem whether or not its errors are counted.\n const sampled = Math.random() < sampleRate\n if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT)\n\n const seen = new Set<string>()\n let queued: ErrorBeaconEvent[] = []\n let sent = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const flush = () => {\n timer = null\n if (!queued.length) return\n const batch = queued\n queued = []\n try {\n const body = JSON.stringify({ events: batch })\n // sendBeacon survives unloads and never blocks; keepalive fetch is the\n // fallback for browsers that refuse the payload.\n if (!navigator.sendBeacon?.(endpoint, body)) {\n void fetch(endpoint, {\n method: 'POST',\n body,\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n }).catch((): undefined => undefined)\n }\n } catch {\n // Reporting never breaks the page.\n }\n }\n\n const enqueue = (event: ErrorBeaconEvent) => {\n if (!sampled || sent >= maxPerPage) return\n // Message + first stack line: enough identity to collapse a render loop\n // without collapsing distinct errors that share a message.\n const key = `${event.message}\\x00${(event.stack ?? '').split('\\n', 2).join('\\n')}`\n if (seen.has(key)) return\n seen.add(key)\n sent += 1\n queued.push(event)\n if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS)\n }\n // Null on an unsampled pageview, so it reports nothing by either door\n // rather than one by one and none by the other.\n publishEvent = sampled ? enqueue : null\n\n window.addEventListener('error', (event) => {\n try {\n const error = event.error as Error | undefined\n const message = clamp(error?.message ?? event.message, MAX_MESSAGE)\n // Cross-origin scripts yield an opaque \"Script error.\" with nothing\n // actionable attached; reporting it would only create a noisy group.\n if (!message || message === 'Script error.') return\n if (isBenignBrowserNotice(message)) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = error?.stack ? String(error.stack) : undefined\n // A webview or extension that evaluated its own code into our page —\n // see `isInjectedThirdPartyFrame`. Nothing we ship can be changed in\n // response, so it is dropped on the same test as `Script error.`.\n // Judged on the WHOLE stack, before the clamp: a frame of ours past the\n // clamp still makes the error ours.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined\n enqueue({\n // MARKED, not dropped: a translator causes a hydration mismatch and\n // so does a real render divergence, and a recovered fault is still a\n // fault. Only a rate tells either apart from noise.\n kind:\n recoveredErrorKind(error ?? { message }) ??\n (isHydrationMismatch(message) ? 'hydration' : 'error'),\n message,\n stack,\n source: scrubUrl(event.filename) || undefined,\n line: typeof event.lineno === 'number' ? event.lineno : undefined,\n col: typeof event.colno === 'number' ? event.colno : undefined,\n url: pageUrl,\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n window.addEventListener('unhandledrejection', (event) => {\n try {\n // An `import()` of a chunk this document names and the origin dropped,\n // which no boundary saw. Recovered by a reload once the tab is hidden,\n // and, like a boundary's reload, not reported: see\n // `recoverStaleBuildRejection`. A tab whose recovery is spent still\n // reports it, as `chunk-load`.\n if (recoverStaleBuildRejection(event.reason)) return\n const reason = event.reason as Error | undefined\n const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE)\n if (!message) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = reason?.stack ? String(reason.stack) : undefined\n // The same injected-code drop as an uncaught error: a rejection raised\n // by a devtools snippet or an automation driver's `evaluate` is no more\n // ours for having been a promise.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n enqueue({\n kind: recoveredErrorKind(event.reason) ?? 'unhandledrejection',\n message,\n stack: fullStack ? clamp(fullStack, MAX_STACK) : undefined,\n url: pageUrl,\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n // A batch waiting out its debounce when the page hides would be lost;\n // `pagehide`/`visibilitychange` are the last reliable moments to send.\n window.addEventListener('pagehide', flush)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush()\n })\n}\n"],"names":["isCrossPoolDesync","isForeignScriptUrl","stackFrameUrls","isStaleBuildError","recoverStaleBuildRejection","MAX_MESSAGE","MAX_STACK","FLUSH_DELAY_MS","scrubUrl","raw","url","URL","String","window","location","href","origin","pathname","clamp","value","max","slice","isInjectedThirdPartyFrame","stack","documentUrl","urls","length","anonymousScriptFrameCount","every","STACK_FRAME_LINE","ANONYMOUS_SCRIPT_POSITION","count","line","split","test","BENIGN_BROWSER_NOTICE","isBenignBrowserNotice","message","AUTH_DESYNC_KIND","CHUNK_LOAD_KIND","recoveredErrorKind","error","undefined","HYDRATION_MINIFIED","HYDRATION_TEXT","isHydrationMismatch","describeRejectionReason","reason","shape","Error","named","map","part","trim","filter","Boolean","join","printed","keys","Object","BEACON_STACK_TRACE_LIMIT","raiseStackTraceLimit","limit","engine","current","stackTraceLimit","installed","publishEvent","redispatchCaughtError","reportHandledError","options","thrown","kind","installErrorBeacon","endpoint","sampleRate","maxPerPage","sampled","Math","random","seen","Set","queued","sent","timer","flush","batch","body","JSON","stringify","events","navigator","sendBeacon","fetch","method","keepalive","headers","catch","enqueue","event","key","has","add","push","setTimeout","addEventListener","pageUrl","fullStack","source","filename","lineno","col","colno","document","visibilityState"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/error-beacon.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 * First-party browser error beacon (AGL-1538).\n *\n * Crashlytics does not exist for web, and adding a third-party error vendor\n * is a subprocessor-list decision that was explicitly deferred. This is the\n * first-party alternative: `window.onerror` / `unhandledrejection` handlers\n * that batch uncaught failures to the app's own `/api/errors` route, which\n * forwards them to Google Cloud Error Reporting — a processor already on the\n * list, alerting through the GCP stack that already pages (AGL-1502).\n *\n * Design constraints, in order:\n *\n * - **It must never become the outage.** Everything is wrapped; a failure to\n * report is silence, not a thrown error. Batches are capped per page so an\n * error thrown in a loop cannot DDoS our own API — after `maxPerPage`\n * events the beacon disarms for the rest of the pageview.\n * - **No PII.** URLs are scrubbed to origin + pathname (query strings and\n * fragments carry tokens and search terms), no user id, no cookies —\n * the payload is the error text and where in the CODE it happened.\n * Cross-origin scripts surface as the browser's opaque \"Script error.\"\n * with no stack; those are dropped since they cannot be acted on.\n * - **Dedupe.** One page throwing the same error on every frame reports it\n * once.\n *\n * Framework-free on purpose: the console mounts it from its root layout, the\n * tenant runtime from its own, and neither needs React here. Deliberately NO\n * 'use client' directive — inside this shared lib the directive forks the\n * module graph (AGL-52); every importer is already a client module.\n */\n\nimport { isCrossPoolDesync } from './cross-pool-desync'\nimport { isForeignScriptUrl, stackFrameUrls } from './foreign-script'\nimport { isStaleBuildError, recoverStaleBuildRejection } from './stale-build-error'\n\nexport interface ErrorBeaconEvent {\n /**\n * Which handler caught it — 'error' | 'unhandledrejection' — or the\n * family it was labeled with: 'hydration', {@link AUTH_DESYNC_KIND},\n * {@link CHUNK_LOAD_KIND}, or a caller's own kind for a handled error.\n * Lands at `jsonPayload.kind`, where the alert policies filter on it.\n */\n kind: string\n /** Error message, clamped. */\n message: string\n /** Stack trace when the thrown value carried one, clamped. */\n stack?: string\n /** Script URL for stackless `window.onerror` events, scrubbed. */\n source?: string\n line?: number\n col?: number\n /** Page URL, scrubbed to origin + pathname. */\n url: string\n /**\n * On a `hydration` event only: the page translator whose rewrite is\n * visible in the document when React gave up — see\n * {@link pageTranslationSignal}. Absent when nothing visibly translated\n * the page, which is the case that is worth reading.\n */\n translated?: string\n /** On a `hydration` event only: `navigator.language`, clamped. */\n language?: string\n}\n\nexport interface ErrorBeaconOptions {\n /** POST target; same-origin. Default `/api/errors`. */\n endpoint?: string\n /** 0..1 — fraction of pageviews that report at all. Default 1. */\n sampleRate?: number\n /** Max events reported per pageview before the beacon disarms. */\n maxPerPage?: number\n}\n\nconst MAX_MESSAGE = 1_024\nconst MAX_STACK = 8_192\nconst FLUSH_DELAY_MS = 2_000\n\n/** Origin + pathname only — query strings and fragments never leave. */\nfunction scrubUrl(raw: string | null | undefined): string {\n if (!raw) return ''\n try {\n const url = new URL(String(raw), window.location.href)\n return `${url.origin}${url.pathname}`\n } catch {\n return ''\n }\n}\n\nfunction clamp(value: unknown, max: number): string {\n return String(value ?? '').slice(0, max)\n}\n\n/**\n * Was this thrown by a script somebody ELSE evaluated into our page?\n *\n * Three tells, all read off the frames' positions and none off a function\n * name — a name would be a denylist needing a new entry per vendor.\n *\n * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on\n * aglyn.com/pricing, reported as an error of ours:\n *\n * sendDataToNative@https://aglyn.com/pricing:1:1325\n * sendPageHideMessage@https://aglyn.com/pricing:1:4139\n * @https://aglyn.com/pricing:1:6257\n *\n * `sendDataToNative` is the Meta in-app browser's native bridge, injected by\n * the Facebook/Instagram webview. In WebKit's format the code was evaluated\n * inline, so every frame is the document rather than a script we served.\n *\n * **Every frame is under a FOREIGN scheme.** Measured 2026-09-10 (AGL-2786) on\n * aglyn.com, the same bridge in the Android webview:\n *\n * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)\n *\n * That webview names its scripts under its own `iabjs://` scheme, and\n * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.\n *\n * **Every frame is a script with NO URL** (AGL-3423). Measured on the\n * sign-up page and on aglyn.com/solutions/agencies:\n *\n * at eval (<anonymous>)\n * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)\n *\n * at <anonymous>:1:54\n * at <anonymous>:1:64\n *\n * An automation driver's `evaluate` and a snippet pasted into devtools. Every\n * script we serve has a URL — a chunk, a CDN asset, or the document for an\n * inline one — so a frame whose position is `<anonymous>:line:col`, or code\n * `eval`'d by such a frame, was not served by us. See\n * {@link anonymousScriptFrameCount} for exactly which frames count.\n *\n * ⚑ Deliberately `every`, and deliberately compared against the document\n * rather than against our asset path. A rule like \"no frame under\n * `/_next/static/`\" would delete EVERY error from a self-hosted deployment\n * that serves its assets from a CDN, because none of its frames would match.\n * Comparing to the document cannot fail that way: a CDN frame is not the\n * document either, so it keeps the report.\n *\n * A stack may mix the tells frame by frame. Nothing we ship can be fixed in\n * response to any of them, which is the same test the opaque `Script error.`\n * cut already applies.\n *\n * The cost is honest and small: a throw from one of our own inline\n * bootstrap scripts looks the same and is dropped with it. That trade buys a\n * rule that needs no maintenance as new webviews and extensions appear.\n */\nexport function isInjectedThirdPartyFrame(\n stack: string,\n documentUrl: string,\n): boolean {\n const urls = stackFrameUrls(stack)\n // No frames parsed is not evidence of anything — keep the report. A stack\n // with neither a URL nor an anonymous-script frame is exactly that: an\n // empty frame list must never read as \"every frame is injected\".\n if (!urls.length && !anonymousScriptFrameCount(stack)) return false\n // Anonymous frames need no test of their own — they are injected by\n // definition — so `every` over the URLs is `every` over the whole stack.\n // One URL of ours, even as the origin of an `eval`, keeps the report.\n return urls.every(\n (url) => isForeignScriptUrl(url) || scrubUrl(url) === documentUrl,\n )\n}\n\n/** A frame line, in V8's `at …` format or WebKit/Firefox's `fn@location`. */\nconst STACK_FRAME_LINE = /^\\s*at\\s|@/\n\n/**\n * A position in a script that has no URL.\n *\n * - `<anonymous>:line:col` — V8's name for such a script, both for a top-level\n * snippet and as the position of code run through `eval`/`new Function`.\n * - `eval at …` — V8's origin of `eval`'d code. When OUR script ran the eval\n * its URL is inside this origin, `stackFrameUrls` reads it, and the report\n * is kept; that is how the marketing plugin's `runJs` step stays reported.\n * - `debugger eval code:line:col` — Firefox's devtools console, in the\n * `fn@location` format WebKit also uses.\n *\n * ⚑ A BARE `(<anonymous>)` with no line and column is NOT one of these. It is\n * how V8 prints a builtin — `at Array.forEach (<anonymous>)`,\n * `at JSON.parse (<anonymous>)` — and a builtin is called by whoever called\n * it, including us.\n */\nconst ANONYMOUS_SCRIPT_POSITION =\n /<anonymous>:\\d+:\\d+|\\beval at\\s|\\bdebugger eval code:\\d+:\\d+/\n\n/** How many frames of this stack are in a script with no URL at all. */\nexport function anonymousScriptFrameCount(stack: string): number {\n let count = 0\n for (const line of stack.split('\\n')) {\n if (STACK_FRAME_LINE.test(line) && ANONYMOUS_SCRIPT_POSITION.test(line)) {\n count += 1\n }\n }\n return count\n}\n\n/**\n * Browser notices that are not failures, and that no fix of ours could act on.\n *\n * `ResizeObserver loop completed with undelivered notifications` (and its older\n * wording, `ResizeObserver loop limit exceeded`) is the browser saying it\n * deferred some resize callbacks to the next frame. Nothing threw: the event\n * carries no error object and no stack, and every observer is still\n * delivered. It arrived from the Besigner on 2026-09-26 and paged. Dropped, on\n * the browser's exact wording.\n */\nconst BENIGN_BROWSER_NOTICE =\n /^(?:Uncaught )?(?:Error: )?ResizeObserver loop (?:completed with undelivered notifications|limit exceeded)/\n\nexport function isBenignBrowserNotice(message: string): boolean {\n return BENIGN_BROWSER_NOTICE.test(message)\n}\n\n/**\n * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to\n * another account pool (AGL-3280). The console recovers the tab by reloading\n * it once it is hidden and deliberately leaves the rejection unhandled so it\n * is still seen; this label is what lets it be seen by RATE instead of paging\n * on each of the several rejections one storage event raises.\n */\nexport const AUTH_DESYNC_KIND = 'auth-desync'\n\n/**\n * `kind` for a chunk the document names and the origin no longer serves\n * (AGL-3279). Every boundary reloads once for it; what still arrives is the\n * failure the reload did not cure. One of those is a visitor on a bad\n * connection; a burst of them is a broken deploy, which only a rate can tell.\n */\nexport const CHUNK_LOAD_KIND = 'chunk-load'\n\n/**\n * The label for a fault we RECOVER from and still want to see, or undefined.\n *\n * These could be ours, so unlike an injected stack they are never dropped:\n * they are written at full severity under their own `kind`, the per-entry\n * policy excludes that kind, and a rate policy watches it — the same split\n * `hydration` already has.\n */\nexport function recoveredErrorKind(error: unknown): string | undefined {\n try {\n if (isCrossPoolDesync(error)) return AUTH_DESYNC_KIND\n if (isStaleBuildError(error)) return CHUNK_LOAD_KIND\n } catch {\n // A getter that throws leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * React's hydration-mismatch family, which a page TRANSLATOR causes and a\n * real render divergence also causes.\n *\n * These are `onRecoverableError` reports, not crashes: React has already\n * re-rendered on the client and the visitor has a working page. They arrive\n * here as uncaught errors only because React re-throws them to `reportError`.\n *\n * They are marked rather than dropped, and the distinction is the whole\n * design. Dropping would hide a real hydration regression, which is a genuine\n * and expensive bug; the entries stay in `client-errors` at full severity and\n * still group in Error Reporting. What the mark buys is that the log-match\n * policy can stop paging on a single report, while a rate-based policy on the\n * same mark still catches a systemic one.\n *\n * ⚠️ THE MARK IS NOT A VERDICT, and the one cluster this repo has measured is\n * why. Eight #418 reports arrived inside a nine-minute window on 2026-09-01,\n * on one build, and the three pages they came from were exactly the three\n * carrying a form — `/`, `/pricing` and the solutions pages reported nothing\n * in the same window, which no browser-side rewrite would respect. A placed\n * form was expanding inside its own grafted subtree, so every page carrying\n * one shipped six nested `<form>` elements; nested forms are invalid HTML, the\n * parser dropped the inner ones, and the client tree disagreed with the server\n * tree. The visible cost was a submission the route never received, because\n * the native submit came from an unhandled ancestor and the browser fell back\n * to a GET with the visitor's name, email and message in the address bar. The\n * reports stopped because the nesting was fixed, roughly a quarter of an hour\n * after the last one.\n *\n * So a rate low enough not to page is a reason to READ the entry — the page\n * it names, and whether the set of pages has a shape — never a reason to\n * assume somebody's extension.\n */\nconst HYDRATION_MINIFIED = /Minified React error #(?:418|423|425)\\b/\nconst HYDRATION_TEXT =\n /Hydration failed because|Text content does not match server-rendered HTML|There was an error while hydrating/\n\nexport function isHydrationMismatch(message: string): boolean {\n return HYDRATION_MINIFIED.test(message) || HYDRATION_TEXT.test(message)\n}\n\n/**\n * WHICH TRANSLATOR rewrote this page's text, read off the document, or\n * undefined when none visibly did.\n *\n * A hydration report on its own cannot say whether React disagreed with our\n * server render or with a translator that rewrote the server's text before\n * React reached it, and the two look identical in `client-errors`: the\n * minified #418, a stack inside the react-dom chunk, the page URL, nothing\n * else. aglyn.com/ carried eight of them between 2026-09-22 and 2026-10-10.\n * The visitor at 2026-10-09 22:28Z went on to sign up two minutes later with\n * Chrome Translate rewriting the console (the `<font>` crash #1342 fixed); the\n * two on 2026-09-22/23 were desktop Chrome OS in Hyderabad, while every other\n * Indian, Pakistani and Turkish home-page visit in GA that fortnight hydrated\n * without a report — so neither the build nor the time zone was the variable,\n * and the beacon had nothing to say which visitor it was.\n *\n * The signatures are the ones each translator leaves on the DOM it rewrote:\n *\n * - Chrome (and every Chromium that ships Google Translate) sets\n * `translated-ltr` / `translated-rtl` on `<html>` and wraps each rewritten\n * run in `<font style=\"vertical-align: inherit;\">`.\n * - Microsoft Translator (Edge) stamps `_msttexthash` / `_msthash` on every\n * element whose text it replaced.\n * - Any translator that does the job properly rewrites `<html lang>`, which is\n * compared against the `servedLang` the beacon read at install, before\n * hydration began.\n *\n * Never throws: it runs inside the error handler.\n */\nexport function pageTranslationSignal(\n doc: Document,\n servedLang: string | null | undefined,\n): string | undefined {\n try {\n const root = doc.documentElement\n if (!root) return undefined\n if (\n root.classList.contains('translated-ltr') ||\n root.classList.contains('translated-rtl') ||\n doc.querySelector('font[style*=\"vertical-align: inherit\"]')\n ) {\n return 'chrome'\n }\n if (doc.querySelector('[_msttexthash], [_msthash]')) return 'microsoft'\n const lang = root.getAttribute('lang')\n if (servedLang && lang && lang.toLowerCase() !== servedLang.toLowerCase()) {\n return `lang:${clamp(lang, 16)}`\n }\n } catch {\n // An unreadable document leaves the event unlabeled, never unreported.\n }\n return undefined\n}\n\n/**\n * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).\n *\n * `unhandledrejection` hands over whatever was passed to `reject`, and only\n * an `Error` brings a message and a stack. A rejection with anything else —\n * an API's `{ code, message }` object, a `DOMException` from an aborted\n * fetch, a bare string, a `Response` — used to be reported as the literal\n * words \"Unhandled promise rejection\" with no stack and no other field,\n * which names the handler rather than the failure. One such report arrived\n * on 2026-09-21 from the Besigner editor and there is nothing in it to act\n * on: no message, no stack, no code, nothing but the page.\n *\n * So the value is DESCRIBED rather than defaulted. In order of how much a\n * reader can do with it: the error's own message; a string as itself; a\n * `name`/`code`/`message` shape assembled from whatever of those it has —\n * the shape every Firebase, DOM and fetch rejection takes; the value's own\n * `toString` when it says more than `[object Object]`; and last its own\n * keys, which at least name what kind of thing rejected.\n *\n * Nothing here may throw: a getter on the reason can, `toString` can, and\n * a describer that throws inside the rejection handler loses the report it\n * was called to improve.\n */\nexport function describeRejectionReason(reason: unknown): string {\n try {\n if (reason instanceof Error && reason.message) return reason.message\n if (typeof reason === 'string') return reason\n if (reason === null || reason === undefined) {\n return `Unhandled promise rejection (${String(reason)})`\n }\n if (typeof reason !== 'object') {\n return `Unhandled promise rejection (${typeof reason}: ${String(reason)})`\n }\n const shape = reason as Record<string, unknown>\n const named = [\n shape['name'] ?? shape['code'] ?? '',\n shape['message'] ?? shape['statusText'] ?? '',\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n if (named.length) return named.join(': ')\n const printed = String(reason)\n if (printed && printed !== '[object Object]') return printed\n const keys = Object.keys(shape).slice(0, 12).join(', ')\n return keys\n ? `Unhandled promise rejection (object with ${keys})`\n : 'Unhandled promise rejection (empty object)'\n } catch {\n return 'Unhandled promise rejection (undescribable)'\n }\n}\n\n/**\n * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).\n *\n * V8 keeps ten by default, and ten is not enough to find where a render loop\n * STARTED. The Besigner's one React #185 report (2026-09-24) was a\n * react-final-form field's `setState` inside final-form's `notify` loop, and\n * every one of its ten frames was inside those two libraries: the component\n * whose update fed the loop was cut off below them. Fifty reaches past a\n * form library's own frames to ours, and still fits the {@link MAX_STACK}\n * clamp at the length a minified frame line has.\n */\nexport const BEACON_STACK_TRACE_LIMIT = 50\n\n/**\n * Raises the engine's frame limit to `limit`, never lowers it. V8 and\n * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no\n * such number, and is left without one rather than handed a property it\n * ignores. It applies to errors created after the call, which is why the\n * beacon sets it at install, at module scope of the app's first client chunk.\n */\nexport function raiseStackTraceLimit(limit: number): void {\n try {\n const engine = Error as ErrorConstructor & { stackTraceLimit?: number }\n const current = engine.stackTraceLimit\n if (typeof current !== 'number' || current >= limit) return\n engine.stackTraceLimit = limit\n } catch {\n // A frozen or exotic `Error` keeps its own limit.\n }\n}\n\nlet installed = false\n\n/**\n * The live beacon's `enqueue`, published for {@link reportHandledError}.\n *\n * Null until `installErrorBeacon` runs, and null forever on a surface that\n * never installs one — so a caught error reported from a page with no beacon\n * is silently dropped rather than throwing inside somebody's error handler.\n */\nlet publishEvent: ((event: ErrorBeaconEvent) => void) | null = null\n\n/**\n * Re-export so `error-beacon` stays the one name a caller has to know for\n * browser error reporting. The definition lives in its own module because a\n * boundary that imports it must not pull the installer below into a chunk\n * every visitor downloads.\n */\nexport { redispatchCaughtError } from './redispatch-caught-error'\n\n/**\n * Report an error the code ALREADY CAUGHT.\n *\n * The two window handlers below see only what nothing caught. A `catch` that\n * swallows its error is invisible to them by construction, which is the whole\n * failure mode this exists for: a background write that can only fail\n * silently is one that stays broken for as long as nobody thinks to look.\n *\n * It rides the same queue as an uncaught error, which is the point — the\n * dedupe collapses a failure that repeats on every edit into one report, and\n * `maxPerPage` bounds it, so a call site in a loop cannot turn a broken\n * feature into a flood. Reporting is never worth an exception of its own, so\n * every path here returns rather than throws.\n */\nexport function reportHandledError(\n error: unknown,\n options?: { kind?: string },\n): void {\n try {\n if (!publishEvent) return\n const thrown = error as Error | undefined\n const message = clamp(\n thrown?.message ?? (typeof error === 'string' ? error : ''),\n MAX_MESSAGE,\n )\n if (!message) return\n publishEvent({\n kind: clamp(options?.kind ?? 'handled', 32),\n message,\n stack: thrown?.stack ? clamp(thrown.stack, MAX_STACK) : undefined,\n url: scrubUrl(window.location.href),\n })\n } catch {\n // An observer that throws is worse than one that misses an event.\n }\n}\n\n/**\n * Installs the handlers once per page. Safe to call from module scope of a\n * client bundle: it no-ops during SSR and on repeat calls.\n */\nexport function installErrorBeacon(options?: ErrorBeaconOptions): void {\n if (typeof window === 'undefined' || installed) return\n installed = true\n\n const endpoint = options?.endpoint ?? '/api/errors'\n const sampleRate = options?.sampleRate ?? 1\n const maxPerPage = options?.maxPerPage ?? 10\n // Only a sampled pageview REPORTS. Every pageview recovers a stale build\n // (the rejection handler below), because a tab left on a dead deploy is a\n // visitor's problem whether or not its errors are counted.\n const sampled = Math.random() < sampleRate\n // The document's language as SERVED, read before hydration and before any\n // translator has had a chance to rewrite it (`pageTranslationSignal`).\n const servedLang = document.documentElement?.getAttribute('lang') ?? null\n if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT)\n\n const seen = new Set<string>()\n let queued: ErrorBeaconEvent[] = []\n let sent = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const flush = () => {\n timer = null\n if (!queued.length) return\n const batch = queued\n queued = []\n try {\n const body = JSON.stringify({ events: batch })\n // sendBeacon survives unloads and never blocks; keepalive fetch is the\n // fallback for browsers that refuse the payload.\n if (!navigator.sendBeacon?.(endpoint, body)) {\n void fetch(endpoint, {\n method: 'POST',\n body,\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n }).catch((): undefined => undefined)\n }\n } catch {\n // Reporting never breaks the page.\n }\n }\n\n const enqueue = (event: ErrorBeaconEvent) => {\n if (!sampled || sent >= maxPerPage) return\n // Message + first stack line: enough identity to collapse a render loop\n // without collapsing distinct errors that share a message.\n const key = `${event.message}\\x00${(event.stack ?? '').split('\\n', 2).join('\\n')}`\n if (seen.has(key)) return\n seen.add(key)\n sent += 1\n queued.push(event)\n if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS)\n }\n // Null on an unsampled pageview, so it reports nothing by either door\n // rather than one by one and none by the other.\n publishEvent = sampled ? enqueue : null\n\n window.addEventListener('error', (event) => {\n try {\n const error = event.error as Error | undefined\n const message = clamp(error?.message ?? event.message, MAX_MESSAGE)\n // Cross-origin scripts yield an opaque \"Script error.\" with nothing\n // actionable attached; reporting it would only create a noisy group.\n if (!message || message === 'Script error.') return\n if (isBenignBrowserNotice(message)) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = error?.stack ? String(error.stack) : undefined\n // A webview or extension that evaluated its own code into our page —\n // see `isInjectedThirdPartyFrame`. Nothing we ship can be changed in\n // response, so it is dropped on the same test as `Script error.`.\n // Judged on the WHOLE stack, before the clamp: a frame of ours past the\n // clamp still makes the error ours.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined\n // MARKED, not dropped: a translator causes a hydration mismatch and so\n // does a real render divergence, and a recovered fault is still a\n // fault. Only a rate tells either apart from noise — and the\n // translator's own mark on the DOM, when it left one.\n const kind =\n recoveredErrorKind(error ?? { message }) ??\n (isHydrationMismatch(message) ? 'hydration' : 'error')\n const translated =\n kind === 'hydration'\n ? pageTranslationSignal(document, servedLang)\n : undefined\n enqueue({\n kind,\n message,\n stack,\n source: scrubUrl(event.filename) || undefined,\n line: typeof event.lineno === 'number' ? event.lineno : undefined,\n col: typeof event.colno === 'number' ? event.colno : undefined,\n url: pageUrl,\n ...(translated ? { translated } : {}),\n ...(kind === 'hydration' && navigator.language\n ? { language: clamp(navigator.language, 16) }\n : {}),\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n window.addEventListener('unhandledrejection', (event) => {\n try {\n // An `import()` of a chunk this document names and the origin dropped,\n // which no boundary saw. Recovered by a reload once the tab is hidden,\n // and, like a boundary's reload, not reported: see\n // `recoverStaleBuildRejection`. A tab whose recovery is spent still\n // reports it, as `chunk-load`.\n if (recoverStaleBuildRejection(event.reason)) return\n const reason = event.reason as Error | undefined\n const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE)\n if (!message) return\n const pageUrl = scrubUrl(window.location.href)\n const fullStack = reason?.stack ? String(reason.stack) : undefined\n // The same injected-code drop as an uncaught error: a rejection raised\n // by a devtools snippet or an automation driver's `evaluate` is no more\n // ours for having been a promise.\n if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return\n enqueue({\n kind: recoveredErrorKind(event.reason) ?? 'unhandledrejection',\n message,\n stack: fullStack ? clamp(fullStack, MAX_STACK) : undefined,\n url: pageUrl,\n })\n } catch {\n // Never rethrow from an error handler.\n }\n })\n\n // A batch waiting out its debounce when the page hides would be lost;\n // `pagehide`/`visibilitychange` are the last reliable moments to send.\n window.addEventListener('pagehide', flush)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush()\n })\n}\n"],"names":["isCrossPoolDesync","isForeignScriptUrl","stackFrameUrls","isStaleBuildError","recoverStaleBuildRejection","MAX_MESSAGE","MAX_STACK","FLUSH_DELAY_MS","scrubUrl","raw","url","URL","String","window","location","href","origin","pathname","clamp","value","max","slice","isInjectedThirdPartyFrame","stack","documentUrl","urls","length","anonymousScriptFrameCount","every","STACK_FRAME_LINE","ANONYMOUS_SCRIPT_POSITION","count","line","split","test","BENIGN_BROWSER_NOTICE","isBenignBrowserNotice","message","AUTH_DESYNC_KIND","CHUNK_LOAD_KIND","recoveredErrorKind","error","undefined","HYDRATION_MINIFIED","HYDRATION_TEXT","isHydrationMismatch","pageTranslationSignal","doc","servedLang","root","documentElement","classList","contains","querySelector","lang","getAttribute","toLowerCase","describeRejectionReason","reason","shape","Error","named","map","part","trim","filter","Boolean","join","printed","keys","Object","BEACON_STACK_TRACE_LIMIT","raiseStackTraceLimit","limit","engine","current","stackTraceLimit","installed","publishEvent","redispatchCaughtError","reportHandledError","options","thrown","kind","installErrorBeacon","document","endpoint","sampleRate","maxPerPage","sampled","Math","random","seen","Set","queued","sent","timer","flush","batch","body","JSON","stringify","events","navigator","sendBeacon","fetch","method","keepalive","headers","catch","enqueue","event","key","has","add","push","setTimeout","addEventListener","pageUrl","fullStack","translated","source","filename","lineno","col","colno","language","visibilityState"],"mappings":"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"}
|
|
@@ -132,8 +132,9 @@ export interface PageRecordScope {
|
|
|
132
132
|
*
|
|
133
133
|
* Run AFTER {@link expandRepeatables}: a repeat inside the page has already
|
|
134
134
|
* spent its own `{{item.*}}` tokens on its own rows by then, so what is left
|
|
135
|
-
* is the page's. A token naming a field the record
|
|
136
|
-
*
|
|
135
|
+
* is the page's. A token naming a field the record leaves empty prints
|
|
136
|
+
* nothing, exactly as in a repeat (AGL-3616). Returns the input when there is
|
|
137
|
+
* no record.
|
|
137
138
|
*/
|
|
138
139
|
export declare function substituteNodesRecordTokens<N>(nodes: Record<NodeId, N>, scope: PageRecordScope | null | undefined): Record<NodeId, N>;
|
|
139
140
|
/**
|
|
@@ -186,7 +187,11 @@ export declare function repeatKeys(nodes: Record<NodeId, unknown> | null | undef
|
|
|
186
187
|
* Repeats (AGL-103, AGL-3111): any node carrying `props.repeatDataset` (a
|
|
187
188
|
* dataset id or display name) renders once per record of that dataset, with
|
|
188
189
|
* `{{item.field}}` tokens in the copied string props replaced by the record's
|
|
189
|
-
* values
|
|
190
|
+
* values. A field the record leaves empty prints nothing — never the raw
|
|
191
|
+
* token — and an element whose whole text is such a binding is left out of
|
|
192
|
+
* that copy (AGL-3616: a tour date with no venue drew `{{ITEM.VENUE}}` as its
|
|
193
|
+
* eyebrow on a published site). The besigner's canvas keeps the tokens of
|
|
194
|
+
* the template an author edits ({@link substituteRecordTokens}).
|
|
190
195
|
*
|
|
191
196
|
* What is copied is the node's {@link repeatScope}:
|
|
192
197
|
*
|
|
@@ -43,12 +43,13 @@ const displayValue = (value)=>Array.isArray(value) ? value.join(', ') : String(v
|
|
|
43
43
|
function substituteValue(value, context) {
|
|
44
44
|
const { record } = context;
|
|
45
45
|
if (typeof value === 'string') {
|
|
46
|
+
const unresolved = (token)=>context.missing === 'empty' ? '' : token;
|
|
46
47
|
return value.replace(ITEM_TOKEN_PATTERN, (token, field, hop)=>{
|
|
47
48
|
if (hop) {
|
|
48
49
|
const resolved = resolveReferenceHop(context, field, hop);
|
|
49
|
-
return resolved != null ? resolved : token;
|
|
50
|
+
return resolved != null ? resolved : unresolved(token);
|
|
50
51
|
}
|
|
51
|
-
return record[field] != null ? displayValue(record[field]) : token;
|
|
52
|
+
return record[field] != null ? displayValue(record[field]) : unresolved(token);
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
if (Array.isArray(value)) {
|
|
@@ -63,6 +64,18 @@ function substituteValue(value, context) {
|
|
|
63
64
|
}
|
|
64
65
|
return value;
|
|
65
66
|
}
|
|
67
|
+
/** A text that is nothing but `{{item.*}}` tokens: a binding, not words. */ const WHOLE_BINDING = new RegExp(`^\\s*(?:${ITEM_TOKEN_PATTERN.source}\\s*)+$`);
|
|
68
|
+
/**
|
|
69
|
+
* Whether a copy's element is a binding its record left empty (AGL-3616):
|
|
70
|
+
* its text, `children`, was only `{{item.*}}` tokens and they printed
|
|
71
|
+
* nothing. The copy leaves the element out rather than drawing an empty line
|
|
72
|
+
* (an eyebrow with no venue), and a record that fills the field draws it.
|
|
73
|
+
*/ function isEmptyBinding(template, substituted) {
|
|
74
|
+
const text = template == null ? void 0 : template['children'];
|
|
75
|
+
if (typeof text !== 'string' || !WHOLE_BINDING.test(text)) return false;
|
|
76
|
+
const printed = substituted['children'];
|
|
77
|
+
return typeof printed === 'string' && printed.trim() === '';
|
|
78
|
+
}
|
|
66
79
|
/**
|
|
67
80
|
* The key a node repeats over, trimmed, or `''` when it repeats over nothing.
|
|
68
81
|
*
|
|
@@ -141,15 +154,18 @@ function substituteValue(value, context) {
|
|
|
141
154
|
*
|
|
142
155
|
* Run AFTER {@link expandRepeatables}: a repeat inside the page has already
|
|
143
156
|
* spent its own `{{item.*}}` tokens on its own rows by then, so what is left
|
|
144
|
-
* is the page's. A token naming a field the record
|
|
145
|
-
*
|
|
157
|
+
* is the page's. A token naming a field the record leaves empty prints
|
|
158
|
+
* nothing, exactly as in a repeat (AGL-3616). Returns the input when there is
|
|
159
|
+
* no record.
|
|
146
160
|
*/ export function substituteNodesRecordTokens(nodes, scope) {
|
|
147
161
|
if (!scope) return nodes;
|
|
148
162
|
const next = {};
|
|
149
163
|
for (const [id, node] of Object.entries(nodes)){
|
|
150
164
|
const props = node == null ? void 0 : node.props;
|
|
151
165
|
next[id] = props ? _extends({}, node, {
|
|
152
|
-
props: substituteValue(props, scope
|
|
166
|
+
props: substituteValue(props, _extends({}, scope, {
|
|
167
|
+
missing: 'empty'
|
|
168
|
+
}))
|
|
153
169
|
}) : node;
|
|
154
170
|
}
|
|
155
171
|
return next;
|
|
@@ -249,7 +265,11 @@ function substituteValue(value, context) {
|
|
|
249
265
|
* Repeats (AGL-103, AGL-3111): any node carrying `props.repeatDataset` (a
|
|
250
266
|
* dataset id or display name) renders once per record of that dataset, with
|
|
251
267
|
* `{{item.field}}` tokens in the copied string props replaced by the record's
|
|
252
|
-
* values
|
|
268
|
+
* values. A field the record leaves empty prints nothing — never the raw
|
|
269
|
+
* token — and an element whose whole text is such a binding is left out of
|
|
270
|
+
* that copy (AGL-3616: a tour date with no venue drew `{{ITEM.VENUE}}` as its
|
|
271
|
+
* eyebrow on a published site). The besigner's canvas keeps the tokens of
|
|
272
|
+
* the template an author edits ({@link substituteRecordTokens}).
|
|
253
273
|
*
|
|
254
274
|
* What is copied is the node's {@link repeatScope}:
|
|
255
275
|
*
|
|
@@ -329,27 +349,36 @@ function substituteValue(value, context) {
|
|
|
329
349
|
records.forEach((record, index)=>{
|
|
330
350
|
const prefix = `${REPEAT_NODE_ID_PREFIX}${repeatId}__${index}__`;
|
|
331
351
|
const prefixId = (id)=>`${prefix}${id}`;
|
|
352
|
+
// Whether the copy drew the node: an element whose whole text is a
|
|
353
|
+
// binding this record leaves empty is left out of it (AGL-3616).
|
|
332
354
|
const cloneSubtree = (id, clonedParentId)=>{
|
|
333
355
|
var _node_props;
|
|
334
356
|
const node = nodes[id];
|
|
335
|
-
if (!node) return;
|
|
357
|
+
if (!node) return false;
|
|
358
|
+
const props = substituteValue((_node_props = node.props) != null ? _node_props : {}, {
|
|
359
|
+
record,
|
|
360
|
+
model: dataset == null ? void 0 : dataset.model,
|
|
361
|
+
datasetsByKey: rowsByKey,
|
|
362
|
+
missing: 'empty'
|
|
363
|
+
});
|
|
364
|
+
if (isEmptyBinding(node.props, props)) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
336
367
|
const clonedChildren = Array.isArray(node.nodes) ? node.nodes : undefined;
|
|
368
|
+
const drawn = clonedChildren == null ? void 0 : clonedChildren.filter((childId)=>cloneSubtree(childId, prefixId(id)));
|
|
337
369
|
next[prefixId(id)] = withoutRepeatDirective(_extends({}, node, {
|
|
338
370
|
$id: prefixId(id),
|
|
339
371
|
parentId: clonedParentId,
|
|
340
|
-
props:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
datasetsByKey: rowsByKey
|
|
344
|
-
})
|
|
345
|
-
}, clonedChildren && {
|
|
346
|
-
nodes: clonedChildren.map((childId)=>prefixId(childId))
|
|
372
|
+
props: props
|
|
373
|
+
}, drawn && {
|
|
374
|
+
nodes: drawn.map((childId)=>prefixId(childId))
|
|
347
375
|
}));
|
|
348
|
-
|
|
376
|
+
return true;
|
|
349
377
|
};
|
|
350
378
|
for (const templateId of templateIds){
|
|
351
|
-
cloneSubtree(templateId, parentId)
|
|
352
|
-
|
|
379
|
+
if (cloneSubtree(templateId, parentId)) {
|
|
380
|
+
copyIds.push(prefixId(templateId));
|
|
381
|
+
}
|
|
353
382
|
}
|
|
354
383
|
});
|
|
355
384
|
if (self) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/expand-repeatables.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 type { AglynNodeSchema, NodeId } from '../foundation'\nimport {\n applyRepeatQuery,\n parseRepeatFilter,\n parseRepeatSort,\n} from './repeat-query'\n\n/** Namespaces cloned template ids per container/record. */\nexport const REPEAT_NODE_ID_PREFIX = 'rep__'\n\n/**\n * `{{item.field}}` token, plus an optional single reference hop\n * (`{{item.author.name}}`, AGL-180). One hop only — the pattern itself is\n * the depth guard, so cycles can't recurse.\n */\nconst ITEM_TOKEN_PATTERN =\n /\\{\\{\\s*item\\.([A-Za-z][A-Za-z0-9_]*)(?:\\.([A-Za-z][A-Za-z0-9_]*))?\\s*\\}\\}/g\n\n/** Hard bound on rows a single repeatable renders, before `repeatLimit`. */\nexport const REPEAT_MAX_RECORDS = 100\n\n/**\n * What the expansion reads of the rows' field model (AGL-177, AGL-180): which\n * fields are references, each with the key its target's rows are answered\n * under (`datasetsByKey`). The model itself is the plugin's that keeps the\n * rows, and it states this much of it when it hands the rows over\n * (`plugin-manager/repeat-rows`, `repeat-sources`); a field it does not name\n * here hops nowhere, and its token is left as written.\n */\nexport interface RepeatRowsModel {\n /** Reference field id → the key of the rows it points into. */\n references?: Readonly<Record<string, string>>\n}\n\nexport interface RepeatableDataset {\n /**\n * Row value maps, in display order. Rows carry `$id` (needed to resolve\n * incoming references); values may be typed (AGL-177), stringified at\n * substitution.\n */\n records: Array<Record<string, unknown>>\n /** The rows' references; required for reference-hop bindings. */\n model?: RepeatRowsModel\n}\n\ninterface SubstituteContext {\n record: Record<string, unknown>\n /** The repeated rows' references (AGL-180). */\n model?: RepeatRowsModel\n /** All host datasets keyed by id (and name) for hop resolution. */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n}\n\nconst displayValue = (value: unknown): string =>\n Array.isArray(value) ? value.join(', ') : String(value)\n\n/** Resolves `{{item.ref.field}}`: FKey(s) → target document field value. */\nfunction resolveReferenceHop(\n context: SubstituteContext,\n fieldId: string,\n targetFieldId: string,\n): string | null {\n const targetKey = context.model?.references?.[fieldId]\n if (!targetKey) return null\n const target = context.datasetsByKey?.[targetKey]\n if (!target) return null\n const keys = context.record[fieldId]\n const ids = Array.isArray(keys) ? keys : keys != null ? [keys] : []\n const resolved = ids\n .map((id) => {\n const match = target.records.find((row) => row['$id'] === id)\n const value = match?.[targetFieldId]\n return value != null ? displayValue(value) : null\n })\n .filter((value): value is string => value != null)\n return resolved.length ? resolved.join(', ') : null\n}\n\nfunction substituteValue(\n value: unknown,\n context: SubstituteContext,\n): unknown {\n const { record } = context\n if (typeof value === 'string') {\n return value.replace(\n ITEM_TOKEN_PATTERN,\n (token, field: string, hop?: string) => {\n if (hop) {\n const resolved = resolveReferenceHop(context, field, hop)\n return resolved != null ? resolved : token\n }\n return record[field] != null ? displayValue(record[field]) : token\n },\n )\n }\n if (Array.isArray(value)) {\n return value.map((entry) => substituteValue(entry, context))\n }\n if (value && typeof value === 'object') {\n const next: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(value)) {\n next[key] = substituteValue(entry, context)\n }\n return next\n }\n return value\n}\n\n/**\n * The key a node repeats over, trimmed, or `''` when it repeats over nothing.\n *\n * The one predicate every repeat question is asked through: the expansion, the\n * datasets-read gate below, the reusable-component graft that moves a\n * placement's repeat onto the element it becomes, and the besigner's preview\n * and badge. Two copies of it are how a `.trim()` on one side and not the\n * other becomes an empty list on a customer's page.\n */\nexport const repeatKey = (node: unknown): string => {\n const key = (node as { props?: { repeatDataset?: unknown } })?.props\n ?.repeatDataset\n return typeof key === 'string' ? key.trim() : ''\n}\n\n/**\n * Makes an element that holds children repeat ITSELF, once per record, instead\n * of what is inside it (AGL-3111): a card whose contents are the design, rather\n * than a list whose contents are the item.\n *\n * Persisted in screen, layout and component documents — never rename. `'true'`\n * counts too, because a switch round-tripped through a text-shaped store comes\n * back as the string.\n */\nexport const REPEAT_SELF_PROP = 'repeatSelf'\n\n/**\n * Every prop that directs a repeat. They are instructions to the composition,\n * never attributes of the element: the expansion consumes them from every node\n * it expands and every copy it makes, so a published element never carries\n * one. Persisted — never rename.\n */\nexport const REPEAT_DIRECTIVE_PROPS: readonly string[] = [\n 'repeatDataset',\n 'repeatLimit',\n 'repeatFilter',\n 'repeatSort',\n REPEAT_SELF_PROP,\n]\n\n/**\n * What a repeating node copies once per record (AGL-3111).\n *\n * - `children` — what is inside it. The node stays one element around the\n * copies: the list, grid or gallery they fill. This is how a Stack has\n * always repeated (AGL-103), and a stored Stack names neither scope.\n * - `self` — the node itself, with everything inside it. What a leaf and a\n * component instance repeat, and what any element carrying\n * {@link REPEAT_SELF_PROP} repeats.\n */\nexport type RepeatScope = 'self' | 'children'\n\n/**\n * Which scope a repeating node has, read off the node alone so the published\n * page's composition and the besigner decide it identically.\n *\n * {@link REPEAT_SELF_PROP} decides it whenever the node carries one, which is\n * every repeat the Attributes panel writes. Without one the node's own shape\n * decides, and it decides CONSERVATIVELY: anything that holds a child list —\n * a Stack, empty or not — keeps repeating its children, and only a node with\n * no child list at all repeats itself. A stored repeat predates the prop, so\n * this branch is the one that has to render it unchanged, and an author who\n * named a dataset before filling the container in would otherwise come back to\n * a hundred empty copies of it.\n *\n * A component instance never reaches that branch: the graft stamps the prop on\n * the element it becomes, whose children are the component's own.\n */\nexport function repeatScope(node: unknown): RepeatScope {\n const { props, nodes } = (node ?? {}) as {\n props?: Record<string, unknown>\n nodes?: unknown\n }\n // `'true'`/`'false'` too: a switch round-tripped through a text-shaped\n // store comes back as the string it was written as.\n const self = props?.[REPEAT_SELF_PROP]\n if (self === true || self === 'true') return 'self'\n if (self === false || self === 'false') return 'children'\n return Array.isArray(nodes) ? 'children' : 'self'\n}\n\n/**\n * One record's values put into `{{item.field}}` tokens, anywhere in a props\n * map — the substitution {@link expandRepeatables} makes on every copy it\n * builds, for a caller that already knows which record it is rendering.\n *\n * The besigner's canvas is that caller (AGL-3111): the element an author\n * edits IS the first copy, so it draws with the first record's values while\n * the node it saves keeps the tokens. Exported rather than reimplemented\n * because a canvas that resolved a token differently from the page would be\n * showing the author a page that does not exist.\n */\nexport function substituteRecordTokens<T>(\n props: T,\n context: {\n record: Record<string, unknown>\n /** The repeated rows' references (AGL-180). */\n model?: RepeatRowsModel\n /** Rows by key, for a one-hop reference (AGL-180). */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n },\n): T {\n return substituteValue(props, context) as T\n}\n\n/**\n * The one record a whole page is rendered for (AGL-3475): a record template's\n * routed record, in the shape a repeat hands each of its copies.\n */\nexport interface PageRecordScope {\n record: Record<string, unknown>\n /** The record's references (AGL-180). */\n model?: RepeatRowsModel\n /** Rows by key, for a one-hop reference (AGL-180). */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n}\n\n/**\n * Every node's props with the page's record put into its `{{item.field}}`\n * tokens — the substitution a repeat makes on each copy, made once over a\n * page that renders one record (AGL-3475).\n *\n * Run AFTER {@link expandRepeatables}: a repeat inside the page has already\n * spent its own `{{item.*}}` tokens on its own rows by then, so what is left\n * is the page's. A token naming a field the record does not have stays as\n * written, exactly as in a repeat. Returns the input when there is no record.\n */\nexport function substituteNodesRecordTokens<N>(\n nodes: Record<NodeId, N>,\n scope: PageRecordScope | null | undefined,\n): Record<NodeId, N> {\n if (!scope) return nodes\n const next: Record<NodeId, N> = {}\n for (const [id, node] of Object.entries(nodes) as Array<[NodeId, N]>) {\n const props = (node as { props?: unknown })?.props\n next[id] = props\n ? ({ ...node, props: substituteValue(props, scope) } as N)\n : node\n }\n return next\n}\n\n/**\n * The page's record put into each repeat's FILTER, before the repeats expand\n * (AGL-3475): on a record template, `category == {{item.category}}` lists the\n * other records in the routed record's category. Only `repeatFilter` is\n * touched — everything else inside a repeat is its template, whose\n * `{{item.*}}` belongs to its own rows. Returns the input when there is no\n * record or no filter names one.\n */\nexport function substituteRepeatFilterRecordTokens<N>(\n nodes: Record<NodeId, N>,\n scope: PageRecordScope | null | undefined,\n): Record<NodeId, N> {\n if (!scope) return nodes\n let next: Record<NodeId, N> | null = null\n for (const [id, node] of Object.entries(nodes) as Array<[NodeId, N]>) {\n const props = (node as { props?: Record<string, unknown> })?.props\n const filter = props?.['repeatFilter']\n if (typeof filter !== 'string' || !filter.includes('{{')) continue\n next ??= { ...nodes }\n next[id] = {\n ...node,\n props: { ...props, repeatFilter: substituteValue(filter, scope) },\n } as N\n }\n return next ?? nodes\n}\n\n/** The node minus its repeat directives, or the node itself when it has none. */\nexport function withoutRepeatDirective<N>(node: N): N {\n const props = (node as { props?: Record<string, unknown> })?.props\n if (!props || !REPEAT_DIRECTIVE_PROPS.some((key) => key in props)) {\n return node\n }\n const kept = { ...props }\n for (const key of REPEAT_DIRECTIVE_PROPS) delete kept[key]\n return { ...node, props: kept }\n}\n\n/**\n * The records a repeating node renders, in order (AGL-181): its filter and sort\n * evaluated over the rows, then its limit, never past\n * {@link REPEAT_MAX_RECORDS}. An unparseable filter or sort fails open.\n *\n * Shared by the expansion and the besigner's canvas preview and badge, so the\n * canvas can never show a copy or a count the published page does not.\n */\nexport function repeatedRecords(\n node: unknown,\n dataset: RepeatableDataset | undefined,\n): Array<Record<string, unknown>> {\n const props = ((node as { props?: unknown })?.props ?? {}) as Record<\n string,\n unknown\n >\n const where = parseRepeatFilter(String(props['repeatFilter'] ?? ''))\n const orderBy = parseRepeatSort(String(props['repeatSort'] ?? ''))\n const limit = Number(props['repeatLimit'])\n return applyRepeatQuery(dataset?.records ?? [], {\n ...(where ? { where: [where] } : {}),\n ...(orderBy ? { orderBy } : {}),\n }).slice(\n 0,\n Number.isFinite(limit) && limit > 0\n ? Math.min(limit, REPEAT_MAX_RECORDS)\n : REPEAT_MAX_RECORDS,\n )\n}\n\n/**\n * Does this tree contain anything {@link expandRepeatables} would expand —\n * or, with no rows, empty?\n *\n * The rows read is the largest on the render path — up to two pages of\n * {@link REPEAT_MAX_RECORDS} records for every key a page repeats over — and a\n * page that repeats over nothing should pay none of it (AGL-1440).\n *\n * Any gate on that read has to ask EXACTLY the question the expansion asks,\n * which is why this and {@link repeatKeys} share the one predicate\n * `expandRepeatables` looks keys up with, {@link repeatKey}. A gate that is\n * even slightly stricter than the expansion is not a saving: it is a published\n * page whose repeat never reaches the expansion at all, and so goes out as\n * its raw template — `{{item.*}}` tokens and all — where the author put a list.\n */\nexport function hasRepeatableNodes(\n nodes: Record<NodeId, unknown> | null | undefined,\n): boolean {\n if (!nodes) return false\n return Object.values(nodes).some((node) => repeatKey(node) !== '')\n}\n\n/**\n * Every key this tree repeats over, exactly as {@link expandRepeatables} looks\n * it up — sorted and without repeats.\n *\n * What the tenant compose asks the repeat-rows contract for\n * (`plugin-manager/repeat-rows.ts`), so a page loads the rows it renders and\n * not everything its site may see. Built on the same predicate as\n * {@link hasRepeatableNodes} for the reason given there.\n */\nexport function repeatKeys(\n nodes: Record<NodeId, unknown> | null | undefined,\n): string[] {\n const keys = new Set<string>()\n for (const node of Object.values(nodes ?? {})) {\n const key = repeatKey(node)\n if (key) keys.add(key)\n }\n return [...keys].sort()\n}\n\n/**\n * Repeats (AGL-103, AGL-3111): any node carrying `props.repeatDataset` (a\n * dataset id or display name) renders once per record of that dataset, with\n * `{{item.field}}` tokens in the copied string props replaced by the record's\n * values (unknown fields keep the literal token, like variable bindings).\n *\n * What is copied is the node's {@link repeatScope}:\n *\n * - `children` — its children are the item template. The node stays, and its\n * child list becomes the copies, record by record. Its own props are not the\n * template, so no token in them is substituted.\n * - `self` — the node and everything inside it are the template. The copies\n * take the node's place in its parent's child list, and every prop in the\n * subtree is substituted, the node's own included.\n *\n * - Copy ids are namespaced `rep__{repeatId}__{index}__…` — the repeat's own\n * id, then the template node's — so repeats never collide, including inside\n * grafted reusable components. A self-scoped node's first copy is\n * `rep__{id}__0__{id}`. Run this AFTER `composeReusableComponentNodes` and\n * BEFORE binding resolution.\n * - Rows are the node's {@link repeatedRecords}.\n * - The repeat directives ({@link REPEAT_DIRECTIVE_PROPS}) are consumed: every\n * node this expands, and every copy it makes, comes out without them.\n * - Repeats do not nest. A repeat inside another repeat's template is copied\n * with its directives consumed, so it renders once in each copy.\n * - No rows renders ZERO copies (AGL-3496): a `children` repeat keeps its\n * element with an empty child list, a `self` repeat leaves its parent's\n * list. That covers a filter matching nothing, an empty dataset, and a key\n * the rows answer nothing under — a deleted or unshared dataset, or no rows\n * map at all — because the template drawn without a record is its literal\n * `{{item.*}}` tokens, printed to every visitor. Never a throw: a dataset\n * going away empties a list, it does not take the page down.\n * - An empty template, or a self-scoped node its parent does not list (the\n * document root, say), has nothing to copy or nowhere to put copies, and is\n * left as written.\n * - Inputs are never mutated; templates stay in the map unreferenced.\n *\n * This is the PUBLISHED page's composition. The besigner's canvas keeps the\n * template on screen whatever the rows say — it is what an author edits — and\n * flags a repeat with no rows on its badge instead ({@link repeatedRecords}\n * is the count both read).\n */\nexport function expandRepeatables<N extends AglynNodeSchema = AglynNodeSchema>(\n nodes: Record<NodeId, N>,\n datasetsByKey: Record<string, RepeatableDataset | undefined> | undefined,\n): Record<NodeId, N> {\n const rowsByKey = datasetsByKey ?? {}\n const repeatIds = Object.entries(nodes).filter(\n ([, node]) => repeatKey(node) !== '',\n )\n if (!repeatIds.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [repeatId, repeated] of repeatIds) {\n const dataset = rowsByKey[repeatKey(repeated)]\n const records = repeatedRecords(repeated, dataset)\n const self = repeatScope(repeated) === 'self'\n // The copies' parent: the node itself, or — for a self-scoped node — the\n // parent whose child list the copies take its place in, read from `next`\n // so two self-scoped siblings both land in the list the other has left.\n const parentId = (self ? repeated.parentId : repeatId) as NodeId\n const siblings = next[parentId]?.nodes\n const slot = Array.isArray(siblings)\n ? (siblings as NodeId[]).indexOf(repeatId)\n : -1\n const templateIds = self\n ? [repeatId]\n : Array.isArray(repeated.nodes)\n ? (repeated.nodes as NodeId[])\n : []\n next[repeatId] = withoutRepeatDirective(next[repeatId])\n if (!templateIds.length || (self && slot < 0)) continue\n if (!records.length) {\n // Zero copies (AGL-3496). The template is a design, not content: drawn\n // without a record it prints `{{item.name}}` to every visitor.\n if (self) {\n const listed = [...(next[parentId].nodes as NodeId[])]\n listed.splice(slot, 1)\n next[parentId] = { ...next[parentId], nodes: listed }\n } else {\n next[repeatId] = { ...next[repeatId], nodes: [] }\n }\n continue\n }\n\n const copyIds: NodeId[] = []\n records.forEach((record, index) => {\n const prefix = `${REPEAT_NODE_ID_PREFIX}${repeatId}__${index}__`\n const prefixId = (id: NodeId) => `${prefix}${id}`\n const cloneSubtree = (id: NodeId, clonedParentId: NodeId) => {\n const node = nodes[id]\n if (!node) return\n const clonedChildren = Array.isArray(node.nodes)\n ? (node.nodes as NodeId[])\n : undefined\n next[prefixId(id)] = withoutRepeatDirective({\n ...node,\n $id: prefixId(id),\n parentId: clonedParentId,\n props: substituteValue(node.props ?? {}, {\n record,\n model: dataset?.model,\n datasetsByKey: rowsByKey,\n }) as any,\n ...(clonedChildren && {\n nodes: clonedChildren.map((childId) => prefixId(childId)),\n }),\n })\n clonedChildren?.forEach((childId) =>\n cloneSubtree(childId, prefixId(id)),\n )\n }\n for (const templateId of templateIds) {\n cloneSubtree(templateId, parentId)\n copyIds.push(prefixId(templateId))\n }\n })\n if (self) {\n const listed = [...(next[parentId].nodes as NodeId[])]\n listed.splice(slot, 1, ...copyIds)\n next[parentId] = { ...next[parentId], nodes: listed }\n } else {\n next[repeatId] = { ...next[repeatId], nodes: copyIds }\n }\n }\n return next\n}\n\nexport default expandRepeatables\n"],"names":["applyRepeatQuery","parseRepeatFilter","parseRepeatSort","REPEAT_NODE_ID_PREFIX","ITEM_TOKEN_PATTERN","REPEAT_MAX_RECORDS","displayValue","value","Array","isArray","join","String","resolveReferenceHop","context","fieldId","targetFieldId","targetKey","model","references","target","datasetsByKey","keys","record","ids","resolved","map","id","match","records","find","row","filter","length","substituteValue","replace","token","field","hop","entry","next","key","Object","entries","repeatKey","node","props","repeatDataset","trim","REPEAT_SELF_PROP","REPEAT_DIRECTIVE_PROPS","repeatScope","nodes","self","substituteRecordTokens","substituteNodesRecordTokens","scope","substituteRepeatFilterRecordTokens","includes","repeatFilter","withoutRepeatDirective","some","kept","repeatedRecords","dataset","where","orderBy","limit","Number","slice","isFinite","Math","min","hasRepeatableNodes","values","repeatKeys","Set","add","sort","expandRepeatables","rowsByKey","repeatIds","repeatId","repeated","parentId","siblings","slot","indexOf","templateIds","listed","splice","copyIds","forEach","index","prefix","prefixId","cloneSubtree","clonedParentId","clonedChildren","undefined","$id","childId","templateId","push"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/expand-repeatables.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 type { AglynNodeSchema, NodeId } from '../foundation'\nimport {\n applyRepeatQuery,\n parseRepeatFilter,\n parseRepeatSort,\n} from './repeat-query'\n\n/** Namespaces cloned template ids per container/record. */\nexport const REPEAT_NODE_ID_PREFIX = 'rep__'\n\n/**\n * `{{item.field}}` token, plus an optional single reference hop\n * (`{{item.author.name}}`, AGL-180). One hop only — the pattern itself is\n * the depth guard, so cycles can't recurse.\n */\nconst ITEM_TOKEN_PATTERN =\n /\\{\\{\\s*item\\.([A-Za-z][A-Za-z0-9_]*)(?:\\.([A-Za-z][A-Za-z0-9_]*))?\\s*\\}\\}/g\n\n/** Hard bound on rows a single repeatable renders, before `repeatLimit`. */\nexport const REPEAT_MAX_RECORDS = 100\n\n/**\n * What the expansion reads of the rows' field model (AGL-177, AGL-180): which\n * fields are references, each with the key its target's rows are answered\n * under (`datasetsByKey`). The model itself is the plugin's that keeps the\n * rows, and it states this much of it when it hands the rows over\n * (`plugin-manager/repeat-rows`, `repeat-sources`); a field it does not name\n * here hops nowhere, and its token is left as written.\n */\nexport interface RepeatRowsModel {\n /** Reference field id → the key of the rows it points into. */\n references?: Readonly<Record<string, string>>\n}\n\nexport interface RepeatableDataset {\n /**\n * Row value maps, in display order. Rows carry `$id` (needed to resolve\n * incoming references); values may be typed (AGL-177), stringified at\n * substitution.\n */\n records: Array<Record<string, unknown>>\n /** The rows' references; required for reference-hop bindings. */\n model?: RepeatRowsModel\n}\n\ninterface SubstituteContext {\n record: Record<string, unknown>\n /** The repeated rows' references (AGL-180). */\n model?: RepeatRowsModel\n /** All host datasets keyed by id (and name) for hop resolution. */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n /**\n * What a token whose field the record leaves empty becomes: `'token'` keeps\n * it as written (the besigner's canvas, where an author edits the template),\n * `'empty'` prints nothing (a rendered page, AGL-3616). A published page\n * never shows a visitor `{{item.venue}}` because one record has no venue.\n */\n missing?: 'token' | 'empty'\n}\n\nconst displayValue = (value: unknown): string =>\n Array.isArray(value) ? value.join(', ') : String(value)\n\n/** Resolves `{{item.ref.field}}`: FKey(s) → target document field value. */\nfunction resolveReferenceHop(\n context: SubstituteContext,\n fieldId: string,\n targetFieldId: string,\n): string | null {\n const targetKey = context.model?.references?.[fieldId]\n if (!targetKey) return null\n const target = context.datasetsByKey?.[targetKey]\n if (!target) return null\n const keys = context.record[fieldId]\n const ids = Array.isArray(keys) ? keys : keys != null ? [keys] : []\n const resolved = ids\n .map((id) => {\n const match = target.records.find((row) => row['$id'] === id)\n const value = match?.[targetFieldId]\n return value != null ? displayValue(value) : null\n })\n .filter((value): value is string => value != null)\n return resolved.length ? resolved.join(', ') : null\n}\n\nfunction substituteValue(\n value: unknown,\n context: SubstituteContext,\n): unknown {\n const { record } = context\n if (typeof value === 'string') {\n const unresolved = (token: string) =>\n context.missing === 'empty' ? '' : token\n return value.replace(\n ITEM_TOKEN_PATTERN,\n (token, field: string, hop?: string) => {\n if (hop) {\n const resolved = resolveReferenceHop(context, field, hop)\n return resolved != null ? resolved : unresolved(token)\n }\n return record[field] != null\n ? displayValue(record[field])\n : unresolved(token)\n },\n )\n }\n if (Array.isArray(value)) {\n return value.map((entry) => substituteValue(entry, context))\n }\n if (value && typeof value === 'object') {\n const next: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(value)) {\n next[key] = substituteValue(entry, context)\n }\n return next\n }\n return value\n}\n\n/** A text that is nothing but `{{item.*}}` tokens: a binding, not words. */\nconst WHOLE_BINDING = new RegExp(\n `^\\\\s*(?:${ITEM_TOKEN_PATTERN.source}\\\\s*)+$`,\n)\n\n/**\n * Whether a copy's element is a binding its record left empty (AGL-3616):\n * its text, `children`, was only `{{item.*}}` tokens and they printed\n * nothing. The copy leaves the element out rather than drawing an empty line\n * (an eyebrow with no venue), and a record that fills the field draws it.\n */\nfunction isEmptyBinding(\n template: Record<string, unknown> | undefined,\n substituted: Record<string, unknown>,\n): boolean {\n const text = template?.['children']\n if (typeof text !== 'string' || !WHOLE_BINDING.test(text)) return false\n const printed = substituted['children']\n return typeof printed === 'string' && printed.trim() === ''\n}\n\n/**\n * The key a node repeats over, trimmed, or `''` when it repeats over nothing.\n *\n * The one predicate every repeat question is asked through: the expansion, the\n * datasets-read gate below, the reusable-component graft that moves a\n * placement's repeat onto the element it becomes, and the besigner's preview\n * and badge. Two copies of it are how a `.trim()` on one side and not the\n * other becomes an empty list on a customer's page.\n */\nexport const repeatKey = (node: unknown): string => {\n const key = (node as { props?: { repeatDataset?: unknown } })?.props\n ?.repeatDataset\n return typeof key === 'string' ? key.trim() : ''\n}\n\n/**\n * Makes an element that holds children repeat ITSELF, once per record, instead\n * of what is inside it (AGL-3111): a card whose contents are the design, rather\n * than a list whose contents are the item.\n *\n * Persisted in screen, layout and component documents — never rename. `'true'`\n * counts too, because a switch round-tripped through a text-shaped store comes\n * back as the string.\n */\nexport const REPEAT_SELF_PROP = 'repeatSelf'\n\n/**\n * Every prop that directs a repeat. They are instructions to the composition,\n * never attributes of the element: the expansion consumes them from every node\n * it expands and every copy it makes, so a published element never carries\n * one. Persisted — never rename.\n */\nexport const REPEAT_DIRECTIVE_PROPS: readonly string[] = [\n 'repeatDataset',\n 'repeatLimit',\n 'repeatFilter',\n 'repeatSort',\n REPEAT_SELF_PROP,\n]\n\n/**\n * What a repeating node copies once per record (AGL-3111).\n *\n * - `children` — what is inside it. The node stays one element around the\n * copies: the list, grid or gallery they fill. This is how a Stack has\n * always repeated (AGL-103), and a stored Stack names neither scope.\n * - `self` — the node itself, with everything inside it. What a leaf and a\n * component instance repeat, and what any element carrying\n * {@link REPEAT_SELF_PROP} repeats.\n */\nexport type RepeatScope = 'self' | 'children'\n\n/**\n * Which scope a repeating node has, read off the node alone so the published\n * page's composition and the besigner decide it identically.\n *\n * {@link REPEAT_SELF_PROP} decides it whenever the node carries one, which is\n * every repeat the Attributes panel writes. Without one the node's own shape\n * decides, and it decides CONSERVATIVELY: anything that holds a child list —\n * a Stack, empty or not — keeps repeating its children, and only a node with\n * no child list at all repeats itself. A stored repeat predates the prop, so\n * this branch is the one that has to render it unchanged, and an author who\n * named a dataset before filling the container in would otherwise come back to\n * a hundred empty copies of it.\n *\n * A component instance never reaches that branch: the graft stamps the prop on\n * the element it becomes, whose children are the component's own.\n */\nexport function repeatScope(node: unknown): RepeatScope {\n const { props, nodes } = (node ?? {}) as {\n props?: Record<string, unknown>\n nodes?: unknown\n }\n // `'true'`/`'false'` too: a switch round-tripped through a text-shaped\n // store comes back as the string it was written as.\n const self = props?.[REPEAT_SELF_PROP]\n if (self === true || self === 'true') return 'self'\n if (self === false || self === 'false') return 'children'\n return Array.isArray(nodes) ? 'children' : 'self'\n}\n\n/**\n * One record's values put into `{{item.field}}` tokens, anywhere in a props\n * map — the substitution {@link expandRepeatables} makes on every copy it\n * builds, for a caller that already knows which record it is rendering.\n *\n * The besigner's canvas is that caller (AGL-3111): the element an author\n * edits IS the first copy, so it draws with the first record's values while\n * the node it saves keeps the tokens. Exported rather than reimplemented\n * because a canvas that resolved a token differently from the page would be\n * showing the author a page that does not exist.\n */\nexport function substituteRecordTokens<T>(\n props: T,\n context: {\n record: Record<string, unknown>\n /** The repeated rows' references (AGL-180). */\n model?: RepeatRowsModel\n /** Rows by key, for a one-hop reference (AGL-180). */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n },\n): T {\n return substituteValue(props, context) as T\n}\n\n/**\n * The one record a whole page is rendered for (AGL-3475): a record template's\n * routed record, in the shape a repeat hands each of its copies.\n */\nexport interface PageRecordScope {\n record: Record<string, unknown>\n /** The record's references (AGL-180). */\n model?: RepeatRowsModel\n /** Rows by key, for a one-hop reference (AGL-180). */\n datasetsByKey?: Record<string, RepeatableDataset | undefined>\n}\n\n/**\n * Every node's props with the page's record put into its `{{item.field}}`\n * tokens — the substitution a repeat makes on each copy, made once over a\n * page that renders one record (AGL-3475).\n *\n * Run AFTER {@link expandRepeatables}: a repeat inside the page has already\n * spent its own `{{item.*}}` tokens on its own rows by then, so what is left\n * is the page's. A token naming a field the record leaves empty prints\n * nothing, exactly as in a repeat (AGL-3616). Returns the input when there is\n * no record.\n */\nexport function substituteNodesRecordTokens<N>(\n nodes: Record<NodeId, N>,\n scope: PageRecordScope | null | undefined,\n): Record<NodeId, N> {\n if (!scope) return nodes\n const next: Record<NodeId, N> = {}\n for (const [id, node] of Object.entries(nodes) as Array<[NodeId, N]>) {\n const props = (node as { props?: unknown })?.props\n next[id] = props\n ? ({\n ...node,\n props: substituteValue(props, { ...scope, missing: 'empty' }),\n } as N)\n : node\n }\n return next\n}\n\n/**\n * The page's record put into each repeat's FILTER, before the repeats expand\n * (AGL-3475): on a record template, `category == {{item.category}}` lists the\n * other records in the routed record's category. Only `repeatFilter` is\n * touched — everything else inside a repeat is its template, whose\n * `{{item.*}}` belongs to its own rows. Returns the input when there is no\n * record or no filter names one.\n */\nexport function substituteRepeatFilterRecordTokens<N>(\n nodes: Record<NodeId, N>,\n scope: PageRecordScope | null | undefined,\n): Record<NodeId, N> {\n if (!scope) return nodes\n let next: Record<NodeId, N> | null = null\n for (const [id, node] of Object.entries(nodes) as Array<[NodeId, N]>) {\n const props = (node as { props?: Record<string, unknown> })?.props\n const filter = props?.['repeatFilter']\n if (typeof filter !== 'string' || !filter.includes('{{')) continue\n next ??= { ...nodes }\n next[id] = {\n ...node,\n props: { ...props, repeatFilter: substituteValue(filter, scope) },\n } as N\n }\n return next ?? nodes\n}\n\n/** The node minus its repeat directives, or the node itself when it has none. */\nexport function withoutRepeatDirective<N>(node: N): N {\n const props = (node as { props?: Record<string, unknown> })?.props\n if (!props || !REPEAT_DIRECTIVE_PROPS.some((key) => key in props)) {\n return node\n }\n const kept = { ...props }\n for (const key of REPEAT_DIRECTIVE_PROPS) delete kept[key]\n return { ...node, props: kept }\n}\n\n/**\n * The records a repeating node renders, in order (AGL-181): its filter and sort\n * evaluated over the rows, then its limit, never past\n * {@link REPEAT_MAX_RECORDS}. An unparseable filter or sort fails open.\n *\n * Shared by the expansion and the besigner's canvas preview and badge, so the\n * canvas can never show a copy or a count the published page does not.\n */\nexport function repeatedRecords(\n node: unknown,\n dataset: RepeatableDataset | undefined,\n): Array<Record<string, unknown>> {\n const props = ((node as { props?: unknown })?.props ?? {}) as Record<\n string,\n unknown\n >\n const where = parseRepeatFilter(String(props['repeatFilter'] ?? ''))\n const orderBy = parseRepeatSort(String(props['repeatSort'] ?? ''))\n const limit = Number(props['repeatLimit'])\n return applyRepeatQuery(dataset?.records ?? [], {\n ...(where ? { where: [where] } : {}),\n ...(orderBy ? { orderBy } : {}),\n }).slice(\n 0,\n Number.isFinite(limit) && limit > 0\n ? Math.min(limit, REPEAT_MAX_RECORDS)\n : REPEAT_MAX_RECORDS,\n )\n}\n\n/**\n * Does this tree contain anything {@link expandRepeatables} would expand —\n * or, with no rows, empty?\n *\n * The rows read is the largest on the render path — up to two pages of\n * {@link REPEAT_MAX_RECORDS} records for every key a page repeats over — and a\n * page that repeats over nothing should pay none of it (AGL-1440).\n *\n * Any gate on that read has to ask EXACTLY the question the expansion asks,\n * which is why this and {@link repeatKeys} share the one predicate\n * `expandRepeatables` looks keys up with, {@link repeatKey}. A gate that is\n * even slightly stricter than the expansion is not a saving: it is a published\n * page whose repeat never reaches the expansion at all, and so goes out as\n * its raw template — `{{item.*}}` tokens and all — where the author put a list.\n */\nexport function hasRepeatableNodes(\n nodes: Record<NodeId, unknown> | null | undefined,\n): boolean {\n if (!nodes) return false\n return Object.values(nodes).some((node) => repeatKey(node) !== '')\n}\n\n/**\n * Every key this tree repeats over, exactly as {@link expandRepeatables} looks\n * it up — sorted and without repeats.\n *\n * What the tenant compose asks the repeat-rows contract for\n * (`plugin-manager/repeat-rows.ts`), so a page loads the rows it renders and\n * not everything its site may see. Built on the same predicate as\n * {@link hasRepeatableNodes} for the reason given there.\n */\nexport function repeatKeys(\n nodes: Record<NodeId, unknown> | null | undefined,\n): string[] {\n const keys = new Set<string>()\n for (const node of Object.values(nodes ?? {})) {\n const key = repeatKey(node)\n if (key) keys.add(key)\n }\n return [...keys].sort()\n}\n\n/**\n * Repeats (AGL-103, AGL-3111): any node carrying `props.repeatDataset` (a\n * dataset id or display name) renders once per record of that dataset, with\n * `{{item.field}}` tokens in the copied string props replaced by the record's\n * values. A field the record leaves empty prints nothing — never the raw\n * token — and an element whose whole text is such a binding is left out of\n * that copy (AGL-3616: a tour date with no venue drew `{{ITEM.VENUE}}` as its\n * eyebrow on a published site). The besigner's canvas keeps the tokens of\n * the template an author edits ({@link substituteRecordTokens}).\n *\n * What is copied is the node's {@link repeatScope}:\n *\n * - `children` — its children are the item template. The node stays, and its\n * child list becomes the copies, record by record. Its own props are not the\n * template, so no token in them is substituted.\n * - `self` — the node and everything inside it are the template. The copies\n * take the node's place in its parent's child list, and every prop in the\n * subtree is substituted, the node's own included.\n *\n * - Copy ids are namespaced `rep__{repeatId}__{index}__…` — the repeat's own\n * id, then the template node's — so repeats never collide, including inside\n * grafted reusable components. A self-scoped node's first copy is\n * `rep__{id}__0__{id}`. Run this AFTER `composeReusableComponentNodes` and\n * BEFORE binding resolution.\n * - Rows are the node's {@link repeatedRecords}.\n * - The repeat directives ({@link REPEAT_DIRECTIVE_PROPS}) are consumed: every\n * node this expands, and every copy it makes, comes out without them.\n * - Repeats do not nest. A repeat inside another repeat's template is copied\n * with its directives consumed, so it renders once in each copy.\n * - No rows renders ZERO copies (AGL-3496): a `children` repeat keeps its\n * element with an empty child list, a `self` repeat leaves its parent's\n * list. That covers a filter matching nothing, an empty dataset, and a key\n * the rows answer nothing under — a deleted or unshared dataset, or no rows\n * map at all — because the template drawn without a record is its literal\n * `{{item.*}}` tokens, printed to every visitor. Never a throw: a dataset\n * going away empties a list, it does not take the page down.\n * - An empty template, or a self-scoped node its parent does not list (the\n * document root, say), has nothing to copy or nowhere to put copies, and is\n * left as written.\n * - Inputs are never mutated; templates stay in the map unreferenced.\n *\n * This is the PUBLISHED page's composition. The besigner's canvas keeps the\n * template on screen whatever the rows say — it is what an author edits — and\n * flags a repeat with no rows on its badge instead ({@link repeatedRecords}\n * is the count both read).\n */\nexport function expandRepeatables<N extends AglynNodeSchema = AglynNodeSchema>(\n nodes: Record<NodeId, N>,\n datasetsByKey: Record<string, RepeatableDataset | undefined> | undefined,\n): Record<NodeId, N> {\n const rowsByKey = datasetsByKey ?? {}\n const repeatIds = Object.entries(nodes).filter(\n ([, node]) => repeatKey(node) !== '',\n )\n if (!repeatIds.length) return nodes\n\n const next: Record<NodeId, N> = { ...nodes }\n for (const [repeatId, repeated] of repeatIds) {\n const dataset = rowsByKey[repeatKey(repeated)]\n const records = repeatedRecords(repeated, dataset)\n const self = repeatScope(repeated) === 'self'\n // The copies' parent: the node itself, or — for a self-scoped node — the\n // parent whose child list the copies take its place in, read from `next`\n // so two self-scoped siblings both land in the list the other has left.\n const parentId = (self ? repeated.parentId : repeatId) as NodeId\n const siblings = next[parentId]?.nodes\n const slot = Array.isArray(siblings)\n ? (siblings as NodeId[]).indexOf(repeatId)\n : -1\n const templateIds = self\n ? [repeatId]\n : Array.isArray(repeated.nodes)\n ? (repeated.nodes as NodeId[])\n : []\n next[repeatId] = withoutRepeatDirective(next[repeatId])\n if (!templateIds.length || (self && slot < 0)) continue\n if (!records.length) {\n // Zero copies (AGL-3496). The template is a design, not content: drawn\n // without a record it prints `{{item.name}}` to every visitor.\n if (self) {\n const listed = [...(next[parentId].nodes as NodeId[])]\n listed.splice(slot, 1)\n next[parentId] = { ...next[parentId], nodes: listed }\n } else {\n next[repeatId] = { ...next[repeatId], nodes: [] }\n }\n continue\n }\n\n const copyIds: NodeId[] = []\n records.forEach((record, index) => {\n const prefix = `${REPEAT_NODE_ID_PREFIX}${repeatId}__${index}__`\n const prefixId = (id: NodeId) => `${prefix}${id}`\n // Whether the copy drew the node: an element whose whole text is a\n // binding this record leaves empty is left out of it (AGL-3616).\n const cloneSubtree = (id: NodeId, clonedParentId: NodeId): boolean => {\n const node = nodes[id]\n if (!node) return false\n const props = substituteValue(node.props ?? {}, {\n record,\n model: dataset?.model,\n datasetsByKey: rowsByKey,\n missing: 'empty',\n }) as Record<string, unknown>\n if (isEmptyBinding(node.props as Record<string, unknown>, props)) {\n return false\n }\n const clonedChildren = Array.isArray(node.nodes)\n ? (node.nodes as NodeId[])\n : undefined\n const drawn = clonedChildren?.filter((childId) =>\n cloneSubtree(childId, prefixId(id)),\n )\n next[prefixId(id)] = withoutRepeatDirective({\n ...node,\n $id: prefixId(id),\n parentId: clonedParentId,\n props: props as any,\n ...(drawn && {\n nodes: drawn.map((childId) => prefixId(childId)),\n }),\n })\n return true\n }\n for (const templateId of templateIds) {\n if (cloneSubtree(templateId, parentId)) {\n copyIds.push(prefixId(templateId))\n }\n }\n })\n if (self) {\n const listed = [...(next[parentId].nodes as NodeId[])]\n listed.splice(slot, 1, ...copyIds)\n next[parentId] = { ...next[parentId], nodes: listed }\n } else {\n next[repeatId] = { ...next[repeatId], nodes: copyIds }\n }\n }\n return next\n}\n\nexport default expandRepeatables\n"],"names":["applyRepeatQuery","parseRepeatFilter","parseRepeatSort","REPEAT_NODE_ID_PREFIX","ITEM_TOKEN_PATTERN","REPEAT_MAX_RECORDS","displayValue","value","Array","isArray","join","String","resolveReferenceHop","context","fieldId","targetFieldId","targetKey","model","references","target","datasetsByKey","keys","record","ids","resolved","map","id","match","records","find","row","filter","length","substituteValue","unresolved","token","missing","replace","field","hop","entry","next","key","Object","entries","WHOLE_BINDING","RegExp","source","isEmptyBinding","template","substituted","text","test","printed","trim","repeatKey","node","props","repeatDataset","REPEAT_SELF_PROP","REPEAT_DIRECTIVE_PROPS","repeatScope","nodes","self","substituteRecordTokens","substituteNodesRecordTokens","scope","substituteRepeatFilterRecordTokens","includes","repeatFilter","withoutRepeatDirective","some","kept","repeatedRecords","dataset","where","orderBy","limit","Number","slice","isFinite","Math","min","hasRepeatableNodes","values","repeatKeys","Set","add","sort","expandRepeatables","rowsByKey","repeatIds","repeatId","repeated","parentId","siblings","slot","indexOf","templateIds","listed","splice","copyIds","forEach","index","prefix","prefixId","cloneSubtree","clonedParentId","clonedChildren","undefined","drawn","childId","$id","templateId","push"],"mappings":"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"}
|