@aglyn/tenant-runtime 1.0.0-beta.222 → 1.0.0-beta.223

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,36 @@ import { getScreen } from '@aglyn/tenant-runtime/get-screen'
92
92
  const screen = await getScreen({ hostId, screenId })
93
93
  ```
94
94
 
95
+ ## The render path never awaits unbounded I/O
96
+
97
+ A page render, and everything it awaits — the layouts, the page-data loader,
98
+ this pipeline, plugin page enrichers, resolvers, redirect resolvers, repeat
99
+ readers and computed variables, and the middleware in front of them — never
100
+ waits on outbound network I/O without a deadline of its own (AGL-3565).
101
+
102
+ - Wrap the await in `boundedAwait(work, ms, fallback, label)` from
103
+ `@aglyn/shared-util-http/bounded-await`: a real timer race that answers with
104
+ `fallback` when `ms` passes, whatever the work does. Pass its `signal` to
105
+ `fetch` so the request is canceled too, and put the body read inside the
106
+ work. An `AbortSignal` on its own is not a deadline: on Vercel, Next's
107
+ patched `fetch` handed back a promise that never settled with
108
+ `AbortSignal.timeout(2500)` on it, and every uncached page on the platform
109
+ waited for the 60-second function limit.
110
+ - Pick a fallback the page can render with, and say what it means: the
111
+ linked font stylesheet, the verdict "unreachable", a redirect rule held.
112
+ - A process cache that outlives a request holds settled values only
113
+ (`createSettledValueCache`, which refuses a promise). A cached in-flight
114
+ promise is awaited by every later render, each with a fresh deadline in
115
+ front of nothing.
116
+ - Work nothing needs the answer of is detached with `void`, never awaited.
117
+
118
+ `apps/tenant/specs/render-path-bounded-io.spec.ts` sweeps every symbol a
119
+ render can reach for a `fetch`, a `fetch` passed on as a value, a token
120
+ exchange or `http(s).request`, and refuses one that is not behind
121
+ `boundedAwait` or `void`. Its allowlist carries a reason per row and may only
122
+ shrink. Firestore reads through `firebase-admin` are outside it: they carry
123
+ the client's own deadlines.
124
+
95
125
  ## How it fits
96
126
 
97
127
  This is the `tenant` scope of the package map. It depends only on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/tenant-runtime",
3
- "version": "1.0.0-beta.222",
3
+ "version": "1.0.0-beta.223",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,10 +25,11 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.222",
29
- "@aglyn/shared-data-types": "1.0.0-beta.222",
30
- "@aglyn/shared-ui-theme": "1.0.0-beta.222",
31
- "@aglyn/tenant-data-admin": "1.0.0-beta.222",
28
+ "@aglyn/aglyn": "1.0.0-beta.223",
29
+ "@aglyn/shared-data-types": "1.0.0-beta.223",
30
+ "@aglyn/shared-ui-theme": "1.0.0-beta.223",
31
+ "@aglyn/shared-util-http": "1.0.0-beta.223",
32
+ "@aglyn/tenant-data-admin": "1.0.0-beta.223",
32
33
  "@swc/helpers": "0.5.23"
33
34
  },
34
35
  "peerDependencies": {
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { boundedAwait, createSettledValueCache } from "@aglyn/shared-util-http/bounded-await";
17
18
  // By path: the theme lib's barrel carries React providers a server module
18
19
  // has no use for (AGL-405).
19
20
  import { getGoogleFontsUrl } from "@aglyn/shared-ui-theme/util/host-theme";
@@ -33,46 +34,44 @@ import { googleFontFileUrl, isSelfHostedFontPath, parseGoogleFontFaces, selfHost
33
34
  /** How long a page render waits for Google before falling back. */ const STYLESHEET_TIMEOUT_MS = 2500;
34
35
  /** The largest font file the route will pass on. */ const MAX_FONT_BYTES = 2 * 1024 * 1024;
35
36
  /** A font file's URL never changes its bytes: the version is in the path. */ export const SELF_HOSTED_FONT_CACHE_CONTROL = 'public, max-age=31536000, s-maxage=31536000, immutable';
36
- const stylesheets = new Map();
37
+ /**
38
+ * What this process learned about each stylesheet: the faces, or null for a
39
+ * failure. VALUES, never an in-flight promise — the settled-value cache
40
+ * refuses one. A promise held across requests is awaited by renders that did
41
+ * not create it, and one that never settles stalled every page of every site
42
+ * on the instance until the function timed out (AGL-3565).
43
+ */ const stylesheets = createSettledValueCache();
37
44
  /** Test seam: the process cache would otherwise leak between cases. */ export function resetSelfHostedFontsForTests() {
38
45
  stylesheets.clear();
39
46
  }
47
+ /**
48
+ * Google's faces for a stylesheet URL, or null; never waits past `ms`. The
49
+ * deadline is a real timer (`boundedAwait`): an abort signal alone did not
50
+ * bound Next's patched fetch on Vercel, whose promise could never settle.
51
+ */ function fetchFaces(url, ms) {
52
+ return boundedAwait((signal)=>fetch(url, {
53
+ headers: {
54
+ 'User-Agent': WOFF2_USER_AGENT
55
+ },
56
+ signal,
57
+ next: {
58
+ revalidate: STYLESHEET_TTL_MS / 1000
59
+ }
60
+ }).then(async (response)=>{
61
+ if (!response.ok) return null;
62
+ const parsed = parseGoogleFontFaces(await response.text());
63
+ return parsed.length ? parsed : null;
64
+ }).catch(()=>null), ms, null, 'self-hosted-fonts.stylesheet');
65
+ }
40
66
  /**
41
67
  * Google's faces for a stylesheet URL, from this process's cache when it has
42
- * them. Null when the fetch failed or answered with nothing usable; the page
43
- * then links the stylesheet as it always did, rather than losing its font.
44
- *
45
- * Asked through `fetch` with a day's revalidation, which on the server is
68
+ * them. Null when the fetch failed, timed out or answered with nothing usable;
69
+ * the page then links the stylesheet as it always did, rather than losing its
70
+ * font. Asked through `fetch` with a day's revalidation, which on the server is
46
71
  * Next's data cache: a cold process reads the stored stylesheet rather than
47
72
  * asking Google again.
48
73
  */ function facesFor(url) {
49
- const now = Date.now();
50
- const held = stylesheets.get(url);
51
- if (held && held.expires > now) return held.faces;
52
- const faces = fetch(url, {
53
- headers: {
54
- 'User-Agent': WOFF2_USER_AGENT
55
- },
56
- signal: AbortSignal.timeout(STYLESHEET_TIMEOUT_MS),
57
- next: {
58
- revalidate: STYLESHEET_TTL_MS / 1000
59
- }
60
- }).then(async (response)=>{
61
- if (!response.ok) return null;
62
- const parsed = parseGoogleFontFaces(await response.text());
63
- return parsed.length ? parsed : null;
64
- }).catch(()=>null);
65
- stylesheets.set(url, {
66
- expires: now + STYLESHEET_TTL_MS,
67
- faces
68
- });
69
- void faces.then((found)=>{
70
- if (!found) stylesheets.set(url, {
71
- expires: now + FAILURE_TTL_MS,
72
- faces
73
- });
74
- });
75
- return faces;
74
+ return stylesheets.readThrough(url, ()=>fetchFaces(url, STYLESHEET_TIMEOUT_MS), (faces)=>faces ? STYLESHEET_TTL_MS : FAILURE_TTL_MS);
76
75
  }
77
76
  /**
78
77
  * The theme's Google fonts as rules to inline and files to preload, or null
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/self-hosted-fonts.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 { HostTheme } from '@aglyn/shared-data-types'\n// By path: the theme lib's barrel carries React providers a server module\n// has no use for (AGL-405).\nimport { getGoogleFontsUrl } from '@aglyn/shared-ui-theme/util/host-theme'\nimport {\n type GoogleFontFace,\n googleFontFileUrl,\n isSelfHostedFontPath,\n parseGoogleFontFaces,\n selfHostedFontFaceCss,\n selfHostedFontPreloads,\n} from '@aglyn/shared-ui-theme/util/self-hosted-fonts'\n\n/**\n * The tenant's half of self-hosted theme fonts (AGL-3485): fetching Google's\n * stylesheet for a theme, and serving the files it names from the site's own\n * origin. What is done with the stylesheet, and why, is the note on\n * `@aglyn/shared-ui-theme/util/self-hosted-fonts`.\n */\n\n/**\n * A current desktop Chrome. Google's CSS2 API picks the font FORMAT from the\n * user agent, and a server's default agent is answered with TrueType; this\n * one is answered with WOFF2, the format every browser a site supports reads.\n */\nconst WOFF2_USER_AGENT =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +\n '(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'\n\n/** How long one process trusts a stylesheet it fetched. */\nconst STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000\n\n/** How long a failed fetch is remembered before it is tried again. */\nconst FAILURE_TTL_MS = 5 * 60 * 1000\n\n/** How long a page render waits for Google before falling back. */\nconst STYLESHEET_TIMEOUT_MS = 2_500\n\n/** The largest font file the route will pass on. */\nconst MAX_FONT_BYTES = 2 * 1024 * 1024\n\n/** A font file's URL never changes its bytes: the version is in the path. */\nexport const SELF_HOSTED_FONT_CACHE_CONTROL =\n 'public, max-age=31536000, s-maxage=31536000, immutable'\n\nconst stylesheets = new Map<\n string,\n { expires: number; faces: Promise<GoogleFontFace[] | null> }\n>()\n\n/** Test seam: the process cache would otherwise leak between cases. */\nexport function resetSelfHostedFontsForTests(): void {\n stylesheets.clear()\n}\n\n/**\n * Google's faces for a stylesheet URL, from this process's cache when it has\n * them. Null when the fetch failed or answered with nothing usable; the page\n * then links the stylesheet as it always did, rather than losing its font.\n *\n * Asked through `fetch` with a day's revalidation, which on the server is\n * Next's data cache: a cold process reads the stored stylesheet rather than\n * asking Google again.\n */\nfunction facesFor(url: string): Promise<GoogleFontFace[] | null> {\n const now = Date.now()\n const held = stylesheets.get(url)\n if (held && held.expires > now) return held.faces\n const faces = fetch(url, {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n signal: AbortSignal.timeout(STYLESHEET_TIMEOUT_MS),\n next: { revalidate: STYLESHEET_TTL_MS / 1000 },\n } as RequestInit)\n .then(async (response) => {\n if (!response.ok) return null\n const parsed = parseGoogleFontFaces(await response.text())\n return parsed.length ? parsed : null\n })\n .catch(() => null)\n stylesheets.set(url, { expires: now + STYLESHEET_TTL_MS, faces })\n void faces.then((found) => {\n if (!found) stylesheets.set(url, { expires: now + FAILURE_TTL_MS, faces })\n })\n return faces\n}\n\n/** What a page inlines in its head for its theme's fonts. */\nexport interface SelfHostedThemeFonts {\n /** The `@font-face` rules, every `src` on the site's own origin. */\n css: string\n /** The one or two files the first screen paints with, to preload. */\n preloads: string[]\n}\n\n/**\n * The theme's Google fonts as rules to inline and files to preload, or null\n * when the theme loads none or Google could not be read — in which case the\n * caller links the stylesheet as before.\n */\nexport async function selfHostedThemeFonts(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n): Promise<SelfHostedThemeFonts | null> {\n const url = getGoogleFontsUrl(theme?.fonts)\n if (!url) return null\n const faces = await facesFor(url)\n if (!faces) return null\n return {\n css: selfHostedFontFaceCss(faces),\n preloads: selfHostedFontPreloads(theme, faces),\n }\n}\n\n/**\n * One font file, fetched from Google and answered from the site's origin with\n * a year-long `immutable` policy. `path` is the part below `/s/` on Google's\n * origin (`inter/v18/….woff2`); anything else is a 404, so this route cannot\n * be pointed at any other URL.\n *\n * The edge keeps the answer for the year too (`s-maxage`), so Google is asked\n * once per file per edge region rather than once per visitor.\n */\nexport async function serveSelfHostedFont(path: string): Promise<Response> {\n const notFound = () =>\n new Response('Not found', {\n status: 404,\n headers: { 'Cache-Control': 'public, max-age=300' },\n })\n if (!isSelfHostedFontPath(path)) return notFound()\n let upstream: Response\n try {\n upstream = await fetch(googleFontFileUrl(path), {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n cache: 'force-cache',\n })\n } catch {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n if (upstream.status === 404) return notFound()\n const length = Number(upstream.headers.get('content-length') ?? 0)\n if (!upstream.ok || !upstream.body || length > MAX_FONT_BYTES) {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n 'Content-Type': 'font/woff2',\n 'Cache-Control': SELF_HOSTED_FONT_CACHE_CONTROL,\n 'X-Content-Type-Options': 'nosniff',\n // Fonts are fetched in CORS mode; a site reached on two hostnames (its\n // own domain and its platform subdomain) shares one cached answer.\n 'Access-Control-Allow-Origin': '*',\n ...(length ? { 'Content-Length': String(length) } : {}),\n },\n })\n}\n"],"names":["getGoogleFontsUrl","googleFontFileUrl","isSelfHostedFontPath","parseGoogleFontFaces","selfHostedFontFaceCss","selfHostedFontPreloads","WOFF2_USER_AGENT","STYLESHEET_TTL_MS","FAILURE_TTL_MS","STYLESHEET_TIMEOUT_MS","MAX_FONT_BYTES","SELF_HOSTED_FONT_CACHE_CONTROL","stylesheets","Map","resetSelfHostedFontsForTests","clear","facesFor","url","now","Date","held","get","expires","faces","fetch","headers","signal","AbortSignal","timeout","next","revalidate","then","response","ok","parsed","text","length","catch","set","found","selfHostedThemeFonts","theme","fonts","css","preloads","serveSelfHostedFont","path","upstream","notFound","Response","status","cache","Number","body","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/self-hosted-fonts.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 { HostTheme } from '@aglyn/shared-data-types'\nimport {\n boundedAwait,\n createSettledValueCache,\n} from '@aglyn/shared-util-http/bounded-await'\n// By path: the theme lib's barrel carries React providers a server module\n// has no use for (AGL-405).\nimport { getGoogleFontsUrl } from '@aglyn/shared-ui-theme/util/host-theme'\nimport {\n type GoogleFontFace,\n googleFontFileUrl,\n isSelfHostedFontPath,\n parseGoogleFontFaces,\n selfHostedFontFaceCss,\n selfHostedFontPreloads,\n} from '@aglyn/shared-ui-theme/util/self-hosted-fonts'\n\n/**\n * The tenant's half of self-hosted theme fonts (AGL-3485): fetching Google's\n * stylesheet for a theme, and serving the files it names from the site's own\n * origin. What is done with the stylesheet, and why, is the note on\n * `@aglyn/shared-ui-theme/util/self-hosted-fonts`.\n */\n\n/**\n * A current desktop Chrome. Google's CSS2 API picks the font FORMAT from the\n * user agent, and a server's default agent is answered with TrueType; this\n * one is answered with WOFF2, the format every browser a site supports reads.\n */\nconst WOFF2_USER_AGENT =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +\n '(KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'\n\n/** How long one process trusts a stylesheet it fetched. */\nconst STYLESHEET_TTL_MS = 24 * 60 * 60 * 1000\n\n/** How long a failed fetch is remembered before it is tried again. */\nconst FAILURE_TTL_MS = 5 * 60 * 1000\n\n/** How long a page render waits for Google before falling back. */\nconst STYLESHEET_TIMEOUT_MS = 2_500\n\n/** The largest font file the route will pass on. */\nconst MAX_FONT_BYTES = 2 * 1024 * 1024\n\n/** A font file's URL never changes its bytes: the version is in the path. */\nexport const SELF_HOSTED_FONT_CACHE_CONTROL =\n 'public, max-age=31536000, s-maxage=31536000, immutable'\n\n/**\n * What this process learned about each stylesheet: the faces, or null for a\n * failure. VALUES, never an in-flight promise — the settled-value cache\n * refuses one. A promise held across requests is awaited by renders that did\n * not create it, and one that never settles stalled every page of every site\n * on the instance until the function timed out (AGL-3565).\n */\nconst stylesheets = createSettledValueCache<string, GoogleFontFace[] | null>()\n\n/** Test seam: the process cache would otherwise leak between cases. */\nexport function resetSelfHostedFontsForTests(): void {\n stylesheets.clear()\n}\n\n/**\n * Google's faces for a stylesheet URL, or null; never waits past `ms`. The\n * deadline is a real timer (`boundedAwait`): an abort signal alone did not\n * bound Next's patched fetch on Vercel, whose promise could never settle.\n */\nfunction fetchFaces(url: string, ms: number): Promise<GoogleFontFace[] | null> {\n return boundedAwait(\n (signal) =>\n fetch(url, {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n signal,\n next: { revalidate: STYLESHEET_TTL_MS / 1000 },\n } as RequestInit)\n .then(async (response) => {\n if (!response.ok) return null\n const parsed = parseGoogleFontFaces(await response.text())\n return parsed.length ? parsed : null\n })\n .catch(() => null),\n ms,\n null,\n 'self-hosted-fonts.stylesheet',\n )\n}\n\n/**\n * Google's faces for a stylesheet URL, from this process's cache when it has\n * them. Null when the fetch failed, timed out or answered with nothing usable;\n * the page then links the stylesheet as it always did, rather than losing its\n * font. Asked through `fetch` with a day's revalidation, which on the server is\n * Next's data cache: a cold process reads the stored stylesheet rather than\n * asking Google again.\n */\nfunction facesFor(url: string): Promise<GoogleFontFace[] | null> {\n return stylesheets.readThrough(\n url,\n () => fetchFaces(url, STYLESHEET_TIMEOUT_MS),\n (faces) => (faces ? STYLESHEET_TTL_MS : FAILURE_TTL_MS),\n )\n}\n\n/** What a page inlines in its head for its theme's fonts. */\nexport interface SelfHostedThemeFonts {\n /** The `@font-face` rules, every `src` on the site's own origin. */\n css: string\n /** The one or two files the first screen paints with, to preload. */\n preloads: string[]\n}\n\n/**\n * The theme's Google fonts as rules to inline and files to preload, or null\n * when the theme loads none or Google could not be read — in which case the\n * caller links the stylesheet as before.\n */\nexport async function selfHostedThemeFonts(\n theme: Pick<HostTheme, 'fonts' | 'typography'> | undefined,\n): Promise<SelfHostedThemeFonts | null> {\n const url = getGoogleFontsUrl(theme?.fonts)\n if (!url) return null\n const faces = await facesFor(url)\n if (!faces) return null\n return {\n css: selfHostedFontFaceCss(faces),\n preloads: selfHostedFontPreloads(theme, faces),\n }\n}\n\n/**\n * One font file, fetched from Google and answered from the site's origin with\n * a year-long `immutable` policy. `path` is the part below `/s/` on Google's\n * origin (`inter/v18/….woff2`); anything else is a 404, so this route cannot\n * be pointed at any other URL.\n *\n * The edge keeps the answer for the year too (`s-maxage`), so Google is asked\n * once per file per edge region rather than once per visitor.\n */\nexport async function serveSelfHostedFont(path: string): Promise<Response> {\n const notFound = () =>\n new Response('Not found', {\n status: 404,\n headers: { 'Cache-Control': 'public, max-age=300' },\n })\n if (!isSelfHostedFontPath(path)) return notFound()\n let upstream: Response\n try {\n upstream = await fetch(googleFontFileUrl(path), {\n headers: { 'User-Agent': WOFF2_USER_AGENT },\n cache: 'force-cache',\n })\n } catch {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n if (upstream.status === 404) return notFound()\n const length = Number(upstream.headers.get('content-length') ?? 0)\n if (!upstream.ok || !upstream.body || length > MAX_FONT_BYTES) {\n return new Response('Font unavailable', {\n status: 502,\n headers: { 'Cache-Control': 'no-store' },\n })\n }\n return new Response(upstream.body, {\n status: 200,\n headers: {\n 'Content-Type': 'font/woff2',\n 'Cache-Control': SELF_HOSTED_FONT_CACHE_CONTROL,\n 'X-Content-Type-Options': 'nosniff',\n // Fonts are fetched in CORS mode; a site reached on two hostnames (its\n // own domain and its platform subdomain) shares one cached answer.\n 'Access-Control-Allow-Origin': '*',\n ...(length ? { 'Content-Length': String(length) } : {}),\n },\n })\n}\n"],"names":["boundedAwait","createSettledValueCache","getGoogleFontsUrl","googleFontFileUrl","isSelfHostedFontPath","parseGoogleFontFaces","selfHostedFontFaceCss","selfHostedFontPreloads","WOFF2_USER_AGENT","STYLESHEET_TTL_MS","FAILURE_TTL_MS","STYLESHEET_TIMEOUT_MS","MAX_FONT_BYTES","SELF_HOSTED_FONT_CACHE_CONTROL","stylesheets","resetSelfHostedFontsForTests","clear","fetchFaces","url","ms","signal","fetch","headers","next","revalidate","then","response","ok","parsed","text","length","catch","facesFor","readThrough","faces","selfHostedThemeFonts","theme","fonts","css","preloads","serveSelfHostedFont","path","upstream","notFound","Response","status","cache","Number","get","body","String"],"mappings":"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"}