@assistant-ui/react-generative-ui 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/convert/boundSpec.d.ts +51 -0
  2. package/dist/convert/boundSpec.d.ts.map +1 -0
  3. package/dist/convert/boundSpec.js +108 -0
  4. package/dist/convert/boundSpec.js.map +1 -0
  5. package/dist/convert/copyBounded.d.ts +28 -0
  6. package/dist/convert/copyBounded.d.ts.map +1 -0
  7. package/dist/convert/copyBounded.js +38 -0
  8. package/dist/convert/copyBounded.js.map +1 -0
  9. package/dist/convert/isElement.d.ts +6 -0
  10. package/dist/convert/isElement.d.ts.map +1 -0
  11. package/dist/convert/isElement.js +6 -0
  12. package/dist/convert/isElement.js.map +1 -0
  13. package/dist/convert/takeRun.d.ts +9 -0
  14. package/dist/convert/takeRun.d.ts.map +1 -0
  15. package/dist/convert/takeRun.js +20 -0
  16. package/dist/convert/takeRun.js.map +1 -0
  17. package/dist/slack/constants.d.ts +1 -26
  18. package/dist/slack/constants.d.ts.map +1 -1
  19. package/dist/slack/constants.js +1 -26
  20. package/dist/slack/constants.js.map +1 -1
  21. package/dist/slack/fromSlackBlocks.js +6 -10
  22. package/dist/slack/fromSlackBlocks.js.map +1 -1
  23. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  24. package/dist/slack/toSlackBlocks.js +28 -95
  25. package/dist/slack/toSlackBlocks.js.map +1 -1
  26. package/dist/teams/constants.d.ts +1 -28
  27. package/dist/teams/constants.d.ts.map +1 -1
  28. package/dist/teams/constants.js +1 -32
  29. package/dist/teams/constants.js.map +1 -1
  30. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  31. package/dist/teams/toAdaptiveCard.js +12 -23
  32. package/dist/teams/toAdaptiveCard.js.map +1 -1
  33. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  34. package/dist/teams/toTeamsAttachments.js +5 -9
  35. package/dist/teams/toTeamsAttachments.js.map +1 -1
  36. package/dist/vocabulary/list.js +4 -2
  37. package/dist/vocabulary/list.js.map +1 -1
  38. package/package.json +6 -6
  39. package/src/convert/boundSpec.test.ts +192 -0
  40. package/src/convert/boundSpec.ts +134 -0
  41. package/src/convert/copyBounded.test.ts +131 -0
  42. package/src/convert/copyBounded.ts +38 -0
  43. package/src/convert/isElement.ts +6 -0
  44. package/src/convert/takeRun.test.ts +64 -0
  45. package/src/convert/takeRun.ts +19 -0
  46. package/src/slack/constants.ts +0 -29
  47. package/src/slack/fromSlackBlocks.test.ts +105 -7
  48. package/src/slack/fromSlackBlocks.ts +9 -9
  49. package/src/slack/toSlackBlocks.test.ts +82 -2
  50. package/src/slack/toSlackBlocks.ts +68 -164
  51. package/src/teams/constants.ts +0 -42
  52. package/src/teams/toAdaptiveCard.test.ts +74 -2
  53. package/src/teams/toAdaptiveCard.ts +29 -36
  54. package/src/teams/toTeamsAttachments.ts +4 -17
  55. package/src/vocabulary/list.test.tsx +35 -9
  56. package/src/vocabulary/list.tsx +4 -2
  57. package/dist/teams/boundSpec.d.ts +0 -20
  58. package/dist/teams/boundSpec.d.ts.map +0 -1
  59. package/dist/teams/boundSpec.js +0 -67
  60. package/dist/teams/boundSpec.js.map +0 -1
  61. package/src/teams/boundSpec.ts +0 -102
@@ -1,19 +1,24 @@
1
+ import {
2
+ MAX_TRAVERSAL_DEPTH,
3
+ boundSpec,
4
+ clampReasonDetail,
5
+ } from "../convert/boundSpec";
6
+ import { copyBounded } from "../convert/copyBounded";
7
+ import { isElement } from "../convert/isElement";
8
+ import { takeRun } from "../convert/takeRun";
1
9
  import {
2
10
  normalizeSpec,
3
11
  type NormalizedUIElement,
4
12
  type NormalizedUINode,
5
13
  } from "../ir";
6
- import { boundSpec } from "./boundSpec";
7
14
  import {
8
15
  CHOICE_OPTION_CAP,
9
- MAX_TRAVERSAL_DEPTH,
10
16
  PAYLOAD_SOFT_CAP,
11
17
  PRIMARY_ACTION_CAP,
12
18
  TABLE_COLUMN_CAP,
13
19
  TABLE_ROW_CAP,
14
20
  buildCard,
15
21
  buildSubmitAction,
16
- clampReasonDetail,
17
22
  utf8ByteLength,
18
23
  } from "./constants";
19
24
  import type {
@@ -44,9 +49,6 @@ export interface ConversionContext {
44
49
  const isRecord = (value: unknown): value is Record<string, unknown> =>
45
50
  typeof value === "object" && value !== null && !Array.isArray(value);
46
51
 
47
- const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
48
- isRecord(node);
49
-
50
52
  const asString = (value: unknown): string =>
51
53
  typeof value === "string" ? value : "";
52
54
 
@@ -125,7 +127,7 @@ function reservedSafeId(
125
127
  }
126
128
 
127
129
  /**
128
- * The shared bounded-iteration primitive: slices `value` to `cap` entries
130
+ * The shared bounded-iteration primitive: copies `value` to `cap` entries
129
131
  * without ever reading past that many indices, so a hostile array (sparse or
130
132
  * proxied with a fabricated `length`) cannot stall the event loop.
131
133
  */
@@ -134,7 +136,7 @@ const clampArray = (
134
136
  cap: number,
135
137
  ): { readonly items: unknown[]; readonly truncated: boolean } => {
136
138
  if (!Array.isArray(value)) return { items: [], truncated: false };
137
- return { items: value.slice(0, cap), truncated: value.length > cap };
139
+ return copyBounded(value, cap);
138
140
  };
139
141
 
140
142
  const normalizedList = (
@@ -337,12 +339,13 @@ function convertTable(
337
339
  : undefined;
338
340
  const dataRows: TeamsTableRow[] = rawRows.map((row) => ({
339
341
  type: "TableRow",
340
- cells: (Array.isArray(row) ? row.slice(0, TABLE_COLUMN_CAP) : []).map(
341
- (cell) => ({
342
- type: "TableCell" as const,
343
- items: [textBlock(stringifyCell(cell))],
344
- }),
345
- ),
342
+ cells: (Array.isArray(row)
343
+ ? copyBounded(row, TABLE_COLUMN_CAP).items
344
+ : []
345
+ ).map((cell) => ({
346
+ type: "TableCell" as const,
347
+ items: [textBlock(stringifyCell(cell))],
348
+ })),
346
349
  }));
347
350
 
348
351
  return {
@@ -729,32 +732,22 @@ export function convertSequence(
729
732
  continue;
730
733
  }
731
734
  if (isElement(current) && current.type === "Fact") {
732
- const facts: NormalizedUIElement[] = [];
733
- while (index < nodes.length) {
734
- const candidate = nodes[index];
735
- if (!candidate || !isElement(candidate) || candidate.type !== "Fact") {
736
- break;
737
- }
738
- facts.push(candidate);
739
- index += 1;
740
- }
735
+ const { run: facts, next } = takeRun(
736
+ nodes,
737
+ index,
738
+ (candidate) => candidate.type === "Fact",
739
+ );
740
+ index = next;
741
741
  emit([convertFacts(facts)]);
742
742
  continue;
743
743
  }
744
744
  if (isElement(current) && current.type === "Button") {
745
- const buttons: NormalizedUIElement[] = [];
746
- while (index < nodes.length) {
747
- const candidate = nodes[index];
748
- if (
749
- !candidate ||
750
- !isElement(candidate) ||
751
- candidate.type !== "Button"
752
- ) {
753
- break;
754
- }
755
- buttons.push(candidate);
756
- index += 1;
757
- }
745
+ const { run: buttons, next } = takeRun(
746
+ nodes,
747
+ index,
748
+ (candidate) => candidate.type === "Button",
749
+ );
750
+ index = next;
758
751
  emit([convertButtons(buttons, context)]);
759
752
  continue;
760
753
  }
@@ -1,14 +1,15 @@
1
+ import { boundSpec, clampReasonDetail } from "../convert/boundSpec";
2
+ import { copyBounded } from "../convert/copyBounded";
3
+ import { isElement } from "../convert/isElement";
1
4
  import {
2
5
  normalizeSpec,
3
6
  type NormalizedUIElement,
4
7
  type NormalizedUINode,
5
8
  } from "../ir";
6
- import { boundSpec } from "./boundSpec";
7
9
  import {
8
10
  CAROUSEL_ATTACHMENT_CAP,
9
11
  PAYLOAD_SOFT_CAP,
10
12
  buildAttachment,
11
- clampReasonDetail,
12
13
  utf8ByteLength,
13
14
  } from "./constants";
14
15
  import {
@@ -23,12 +24,6 @@ import type {
23
24
  ToAdaptiveCardOptions,
24
25
  } from "./types";
25
26
 
26
- const isRecord = (value: unknown): value is Record<string, unknown> =>
27
- typeof value === "object" && value !== null && !Array.isArray(value);
28
-
29
- const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
30
- isRecord(node);
31
-
32
27
  const normalizedList = (
33
28
  node: NormalizedUINode | undefined,
34
29
  ): NormalizedUINode[] => {
@@ -37,14 +32,6 @@ const normalizedList = (
37
32
  return node.flatMap((child) => normalizedList(child));
38
33
  };
39
34
 
40
- const clampArray = <T>(
41
- value: readonly T[],
42
- cap: number,
43
- ): { readonly items: T[]; readonly truncated: boolean } => ({
44
- items: value.slice(0, cap),
45
- truncated: value.length > cap,
46
- });
47
-
48
35
  /**
49
36
  * Recognizes a root that is a single `Carousel` element, returning its `Card`
50
37
  * children. A non-card child is routed through {@link discardedChild}, which
@@ -102,7 +89,7 @@ export function toTeamsAttachments(
102
89
  const { root } = normalizeSpec(bounded as never);
103
90
  const carouselCards = rootCarouselCards(root, context);
104
91
  if (carouselCards !== undefined) {
105
- const { items, truncated } = clampArray(
92
+ const { items, truncated } = copyBounded(
106
93
  carouselCards,
107
94
  CAROUSEL_ATTACHMENT_CAP,
108
95
  );
@@ -69,13 +69,17 @@ describe("listVocabulary $action dispatch", () => {
69
69
  const trigger = rowOut(registry.dispatch).props.children as ReactElement;
70
70
  const onClick = (trigger.props as { onClick: (e: unknown) => void })
71
71
  .onClick;
72
- onClick({ target: { closest: () => null } });
72
+ onClick({
73
+ target: { closest: () => trigger },
74
+ currentTarget: trigger,
75
+ });
73
76
  expect(handler).toHaveBeenCalledWith({ payload: { type: "open" } });
74
77
  });
75
78
 
76
79
  type KeyDownEvent = {
77
80
  key: string;
78
81
  target: { closest: (selector: string) => unknown };
82
+ currentTarget: unknown;
79
83
  preventDefault: () => void;
80
84
  };
81
85
 
@@ -89,14 +93,17 @@ describe("listVocabulary $action dispatch", () => {
89
93
  const handler = vi.fn();
90
94
  const registry = createActionRegistry({ open: handler });
91
95
  const onKeyDown = getOnKeyDown(registry.dispatch);
96
+ const currentTarget = {};
92
97
  onKeyDown({
93
98
  key: "Enter",
94
- target: { closest: () => null },
99
+ target: { closest: () => currentTarget },
100
+ currentTarget,
95
101
  preventDefault: vi.fn(),
96
102
  });
97
103
  onKeyDown({
98
104
  key: " ",
99
- target: { closest: () => null },
105
+ target: { closest: () => currentTarget },
106
+ currentTarget,
100
107
  preventDefault: vi.fn(),
101
108
  });
102
109
  expect(handler).toHaveBeenCalledTimes(2);
@@ -109,6 +116,7 @@ describe("listVocabulary $action dispatch", () => {
109
116
  onKeyDown({
110
117
  key: "Tab",
111
118
  target: { closest: () => null },
119
+ currentTarget: {},
112
120
  preventDefault: vi.fn(),
113
121
  });
114
122
  expect(handler).not.toHaveBeenCalled();
@@ -124,7 +132,7 @@ describe("listVocabulary $action dispatch", () => {
124
132
  closest: (selector: string) =>
125
133
  selector.includes("button") ? nestedButton : null,
126
134
  };
127
- onClick({ target: nestedButton });
135
+ onClick({ target: nestedButton, currentTarget: {} });
128
136
  expect(handler).not.toHaveBeenCalled();
129
137
  });
130
138
 
@@ -137,7 +145,12 @@ describe("listVocabulary $action dispatch", () => {
137
145
  selector.includes("input") ? nestedInput : null,
138
146
  };
139
147
  const preventDefault = vi.fn();
140
- onKeyDown({ key: "Enter", target: nestedInput, preventDefault });
148
+ onKeyDown({
149
+ key: "Enter",
150
+ target: nestedInput,
151
+ currentTarget: {},
152
+ preventDefault,
153
+ });
141
154
  expect(handler).not.toHaveBeenCalled();
142
155
  expect(preventDefault).not.toHaveBeenCalled();
143
156
  });
@@ -152,7 +165,7 @@ describe("listVocabulary $action dispatch", () => {
152
165
  closest: (selector: string) =>
153
166
  selector.includes('[role="button"]') ? nestedRoleButton : null,
154
167
  };
155
- onClick({ target: nestedRoleButton });
168
+ onClick({ target: nestedRoleButton, currentTarget: {} });
156
169
  expect(handler).not.toHaveBeenCalled();
157
170
  });
158
171
 
@@ -165,7 +178,12 @@ describe("listVocabulary $action dispatch", () => {
165
178
  selector.includes('[role="button"]') ? nestedRoleButton : null,
166
179
  };
167
180
  const preventDefault = vi.fn();
168
- onKeyDown({ key: "Enter", target: nestedRoleButton, preventDefault });
181
+ onKeyDown({
182
+ key: "Enter",
183
+ target: nestedRoleButton,
184
+ currentTarget: {},
185
+ preventDefault,
186
+ });
169
187
  expect(handler).not.toHaveBeenCalled();
170
188
  });
171
189
 
@@ -173,8 +191,14 @@ describe("listVocabulary $action dispatch", () => {
173
191
  const handler = vi.fn();
174
192
  const registry = createActionRegistry({ open: handler });
175
193
  const onKeyDown = getOnKeyDown(registry.dispatch);
194
+ const currentTarget = {};
176
195
  const preventDefault = vi.fn();
177
- onKeyDown({ key: " ", target: { closest: () => null }, preventDefault });
196
+ onKeyDown({
197
+ key: " ",
198
+ target: { closest: () => currentTarget },
199
+ currentTarget,
200
+ preventDefault,
201
+ });
178
202
  expect(handler).toHaveBeenCalledWith({ payload: { type: "open" } });
179
203
  expect(preventDefault).toHaveBeenCalledTimes(1);
180
204
  });
@@ -183,10 +207,12 @@ describe("listVocabulary $action dispatch", () => {
183
207
  const handler = vi.fn();
184
208
  const registry = createActionRegistry({ open: handler });
185
209
  const onKeyDown = getOnKeyDown(registry.dispatch);
210
+ const currentTarget = {};
186
211
  const preventDefault = vi.fn();
187
212
  onKeyDown({
188
213
  key: "Enter",
189
- target: { closest: () => null },
214
+ target: { closest: () => currentTarget },
215
+ currentTarget,
190
216
  preventDefault,
191
217
  });
192
218
  expect(handler).toHaveBeenCalledWith({ payload: { type: "open" } });
@@ -28,13 +28,15 @@ export const listVocabulary = {
28
28
  data-aui="listview-item-trigger"
29
29
  onClick={(event) => {
30
30
  const target = event.target as Element;
31
- if (target.closest(INTERACTIVE_DESCENDANT_SELECTOR)) return;
31
+ const hit = target.closest(INTERACTIVE_DESCENDANT_SELECTOR);
32
+ if (hit !== null && hit !== event.currentTarget) return;
32
33
  fire($action, $dispatch);
33
34
  }}
34
35
  onKeyDown={(event) => {
35
36
  if (event.key !== "Enter" && event.key !== " ") return;
36
37
  const target = event.target as Element;
37
- if (target.closest(INTERACTIVE_DESCENDANT_SELECTOR)) return;
38
+ const hit = target.closest(INTERACTIVE_DESCENDANT_SELECTOR);
39
+ if (hit !== null && hit !== event.currentTarget) return;
38
40
  if (event.key === " ") event.preventDefault();
39
41
  fire($action, $dispatch);
40
42
  }}
@@ -1,20 +0,0 @@
1
- import { ClampReason } from "./constants.js";
2
- //#region src/teams/boundSpec.d.ts
3
- /**
4
- * Produces a bounded plain copy of a raw generative-ui spec before it
5
- * reaches `normalizeSpec`, whose own traversal of the root array or any
6
- * `children` array walks the full reported length of a hostile proxied
7
- * array before any per-field cap downstream ever applies. Every array
8
- * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}
9
- * entries via `Array.prototype.slice`, which bounds even a proxy with a
10
- * fabricated `length`; recursion itself is capped at
11
- * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was
12
- * truncated, receiving the reason for that truncation: `"children"`,
13
- * `"depth"`, `"budget"`, or `"cycle"`. The walk also spends a total budget of
14
- * {@link NODE_BUDGET} nodes, so shared references cannot multiply work
15
- * exponentially, and a node that is its own ancestor is cut to `null`.
16
- */
17
- declare function boundSpec(spec: unknown, onClamp: (reason: ClampReason) => void): unknown;
18
- //#endregion
19
- export { boundSpec };
20
- //# sourceMappingURL=boundSpec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"boundSpec.d.ts","names":[],"sources":["../../src/teams/boundSpec.ts"],"mappings":";;;;;;;;;;;;;;;;iBA0FgB,UACd,eACA,UAAU,QAAQ"}
@@ -1,67 +0,0 @@
1
- import { NODE_BUDGET } from "./constants.js";
2
- //#region src/teams/boundSpec.ts
3
- function boundNode(value, depth, onClamp, state, ancestors) {
4
- if (state.remaining <= 0) {
5
- if (!state.exhausted) {
6
- state.exhausted = true;
7
- onClamp("budget");
8
- }
9
- return null;
10
- }
11
- state.remaining -= 1;
12
- if (depth > 64) {
13
- onClamp("depth");
14
- return null;
15
- }
16
- if (Array.isArray(value)) {
17
- if (ancestors.has(value)) {
18
- onClamp("cycle");
19
- return null;
20
- }
21
- ancestors.add(value);
22
- const bounded = Array.prototype.slice.call(value, 0, 200);
23
- if (value.length > 200) onClamp("children");
24
- const result = bounded.map((item) => boundNode(item, depth + 1, onClamp, state, ancestors));
25
- ancestors.delete(value);
26
- return result;
27
- }
28
- if (value !== null && typeof value === "object" && "children" in value) {
29
- if (ancestors.has(value)) {
30
- onClamp("cycle");
31
- return null;
32
- }
33
- ancestors.add(value);
34
- const record = value;
35
- const result = {
36
- ...record,
37
- children: boundNode(record["children"], depth + 1, onClamp, state, ancestors)
38
- };
39
- ancestors.delete(value);
40
- return result;
41
- }
42
- return value;
43
- }
44
- /**
45
- * Produces a bounded plain copy of a raw generative-ui spec before it
46
- * reaches `normalizeSpec`, whose own traversal of the root array or any
47
- * `children` array walks the full reported length of a hostile proxied
48
- * array before any per-field cap downstream ever applies. Every array
49
- * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}
50
- * entries via `Array.prototype.slice`, which bounds even a proxy with a
51
- * fabricated `length`; recursion itself is capped at
52
- * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was
53
- * truncated, receiving the reason for that truncation: `"children"`,
54
- * `"depth"`, `"budget"`, or `"cycle"`. The walk also spends a total budget of
55
- * {@link NODE_BUDGET} nodes, so shared references cannot multiply work
56
- * exponentially, and a node that is its own ancestor is cut to `null`.
57
- */
58
- function boundSpec(spec, onClamp) {
59
- return boundNode(spec, 0, onClamp, {
60
- remaining: NODE_BUDGET,
61
- exhausted: false
62
- }, /* @__PURE__ */ new WeakSet());
63
- }
64
- //#endregion
65
- export { boundSpec };
66
-
67
- //# sourceMappingURL=boundSpec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"boundSpec.js","names":[],"sources":["../../src/teams/boundSpec.ts"],"sourcesContent":["import {\n CHILDREN_CAP,\n MAX_TRAVERSAL_DEPTH,\n NODE_BUDGET,\n type ClampReason,\n} from \"./constants\";\n\ninterface BoundState {\n remaining: number;\n exhausted: boolean;\n}\n\nfunction boundNode(\n value: unknown,\n depth: number,\n onClamp: (reason: ClampReason) => void,\n state: BoundState,\n ancestors: WeakSet<object>,\n): unknown {\n if (state.remaining <= 0) {\n if (!state.exhausted) {\n state.exhausted = true;\n onClamp(\"budget\");\n }\n return null;\n }\n state.remaining -= 1;\n if (depth > MAX_TRAVERSAL_DEPTH) {\n onClamp(\"depth\");\n return null;\n }\n if (Array.isArray(value)) {\n if (ancestors.has(value)) {\n onClamp(\"cycle\");\n return null;\n }\n ancestors.add(value);\n const bounded = Array.prototype.slice.call(\n value,\n 0,\n CHILDREN_CAP,\n ) as unknown[];\n if (value.length > CHILDREN_CAP) onClamp(\"children\");\n const result = bounded.map((item) =>\n boundNode(item, depth + 1, onClamp, state, ancestors),\n );\n ancestors.delete(value);\n return result;\n }\n if (\n value !== null &&\n typeof value === \"object\" &&\n \"children\" in (value as Record<string, unknown>)\n ) {\n if (ancestors.has(value)) {\n onClamp(\"cycle\");\n return null;\n }\n ancestors.add(value);\n const record = value as Record<string, unknown>;\n const result = {\n ...record,\n children: boundNode(\n record[\"children\"],\n depth + 1,\n onClamp,\n state,\n ancestors,\n ),\n };\n ancestors.delete(value);\n return result;\n }\n return value;\n}\n\n/**\n * Produces a bounded plain copy of a raw generative-ui spec before it\n * reaches `normalizeSpec`, whose own traversal of the root array or any\n * `children` array walks the full reported length of a hostile proxied\n * array before any per-field cap downstream ever applies. Every array\n * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}\n * entries via `Array.prototype.slice`, which bounds even a proxy with a\n * fabricated `length`; recursion itself is capped at\n * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was\n * truncated, receiving the reason for that truncation: `\"children\"`,\n * `\"depth\"`, `\"budget\"`, or `\"cycle\"`. The walk also spends a total budget of\n * {@link NODE_BUDGET} nodes, so shared references cannot multiply work\n * exponentially, and a node that is its own ancestor is cut to `null`.\n */\nexport function boundSpec(\n spec: unknown,\n onClamp: (reason: ClampReason) => void,\n): unknown {\n return boundNode(\n spec,\n 0,\n onClamp,\n { remaining: NODE_BUDGET, exhausted: false },\n new WeakSet(),\n );\n}\n"],"mappings":";;AAYA,SAAS,UACP,OACA,OACA,SACA,OACA,WACS;CACT,IAAI,MAAM,aAAa,GAAG;EACxB,IAAI,CAAC,MAAM,WAAW;GACpB,MAAM,YAAY;GAClB,QAAQ,QAAQ;EAClB;EACA,OAAO;CACT;CACA,MAAM,aAAa;CACnB,IAAI,QAAA,IAA6B;EAC/B,QAAQ,OAAO;EACf,OAAO;CACT;CACA,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,IAAI,UAAU,IAAI,KAAK,GAAG;GACxB,QAAQ,OAAO;GACf,OAAO;EACT;EACA,UAAU,IAAI,KAAK;EACnB,MAAM,UAAU,MAAM,UAAU,MAAM,KACpC,OACA,GAAA,GAEF;EACA,IAAI,MAAM,SAAA,KAAuB,QAAQ,UAAU;EACnD,MAAM,SAAS,QAAQ,KAAK,SAC1B,UAAU,MAAM,QAAQ,GAAG,SAAS,OAAO,SAAS,CACtD;EACA,UAAU,OAAO,KAAK;EACtB,OAAO;CACT;CACA,IACE,UAAU,QACV,OAAO,UAAU,YACjB,cAAe,OACf;EACA,IAAI,UAAU,IAAI,KAAK,GAAG;GACxB,QAAQ,OAAO;GACf,OAAO;EACT;EACA,UAAU,IAAI,KAAK;EACnB,MAAM,SAAS;EACf,MAAM,SAAS;GACb,GAAG;GACH,UAAU,UACR,OAAO,aACP,QAAQ,GACR,SACA,OACA,SACF;EACF;EACA,UAAU,OAAO,KAAK;EACtB,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,UACd,MACA,SACS;CACT,OAAO,UACL,MACA,GACA,SACA;EAAE,WAAW;EAAa,WAAW;CAAM,mBAC3C,IAAI,QAAQ,CACd;AACF"}
@@ -1,102 +0,0 @@
1
- import {
2
- CHILDREN_CAP,
3
- MAX_TRAVERSAL_DEPTH,
4
- NODE_BUDGET,
5
- type ClampReason,
6
- } from "./constants";
7
-
8
- interface BoundState {
9
- remaining: number;
10
- exhausted: boolean;
11
- }
12
-
13
- function boundNode(
14
- value: unknown,
15
- depth: number,
16
- onClamp: (reason: ClampReason) => void,
17
- state: BoundState,
18
- ancestors: WeakSet<object>,
19
- ): unknown {
20
- if (state.remaining <= 0) {
21
- if (!state.exhausted) {
22
- state.exhausted = true;
23
- onClamp("budget");
24
- }
25
- return null;
26
- }
27
- state.remaining -= 1;
28
- if (depth > MAX_TRAVERSAL_DEPTH) {
29
- onClamp("depth");
30
- return null;
31
- }
32
- if (Array.isArray(value)) {
33
- if (ancestors.has(value)) {
34
- onClamp("cycle");
35
- return null;
36
- }
37
- ancestors.add(value);
38
- const bounded = Array.prototype.slice.call(
39
- value,
40
- 0,
41
- CHILDREN_CAP,
42
- ) as unknown[];
43
- if (value.length > CHILDREN_CAP) onClamp("children");
44
- const result = bounded.map((item) =>
45
- boundNode(item, depth + 1, onClamp, state, ancestors),
46
- );
47
- ancestors.delete(value);
48
- return result;
49
- }
50
- if (
51
- value !== null &&
52
- typeof value === "object" &&
53
- "children" in (value as Record<string, unknown>)
54
- ) {
55
- if (ancestors.has(value)) {
56
- onClamp("cycle");
57
- return null;
58
- }
59
- ancestors.add(value);
60
- const record = value as Record<string, unknown>;
61
- const result = {
62
- ...record,
63
- children: boundNode(
64
- record["children"],
65
- depth + 1,
66
- onClamp,
67
- state,
68
- ancestors,
69
- ),
70
- };
71
- ancestors.delete(value);
72
- return result;
73
- }
74
- return value;
75
- }
76
-
77
- /**
78
- * Produces a bounded plain copy of a raw generative-ui spec before it
79
- * reaches `normalizeSpec`, whose own traversal of the root array or any
80
- * `children` array walks the full reported length of a hostile proxied
81
- * array before any per-field cap downstream ever applies. Every array
82
- * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}
83
- * entries via `Array.prototype.slice`, which bounds even a proxy with a
84
- * fabricated `length`; recursion itself is capped at
85
- * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was
86
- * truncated, receiving the reason for that truncation: `"children"`,
87
- * `"depth"`, `"budget"`, or `"cycle"`. The walk also spends a total budget of
88
- * {@link NODE_BUDGET} nodes, so shared references cannot multiply work
89
- * exponentially, and a node that is its own ancestor is cut to `null`.
90
- */
91
- export function boundSpec(
92
- spec: unknown,
93
- onClamp: (reason: ClampReason) => void,
94
- ): unknown {
95
- return boundNode(
96
- spec,
97
- 0,
98
- onClamp,
99
- { remaining: NODE_BUDGET, exhausted: false },
100
- new WeakSet(),
101
- );
102
- }