@aglyn/aglyn 1.0.0-beta.147 → 1.0.0-beta.150
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 +10 -10
- package/src/lib/app-utils/binding-token-catalog.js +9 -2
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +52 -3
- package/src/lib/app-utils/collection-entries.js +98 -9
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -34
- package/src/lib/app-utils/console-routes.js +29 -11
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contexts.d.ts +1 -0
- package/src/lib/app-utils/contexts.js +1 -0
- package/src/lib/app-utils/contexts.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +13 -1
- package/src/lib/app-utils/docs-help.generated.js +37 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-src.d.ts +93 -0
- package/src/lib/app-utils/media-src.js +114 -0
- package/src/lib/app-utils/media-src.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +136 -2
- package/src/lib/app-utils/notifications.js +139 -3
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/page-header-help-context.d.ts +83 -0
- package/src/lib/app-utils/page-header-help-context.js +56 -0
- package/src/lib/app-utils/page-header-help-context.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +5 -4
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
- package/src/lib/foundation/definitions/host-content-collections.js +47 -109
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +121 -22
- package/src/lib/plugin-manager/feature-plugins.js +41 -18
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
- package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-publish.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-media-publish.js +118 -0
- package/src/lib/plugin-manager/plugin-media-publish.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
- package/src/lib/plugin-manager/plugin-route-metadata.d.ts +141 -0
- package/src/lib/plugin-manager/plugin-route-metadata.js +140 -0
- package/src/lib/plugin-manager/plugin-route-metadata.js.map +1 -0
- package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
- package/src/lib/plugin-manager/record-captured-contact.js +128 -0
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* What a plugin's page says about itself in the `<head>` (AGL-3080).
|
|
18
|
+
*
|
|
19
|
+
* A console page that a plugin draws has its head built by the SHELL: the
|
|
20
|
+
* page is a client component and cannot export metadata, so the nearest
|
|
21
|
+
* server layout does it, and for every plugin route that layout is the
|
|
22
|
+
* generic one. It can title a page from the slugs in the URL and nothing
|
|
23
|
+
* more — which is correct for a hub and wrong for a record. A marketplace
|
|
24
|
+
* listing shared into Slack unfurled as the same generic console card as
|
|
25
|
+
* every other listing until the app grew a hand-written route with a layout
|
|
26
|
+
* of its own, and that route is one of the ~20 that cannot move onto the
|
|
27
|
+
* generic one while this is the only way to have a card.
|
|
28
|
+
*
|
|
29
|
+
* So the plugin declares how to answer for an address under its own route,
|
|
30
|
+
* and the shell asks.
|
|
31
|
+
*
|
|
32
|
+
* ## The head is not a framework's
|
|
33
|
+
*
|
|
34
|
+
* A declaration answers a {@link PluginRouteHead} — a title, a description,
|
|
35
|
+
* an image — and never Next's `Metadata`. Core does not depend on a web
|
|
36
|
+
* framework and a plugin must not have to; more usefully, how a title,
|
|
37
|
+
* description and image become `openGraph` and `twitter` tags is the SHELL's
|
|
38
|
+
* knowledge and the same for every route: which card size degrades without
|
|
39
|
+
* an image, that defining `openGraph` replaces the root layout's wholesale
|
|
40
|
+
* so the site name has to be restated. A plugin that wrote those tags itself
|
|
41
|
+
* would get a different answer per plugin, and the two that got it wrong
|
|
42
|
+
* would look exactly like the two that got it right until somebody shared a
|
|
43
|
+
* link.
|
|
44
|
+
*
|
|
45
|
+
* ## Absent is a real answer, and so is silence
|
|
46
|
+
*
|
|
47
|
+
* `resolve` answers `null` for an address it has nothing to add about — a
|
|
48
|
+
* hub, a tab, a record it must not describe — and the shell keeps the title
|
|
49
|
+
* it would have built. That is the same answer as a plugin that declares
|
|
50
|
+
* nothing at all, deliberately: a head is an enrichment, and a route with no
|
|
51
|
+
* declaration is the ordinary case rather than a degraded one.
|
|
52
|
+
*
|
|
53
|
+
* ⛔ WHICH MAKES THE LOADING ORDER THE WHOLE RISK. This is a runtime
|
|
54
|
+
* registry, and an unfilled one answers "nothing to add" for every address —
|
|
55
|
+
* silently, and indistinguishably from the honest answer. A layout that asked
|
|
56
|
+
* before loading the plugin's server surface would drop every card it was
|
|
57
|
+
* built to produce and stay green. A registry is nonetheless the right shape
|
|
58
|
+
* here, and a compiled table is not: the answer depends on the DOCUMENT
|
|
59
|
+
* behind the address, so it cannot be compiled, and the reader is a server
|
|
60
|
+
* layout that already awaits — so it can load the surface first, which is
|
|
61
|
+
* what `pluginRouteHead` in the console does and what the app's
|
|
62
|
+
* `*-is-registered` spec holds it to.
|
|
63
|
+
*
|
|
64
|
+
* ## One route, one owner
|
|
65
|
+
*
|
|
66
|
+
* A route belongs to the plugin that serves it, so a second plugin's
|
|
67
|
+
* declaration for the same route is refused naming both and the incumbent
|
|
68
|
+
* keeps answering. The same plugin declaring again — a hot reload, a second
|
|
69
|
+
* surface — replaces its own.
|
|
70
|
+
*
|
|
71
|
+
* Server-side: reached by its own subpath, never through
|
|
72
|
+
* `plugin-manager/index.ts`. What a published page does not need, it must
|
|
73
|
+
* not import.
|
|
74
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
75
|
+
import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
76
|
+
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
77
|
+
export const PLUGIN_ROUTE_METADATA = definePluginServiceContract('core.route-metadata', {
|
|
78
|
+
multiple: true
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* Declares how a plugin answers for addresses under one of its routes. The
|
|
82
|
+
* owner is the loader's marker when a register fn is running, else
|
|
83
|
+
* `options.pluginId`.
|
|
84
|
+
*/ export function registerPluginRouteMetadata(declaration, options) {
|
|
85
|
+
var _ref, _ref1, _getRegisteringPluginId;
|
|
86
|
+
var _declaration_route;
|
|
87
|
+
const key = (_ref = (_declaration_route = declaration.route) == null ? void 0 : _declaration_route.trim().replace(/^\/+/, '')) != null ? _ref : '';
|
|
88
|
+
if (!key) throw new Error('a plugin route metadata declaration needs a route');
|
|
89
|
+
if (typeof declaration.resolve !== 'function') {
|
|
90
|
+
throw new Error(`route metadata for "${key}" needs a resolve function`);
|
|
91
|
+
}
|
|
92
|
+
const pluginId = ((_ref1 = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref1 : '').trim();
|
|
93
|
+
const incumbent = resolvePluginServices(PLUGIN_ROUTE_METADATA).find((entry)=>entry.key === key);
|
|
94
|
+
if (incumbent && pluginId && incumbent.pluginId !== pluginId) {
|
|
95
|
+
throw new Error(`route metadata for "${key}" is already declared by ` + `"${incumbent.pluginId}"; refused "${pluginId}"`);
|
|
96
|
+
}
|
|
97
|
+
registerPluginService(PLUGIN_ROUTE_METADATA, _extends({}, declaration, {
|
|
98
|
+
route: key
|
|
99
|
+
}), _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
100
|
+
pluginId: options.pluginId
|
|
101
|
+
} : {}, {
|
|
102
|
+
key
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** The declaration for one route, or `null` when nobody declared one. */ export function pluginRouteMetadata(route) {
|
|
106
|
+
const key = String(route != null ? route : '').trim().replace(/^\/+/, '');
|
|
107
|
+
const entry = resolvePluginServices(PLUGIN_ROUTE_METADATA).find((one)=>one.key === key);
|
|
108
|
+
return entry ? _extends({}, entry.impl, {
|
|
109
|
+
pluginId: entry.pluginId
|
|
110
|
+
}) : null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What one address says about itself, or `null` when nothing does.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ A DECLARATION THAT THROWS COSTS THE PAGE NOTHING. The head is an
|
|
116
|
+
* enrichment of a page that renders without it, and a layout's metadata
|
|
117
|
+
* rejecting fails the route — so the failure is logged and the shell falls
|
|
118
|
+
* back to the title it built itself.
|
|
119
|
+
*
|
|
120
|
+
* ⛔ It asks the registry as it stands. A caller that has not loaded the
|
|
121
|
+
* plugins' server surfaces gets `null` for every address, which is the same
|
|
122
|
+
* answer as a route nobody declared — see the module docblock.
|
|
123
|
+
*/ export async function resolvePluginRouteHead(route, segments) {
|
|
124
|
+
const declared = pluginRouteMetadata(route);
|
|
125
|
+
if (!declared) return null;
|
|
126
|
+
try {
|
|
127
|
+
var _ref;
|
|
128
|
+
return (_ref = await declared.resolve(segments)) != null ? _ref : null;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`[route-metadata] "${declared.pluginId}" failed to answer for ` + `${route}/${segments.join('/')}`, error);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Every declared route, with its plugin, in registration order. */ export function listPluginRouteMetadata() {
|
|
135
|
+
return resolvePluginServices(PLUGIN_ROUTE_METADATA).map((entry)=>_extends({}, entry.impl, {
|
|
136
|
+
pluginId: entry.pluginId
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=plugin-route-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-route-metadata.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 * What a plugin's page says about itself in the `<head>` (AGL-3080).\n *\n * A console page that a plugin draws has its head built by the SHELL: the\n * page is a client component and cannot export metadata, so the nearest\n * server layout does it, and for every plugin route that layout is the\n * generic one. It can title a page from the slugs in the URL and nothing\n * more — which is correct for a hub and wrong for a record. A marketplace\n * listing shared into Slack unfurled as the same generic console card as\n * every other listing until the app grew a hand-written route with a layout\n * of its own, and that route is one of the ~20 that cannot move onto the\n * generic one while this is the only way to have a card.\n *\n * So the plugin declares how to answer for an address under its own route,\n * and the shell asks.\n *\n * ## The head is not a framework's\n *\n * A declaration answers a {@link PluginRouteHead} — a title, a description,\n * an image — and never Next's `Metadata`. Core does not depend on a web\n * framework and a plugin must not have to; more usefully, how a title,\n * description and image become `openGraph` and `twitter` tags is the SHELL's\n * knowledge and the same for every route: which card size degrades without\n * an image, that defining `openGraph` replaces the root layout's wholesale\n * so the site name has to be restated. A plugin that wrote those tags itself\n * would get a different answer per plugin, and the two that got it wrong\n * would look exactly like the two that got it right until somebody shared a\n * link.\n *\n * ## Absent is a real answer, and so is silence\n *\n * `resolve` answers `null` for an address it has nothing to add about — a\n * hub, a tab, a record it must not describe — and the shell keeps the title\n * it would have built. That is the same answer as a plugin that declares\n * nothing at all, deliberately: a head is an enrichment, and a route with no\n * declaration is the ordinary case rather than a degraded one.\n *\n * ⛔ WHICH MAKES THE LOADING ORDER THE WHOLE RISK. This is a runtime\n * registry, and an unfilled one answers \"nothing to add\" for every address —\n * silently, and indistinguishably from the honest answer. A layout that asked\n * before loading the plugin's server surface would drop every card it was\n * built to produce and stay green. A registry is nonetheless the right shape\n * here, and a compiled table is not: the answer depends on the DOCUMENT\n * behind the address, so it cannot be compiled, and the reader is a server\n * layout that already awaits — so it can load the surface first, which is\n * what `pluginRouteHead` in the console does and what the app's\n * `*-is-registered` spec holds it to.\n *\n * ## One route, one owner\n *\n * A route belongs to the plugin that serves it, so a second plugin's\n * declaration for the same route is refused naming both and the incumbent\n * keeps answering. The same plugin declaring again — a hot reload, a second\n * surface — replaces its own.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`. What a published page does not need, it must\n * not import.\n */\n\nimport type { ResolvedSocialImage } from '../app-utils/social-image'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/** What one address under a plugin's route says about itself. */\nexport interface PluginRouteHead {\n /**\n * The page's own title, before the shell's scope and brand are affixed.\n * Absent leaves the title the shell built from the URL.\n */\n title?: string\n /** One or two sentences. Already cut to whatever budget the owner keeps. */\n description?: string\n /**\n * The card image, already resolved and absolute —\n * `resolveSocialImage` answers exactly this shape. A crawler reads it out\n * of band, so a relative URL is not a card, it is a broken one.\n */\n image?: ResolvedSocialImage\n}\n\nexport interface PluginRouteMetadataDeclaration {\n /**\n * The plugin's own console route, as its `contributes.console.routes`\n * spells it and without the leading slash: `marketplace`.\n */\n route: string\n /**\n * Answers for one address under that route. The segments are what follows\n * the route itself — `['listing-1']` for `/acme/marketplace/listing-1` —\n * so a declaration reads the address it understands and answers `null` for\n * every other, including the route's own hub.\n *\n * ⚠️ IT RUNS ON THE RENDER PATH and must never throw: an unhandled\n * rejection in a layout's metadata fails the whole route, and this route's\n * job is to serve its page whether or not the card resolves. The shell\n * catches anyway, because a plugin's mistake must not cost a page.\n */\n resolve(segments: readonly string[]): Promise<PluginRouteHead | null>\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginRouteMetadata = PluginRouteMetadataDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_ROUTE_METADATA =\n definePluginServiceContract<PluginRouteMetadataDeclaration>(\n 'core.route-metadata',\n { multiple: true },\n )\n\n/**\n * Declares how a plugin answers for addresses under one of its routes. The\n * owner is the loader's marker when a register fn is running, else\n * `options.pluginId`.\n */\nexport function registerPluginRouteMetadata(\n declaration: PluginRouteMetadataDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.route?.trim().replace(/^\\/+/, '') ?? ''\n if (!key) throw new Error('a plugin route metadata declaration needs a route')\n if (typeof declaration.resolve !== 'function') {\n throw new Error(`route metadata for \"${key}\" needs a resolve function`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_ROUTE_METADATA).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `route metadata for \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_ROUTE_METADATA,\n { ...declaration, route: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\n/** The declaration for one route, or `null` when nobody declared one. */\nexport function pluginRouteMetadata(\n route: string,\n): ResolvedPluginRouteMetadata | null {\n const key = String(route ?? '')\n .trim()\n .replace(/^\\/+/, '')\n const entry = resolvePluginServices(PLUGIN_ROUTE_METADATA).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n\n/**\n * What one address says about itself, or `null` when nothing does.\n *\n * ⚠️ A DECLARATION THAT THROWS COSTS THE PAGE NOTHING. The head is an\n * enrichment of a page that renders without it, and a layout's metadata\n * rejecting fails the route — so the failure is logged and the shell falls\n * back to the title it built itself.\n *\n * ⛔ It asks the registry as it stands. A caller that has not loaded the\n * plugins' server surfaces gets `null` for every address, which is the same\n * answer as a route nobody declared — see the module docblock.\n */\nexport async function resolvePluginRouteHead(\n route: string,\n segments: readonly string[],\n): Promise<PluginRouteHead | null> {\n const declared = pluginRouteMetadata(route)\n if (!declared) return null\n try {\n return (await declared.resolve(segments)) ?? null\n } catch (error) {\n console.error(\n `[route-metadata] \"${declared.pluginId}\" failed to answer for ` +\n `${route}/${segments.join('/')}`,\n error,\n )\n return null\n }\n}\n\n/** Every declared route, with its plugin, in registration order. */\nexport function listPluginRouteMetadata(): ResolvedPluginRouteMetadata[] {\n return resolvePluginServices(PLUGIN_ROUTE_METADATA).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_ROUTE_METADATA","multiple","registerPluginRouteMetadata","declaration","options","key","route","trim","replace","Error","resolve","pluginId","incumbent","find","entry","pluginRouteMetadata","String","one","impl","resolvePluginRouteHead","segments","declared","error","console","join","listPluginRouteMetadata","map"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DC;AAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4C1B,OAAO,MAAMC,wBACXH,4BACE,uBACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;CAIC,GACD,OAAO,SAASC,4BACdC,WAA2C,EAC3CC,OAA+B;cAObR,OAAAA;QALNO;IAAZ,MAAME,eAAMF,qBAAAA,YAAYG,KAAK,qBAAjBH,mBAAmBI,IAAI,GAAGC,OAAO,CAAC,QAAQ,sBAAO;IAC7D,IAAI,CAACH,KAAK,MAAM,IAAII,MAAM;IAC1B,IAAI,OAAON,YAAYO,OAAO,KAAK,YAAY;QAC7C,MAAM,IAAID,MAAM,CAAC,oBAAoB,EAAEJ,IAAI,0BAA0B,CAAC;IACxE;IACA,MAAMM,WAAW,EAACf,SAAAA,0BAAAA,oCAAAA,0BAA4BQ,2BAAAA,QAASO,QAAQ,YAA7Cf,QAAiD,IAAIW,IAAI;IAC3E,MAAMK,YAAYb,sBAAsBC,uBAAuBa,IAAI,CACjE,CAACC,QAAUA,MAAMT,GAAG,KAAKA;IAE3B,IAAIO,aAAaD,YAAYC,UAAUD,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIF,MACR,CAAC,oBAAoB,EAAEJ,IAAI,yBAAyB,CAAC,GACnD,CAAC,CAAC,EAAEO,UAAUD,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAb,sBACEE,uBACA,aAAKG;QAAaG,OAAOD;QACzB,aACMD,CAAAA,2BAAAA,QAASO,QAAQ,IAAG;QAAEA,UAAUP,QAAQO,QAAQ;IAAC,IAAI,CAAC;QAC1DN;;AAGN;AAEA,uEAAuE,GACvE,OAAO,SAASU,oBACdT,KAAa;IAEb,MAAMD,MAAMW,OAAOV,gBAAAA,QAAS,IACzBC,IAAI,GACJC,OAAO,CAAC,QAAQ;IACnB,MAAMM,QAAQf,sBAAsBC,uBAAuBa,IAAI,CAC7D,CAACI,MAAQA,IAAIZ,GAAG,KAAKA;IAEvB,OAAOS,QAAQ,aAAKA,MAAMI,IAAI;QAAEP,UAAUG,MAAMH,QAAQ;SAAK;AAC/D;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,eAAeQ,uBACpBb,KAAa,EACbc,QAA2B;IAE3B,MAAMC,WAAWN,oBAAoBT;IACrC,IAAI,CAACe,UAAU,OAAO;IACtB,IAAI;YACM;QAAR,QAAQ,OAAA,MAAMA,SAASX,OAAO,CAACU,qBAAvB,OAAqC;IAC/C,EAAE,OAAOE,OAAO;QACdC,QAAQD,KAAK,CACX,CAAC,kBAAkB,EAAED,SAASV,QAAQ,CAAC,uBAAuB,CAAC,GAC7D,GAAGL,MAAM,CAAC,EAAEc,SAASI,IAAI,CAAC,MAAM,EAClCF;QAEF,OAAO;IACT;AACF;AAEA,kEAAkE,GAClE,OAAO,SAASG;IACd,OAAO1B,sBAAsBC,uBAAuB0B,GAAG,CAAC,CAACZ,QAAW,aAC/DA,MAAMI,IAAI;YACbP,UAAUG,MAAMH,QAAQ;;AAE5B"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* How a door hands somebody it just met to whichever plugin keeps people
|
|
19
|
+
* (AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* `capturePluginContact` is the contract; this is the three things every
|
|
22
|
+
* door calling it has to get right, in one place so no door has to remember
|
|
23
|
+
* them. It lives in core rather than in an app because the doors are not all
|
|
24
|
+
* in one place: a form submission is a core route in the tenant app, a
|
|
25
|
+
* newsletter signup and a membership registration are commerce's, a booking
|
|
26
|
+
* request is bookings'. A plugin may not import an app, so a wrapper that
|
|
27
|
+
* lived in one would be a wrapper the plugin doors had to copy — and a copy
|
|
28
|
+
* is where the two halves of this behaviour would drift.
|
|
29
|
+
*
|
|
30
|
+
* ## It makes sure somebody is listening, without slowing down the case where
|
|
31
|
+
* somebody already is
|
|
32
|
+
*
|
|
33
|
+
* `capturePluginContact` answers `null` when no plugin keeps people, and the
|
|
34
|
+
* plugin that does registers itself from the server declarations each app
|
|
35
|
+
* runs at boot. That boot step is wrapped in a `catch` — a declaration that
|
|
36
|
+
* fails to load must not cost every route — so a boot that went wrong leaves
|
|
37
|
+
* a process where every capture answers `null` and no contact is ever
|
|
38
|
+
* written, with one line in a log nobody is reading.
|
|
39
|
+
*
|
|
40
|
+
* So the writer is looked up FIRST, synchronously, and handed the capture
|
|
41
|
+
* with no await in between. That matters for more than speed: every caller
|
|
42
|
+
* is fire-and-forget, and a door that awaited anything before reaching the
|
|
43
|
+
* writer would hand the capture to a request that may already have returned.
|
|
44
|
+
* Only when no writer answers does this run the app's boot step and try once
|
|
45
|
+
* more — repairing the broken-boot case rather than just reporting it.
|
|
46
|
+
*
|
|
47
|
+
* The boot step itself is the app's, because the list of plugins to declare
|
|
48
|
+
* is generated per app and core may not import a plugin. The app hands it
|
|
49
|
+
* over at boot with {@link registerPluginDeclarationsRepair}. An app that has
|
|
50
|
+
* not — or a process that never ran its instrumentation at all — loses the
|
|
51
|
+
* repair and keeps every other behaviour here, which is the honest outcome:
|
|
52
|
+
* a process that ran no boot step has bigger silences than this one.
|
|
53
|
+
*
|
|
54
|
+
* ## It tells the difference between the two silences
|
|
55
|
+
*
|
|
56
|
+
* `null` is "this workspace has no record system", which is a real and quiet
|
|
57
|
+
* answer for a workspace with the record plugin switched off. It is ALSO what
|
|
58
|
+
* a broken registration looks like, and the two are indistinguishable from
|
|
59
|
+
* here. So it is logged rather than ignored: a workspace that should be
|
|
60
|
+
* keeping contacts and has silently stopped is the failure mode this seam can
|
|
61
|
+
* produce, and a line naming the site is what turns an unreproducible support
|
|
62
|
+
* ticket into a one-minute answer. A refusal is different again — the owner
|
|
63
|
+
* considered this person and declined, for a reason it states — and is logged
|
|
64
|
+
* as itself.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has
|
|
67
|
+
* already done the thing it is recording; losing the submission, the signup
|
|
68
|
+
* or the booking over the contact would be the larger failure.
|
|
69
|
+
*
|
|
70
|
+
* Server-side: reached by its own subpath, never through
|
|
71
|
+
* `plugin-manager/index.ts`.
|
|
72
|
+
*/
|
|
73
|
+
import { type PluginContactCaptureRequest, type PluginContactCaptured } from './plugin-contact-capture';
|
|
74
|
+
/**
|
|
75
|
+
* The app offers its boot step, so a capture that finds nobody can run it.
|
|
76
|
+
*
|
|
77
|
+
* Called at boot from the app's own instrumentation, beside the declarations
|
|
78
|
+
* themselves and BEFORE them — this is a plain assignment that cannot fail,
|
|
79
|
+
* and the failure it exists to repair is the one immediately after it. The
|
|
80
|
+
* app passes a function rather than core importing the generated manifest,
|
|
81
|
+
* which core may not do: the manifest names every plugin.
|
|
82
|
+
*
|
|
83
|
+
* Idempotent and last-one-wins; an app registers exactly once per process.
|
|
84
|
+
*/
|
|
85
|
+
export declare function registerPluginDeclarationsRepair(run: () => Promise<void>): void;
|
|
86
|
+
/** Only for specs: forgets the registered boot step. */
|
|
87
|
+
export declare function resetPluginDeclarationsRepairForTests(): void;
|
|
88
|
+
export declare function recordCapturedContact(request: PluginContactCaptureRequest): Promise<PluginContactCaptured | null>;
|
|
89
|
+
export default recordCapturedContact;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* How a door hands somebody it just met to whichever plugin keeps people
|
|
18
|
+
* (AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* `capturePluginContact` is the contract; this is the three things every
|
|
21
|
+
* door calling it has to get right, in one place so no door has to remember
|
|
22
|
+
* them. It lives in core rather than in an app because the doors are not all
|
|
23
|
+
* in one place: a form submission is a core route in the tenant app, a
|
|
24
|
+
* newsletter signup and a membership registration are commerce's, a booking
|
|
25
|
+
* request is bookings'. A plugin may not import an app, so a wrapper that
|
|
26
|
+
* lived in one would be a wrapper the plugin doors had to copy — and a copy
|
|
27
|
+
* is where the two halves of this behaviour would drift.
|
|
28
|
+
*
|
|
29
|
+
* ## It makes sure somebody is listening, without slowing down the case where
|
|
30
|
+
* somebody already is
|
|
31
|
+
*
|
|
32
|
+
* `capturePluginContact` answers `null` when no plugin keeps people, and the
|
|
33
|
+
* plugin that does registers itself from the server declarations each app
|
|
34
|
+
* runs at boot. That boot step is wrapped in a `catch` — a declaration that
|
|
35
|
+
* fails to load must not cost every route — so a boot that went wrong leaves
|
|
36
|
+
* a process where every capture answers `null` and no contact is ever
|
|
37
|
+
* written, with one line in a log nobody is reading.
|
|
38
|
+
*
|
|
39
|
+
* So the writer is looked up FIRST, synchronously, and handed the capture
|
|
40
|
+
* with no await in between. That matters for more than speed: every caller
|
|
41
|
+
* is fire-and-forget, and a door that awaited anything before reaching the
|
|
42
|
+
* writer would hand the capture to a request that may already have returned.
|
|
43
|
+
* Only when no writer answers does this run the app's boot step and try once
|
|
44
|
+
* more — repairing the broken-boot case rather than just reporting it.
|
|
45
|
+
*
|
|
46
|
+
* The boot step itself is the app's, because the list of plugins to declare
|
|
47
|
+
* is generated per app and core may not import a plugin. The app hands it
|
|
48
|
+
* over at boot with {@link registerPluginDeclarationsRepair}. An app that has
|
|
49
|
+
* not — or a process that never ran its instrumentation at all — loses the
|
|
50
|
+
* repair and keeps every other behaviour here, which is the honest outcome:
|
|
51
|
+
* a process that ran no boot step has bigger silences than this one.
|
|
52
|
+
*
|
|
53
|
+
* ## It tells the difference between the two silences
|
|
54
|
+
*
|
|
55
|
+
* `null` is "this workspace has no record system", which is a real and quiet
|
|
56
|
+
* answer for a workspace with the record plugin switched off. It is ALSO what
|
|
57
|
+
* a broken registration looks like, and the two are indistinguishable from
|
|
58
|
+
* here. So it is logged rather than ignored: a workspace that should be
|
|
59
|
+
* keeping contacts and has silently stopped is the failure mode this seam can
|
|
60
|
+
* produce, and a line naming the site is what turns an unreproducible support
|
|
61
|
+
* ticket into a one-minute answer. A refusal is different again — the owner
|
|
62
|
+
* considered this person and declined, for a reason it states — and is logged
|
|
63
|
+
* as itself.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has
|
|
66
|
+
* already done the thing it is recording; losing the submission, the signup
|
|
67
|
+
* or the booking over the contact would be the larger failure.
|
|
68
|
+
*
|
|
69
|
+
* Server-side: reached by its own subpath, never through
|
|
70
|
+
* `plugin-manager/index.ts`.
|
|
71
|
+
*/ import { pluginContactCaptureWriter } from "./plugin-contact-capture.js";
|
|
72
|
+
/** The app's boot-time declarations step, when it has offered one. */ let repair = null;
|
|
73
|
+
/**
|
|
74
|
+
* The app offers its boot step, so a capture that finds nobody can run it.
|
|
75
|
+
*
|
|
76
|
+
* Called at boot from the app's own instrumentation, beside the declarations
|
|
77
|
+
* themselves and BEFORE them — this is a plain assignment that cannot fail,
|
|
78
|
+
* and the failure it exists to repair is the one immediately after it. The
|
|
79
|
+
* app passes a function rather than core importing the generated manifest,
|
|
80
|
+
* which core may not do: the manifest names every plugin.
|
|
81
|
+
*
|
|
82
|
+
* Idempotent and last-one-wins; an app registers exactly once per process.
|
|
83
|
+
*/ export function registerPluginDeclarationsRepair(run) {
|
|
84
|
+
repair = run;
|
|
85
|
+
}
|
|
86
|
+
/** Only for specs: forgets the registered boot step. */ export function resetPluginDeclarationsRepairForTests() {
|
|
87
|
+
repair = null;
|
|
88
|
+
}
|
|
89
|
+
export async function recordCapturedContact(request) {
|
|
90
|
+
const where = `${request.interaction.source} on ${request.hostId}`;
|
|
91
|
+
const found = pluginContactCaptureWriter();
|
|
92
|
+
if (found) return await handedTo(found.writer, request, where);
|
|
93
|
+
// Nobody answers. Either this workspace keeps no records, or the boot step
|
|
94
|
+
// that registers the writer failed and every capture in this process has
|
|
95
|
+
// been going nowhere. Run it and ask again — the registration memoizes its
|
|
96
|
+
// own promise, so this is one attempt per process and not one per capture.
|
|
97
|
+
if (repair) {
|
|
98
|
+
try {
|
|
99
|
+
await repair();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(`[capture] plugin declarations failed before ${where}`, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const late = pluginContactCaptureWriter();
|
|
105
|
+
if (!late) {
|
|
106
|
+
console.warn(`[capture] no plugin keeps people, so the ${where} recorded nobody. ` + 'Expected when the record plugin is off for this workspace; a bug otherwise.');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
console.warn(`[capture] the ${where} found no writer until it registered one itself — ` + 'the boot-time declarations did not run in this process.');
|
|
110
|
+
return await handedTo(late.writer, request, where);
|
|
111
|
+
}
|
|
112
|
+
/** One capture, with the two failures a door must not be made to handle. */ async function handedTo(writer, request, where) {
|
|
113
|
+
try {
|
|
114
|
+
const verdict = await writer.capture(request);
|
|
115
|
+
if (verdict.ok === false) {
|
|
116
|
+
console.warn(`[capture] ${where} was refused (${verdict.reason})`);
|
|
117
|
+
}
|
|
118
|
+
return verdict;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// The contract says a writer RETURNS its refusals, so reaching here means
|
|
121
|
+
// one broke its own contract. It still costs the door nothing.
|
|
122
|
+
console.error(`[capture] ${where} threw`, error);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export default recordCapturedContact;
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=record-captured-contact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/record-captured-contact.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 * How a door hands somebody it just met to whichever plugin keeps people\n * (AGL-3080).\n *\n * `capturePluginContact` is the contract; this is the three things every\n * door calling it has to get right, in one place so no door has to remember\n * them. It lives in core rather than in an app because the doors are not all\n * in one place: a form submission is a core route in the tenant app, a\n * newsletter signup and a membership registration are commerce's, a booking\n * request is bookings'. A plugin may not import an app, so a wrapper that\n * lived in one would be a wrapper the plugin doors had to copy — and a copy\n * is where the two halves of this behaviour would drift.\n *\n * ## It makes sure somebody is listening, without slowing down the case where\n * somebody already is\n *\n * `capturePluginContact` answers `null` when no plugin keeps people, and the\n * plugin that does registers itself from the server declarations each app\n * runs at boot. That boot step is wrapped in a `catch` — a declaration that\n * fails to load must not cost every route — so a boot that went wrong leaves\n * a process where every capture answers `null` and no contact is ever\n * written, with one line in a log nobody is reading.\n *\n * So the writer is looked up FIRST, synchronously, and handed the capture\n * with no await in between. That matters for more than speed: every caller\n * is fire-and-forget, and a door that awaited anything before reaching the\n * writer would hand the capture to a request that may already have returned.\n * Only when no writer answers does this run the app's boot step and try once\n * more — repairing the broken-boot case rather than just reporting it.\n *\n * The boot step itself is the app's, because the list of plugins to declare\n * is generated per app and core may not import a plugin. The app hands it\n * over at boot with {@link registerPluginDeclarationsRepair}. An app that has\n * not — or a process that never ran its instrumentation at all — loses the\n * repair and keeps every other behaviour here, which is the honest outcome:\n * a process that ran no boot step has bigger silences than this one.\n *\n * ## It tells the difference between the two silences\n *\n * `null` is \"this workspace has no record system\", which is a real and quiet\n * answer for a workspace with the record plugin switched off. It is ALSO what\n * a broken registration looks like, and the two are indistinguishable from\n * here. So it is logged rather than ignored: a workspace that should be\n * keeping contacts and has silently stopped is the failure mode this seam can\n * produce, and a line naming the site is what turns an unreproducible support\n * ticket into a one-minute answer. A refusal is different again — the owner\n * considered this person and declined, for a reason it states — and is logged\n * as itself.\n *\n * ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has\n * already done the thing it is recording; losing the submission, the signup\n * or the booking over the contact would be the larger failure.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\nimport {\n pluginContactCaptureWriter,\n type PluginContactCaptureRequest,\n type PluginContactCaptureWriter,\n type PluginContactCaptured,\n} from './plugin-contact-capture'\n\n/** The app's boot-time declarations step, when it has offered one. */\nlet repair: (() => Promise<void>) | null = null\n\n/**\n * The app offers its boot step, so a capture that finds nobody can run it.\n *\n * Called at boot from the app's own instrumentation, beside the declarations\n * themselves and BEFORE them — this is a plain assignment that cannot fail,\n * and the failure it exists to repair is the one immediately after it. The\n * app passes a function rather than core importing the generated manifest,\n * which core may not do: the manifest names every plugin.\n *\n * Idempotent and last-one-wins; an app registers exactly once per process.\n */\nexport function registerPluginDeclarationsRepair(run: () => Promise<void>): void {\n repair = run\n}\n\n/** Only for specs: forgets the registered boot step. */\nexport function resetPluginDeclarationsRepairForTests(): void {\n repair = null\n}\n\nexport async function recordCapturedContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const where = `${request.interaction.source} on ${request.hostId}`\n const found = pluginContactCaptureWriter()\n if (found) return await handedTo(found.writer, request, where)\n\n // Nobody answers. Either this workspace keeps no records, or the boot step\n // that registers the writer failed and every capture in this process has\n // been going nowhere. Run it and ask again — the registration memoizes its\n // own promise, so this is one attempt per process and not one per capture.\n if (repair) {\n try {\n await repair()\n } catch (error) {\n console.error(`[capture] plugin declarations failed before ${where}`, error)\n }\n }\n const late = pluginContactCaptureWriter()\n if (!late) {\n console.warn(\n `[capture] no plugin keeps people, so the ${where} recorded nobody. ` +\n 'Expected when the record plugin is off for this workspace; a bug otherwise.',\n )\n return null\n }\n console.warn(\n `[capture] the ${where} found no writer until it registered one itself — ` +\n 'the boot-time declarations did not run in this process.',\n )\n return await handedTo(late.writer, request, where)\n}\n\n/** One capture, with the two failures a door must not be made to handle. */\nasync function handedTo(\n writer: PluginContactCaptureWriter,\n request: PluginContactCaptureRequest,\n where: string,\n): Promise<PluginContactCaptured | null> {\n try {\n const verdict = await writer.capture(request)\n if (verdict.ok === false) {\n console.warn(`[capture] ${where} was refused (${verdict.reason})`)\n }\n return verdict\n } catch (error) {\n // The contract says a writer RETURNS its refusals, so reaching here means\n // one broke its own contract. It still costs the door nothing.\n console.error(`[capture] ${where} threw`, error)\n return null\n }\n}\n\nexport default recordCapturedContact\n"],"names":["pluginContactCaptureWriter","repair","registerPluginDeclarationsRepair","run","resetPluginDeclarationsRepairForTests","recordCapturedContact","request","where","interaction","source","hostId","found","handedTo","writer","error","console","late","warn","verdict","capture","ok","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDC,GAED,SACEA,0BAA0B,QAIrB,8BAA0B;AAEjC,oEAAoE,GACpE,IAAIC,SAAuC;AAE3C;;;;;;;;;;CAUC,GACD,OAAO,SAASC,iCAAiCC,GAAwB;IACvEF,SAASE;AACX;AAEA,sDAAsD,GACtD,OAAO,SAASC;IACdH,SAAS;AACX;AAEA,OAAO,eAAeI,sBACpBC,OAAoC;IAEpC,MAAMC,QAAQ,GAAGD,QAAQE,WAAW,CAACC,MAAM,CAAC,IAAI,EAAEH,QAAQI,MAAM,EAAE;IAClE,MAAMC,QAAQX;IACd,IAAIW,OAAO,OAAO,MAAMC,SAASD,MAAME,MAAM,EAAEP,SAASC;IAExD,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAIN,QAAQ;QACV,IAAI;YACF,MAAMA;QACR,EAAE,OAAOa,OAAO;YACdC,QAAQD,KAAK,CAAC,CAAC,4CAA4C,EAAEP,OAAO,EAAEO;QACxE;IACF;IACA,MAAME,OAAOhB;IACb,IAAI,CAACgB,MAAM;QACTD,QAAQE,IAAI,CACV,CAAC,yCAAyC,EAAEV,MAAM,kBAAkB,CAAC,GACnE;QAEJ,OAAO;IACT;IACAQ,QAAQE,IAAI,CACV,CAAC,cAAc,EAAEV,MAAM,kDAAkD,CAAC,GACxE;IAEJ,OAAO,MAAMK,SAASI,KAAKH,MAAM,EAAEP,SAASC;AAC9C;AAEA,0EAA0E,GAC1E,eAAeK,SACbC,MAAkC,EAClCP,OAAoC,EACpCC,KAAa;IAEb,IAAI;QACF,MAAMW,UAAU,MAAML,OAAOM,OAAO,CAACb;QACrC,IAAIY,QAAQE,EAAE,KAAK,OAAO;YACxBL,QAAQE,IAAI,CAAC,CAAC,UAAU,EAAEV,MAAM,cAAc,EAAEW,QAAQG,MAAM,CAAC,CAAC,CAAC;QACnE;QACA,OAAOH;IACT,EAAE,OAAOJ,OAAO;QACd,0EAA0E;QAC1E,+DAA+D;QAC/DC,QAAQD,KAAK,CAAC,CAAC,UAAU,EAAEP,MAAM,MAAM,CAAC,EAAEO;QAC1C,OAAO;IACT;AACF;AAEA,eAAeT,sBAAqB"}
|