@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.
- package/dist/convert/boundSpec.d.ts +51 -0
- package/dist/convert/boundSpec.d.ts.map +1 -0
- package/dist/convert/boundSpec.js +108 -0
- package/dist/convert/boundSpec.js.map +1 -0
- package/dist/convert/copyBounded.d.ts +28 -0
- package/dist/convert/copyBounded.d.ts.map +1 -0
- package/dist/convert/copyBounded.js +38 -0
- package/dist/convert/copyBounded.js.map +1 -0
- package/dist/convert/isElement.d.ts +6 -0
- package/dist/convert/isElement.d.ts.map +1 -0
- package/dist/convert/isElement.js +6 -0
- package/dist/convert/isElement.js.map +1 -0
- package/dist/convert/takeRun.d.ts +9 -0
- package/dist/convert/takeRun.d.ts.map +1 -0
- package/dist/convert/takeRun.js +20 -0
- package/dist/convert/takeRun.js.map +1 -0
- package/dist/slack/constants.d.ts +1 -26
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +1 -26
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/fromSlackBlocks.js +6 -10
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +28 -95
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/teams/constants.d.ts +1 -28
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +1 -32
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +12 -23
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js +5 -9
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/vocabulary/list.js +4 -2
- package/dist/vocabulary/list.js.map +1 -1
- package/package.json +6 -6
- package/src/convert/boundSpec.test.ts +192 -0
- package/src/convert/boundSpec.ts +134 -0
- package/src/convert/copyBounded.test.ts +131 -0
- package/src/convert/copyBounded.ts +38 -0
- package/src/convert/isElement.ts +6 -0
- package/src/convert/takeRun.test.ts +64 -0
- package/src/convert/takeRun.ts +19 -0
- package/src/slack/constants.ts +0 -29
- package/src/slack/fromSlackBlocks.test.ts +105 -7
- package/src/slack/fromSlackBlocks.ts +9 -9
- package/src/slack/toSlackBlocks.test.ts +82 -2
- package/src/slack/toSlackBlocks.ts +68 -164
- package/src/teams/constants.ts +0 -42
- package/src/teams/toAdaptiveCard.test.ts +74 -2
- package/src/teams/toAdaptiveCard.ts +29 -36
- package/src/teams/toTeamsAttachments.ts +4 -17
- package/src/vocabulary/list.test.tsx +35 -9
- package/src/vocabulary/list.tsx +4 -2
- package/dist/teams/boundSpec.d.ts +0 -20
- package/dist/teams/boundSpec.d.ts.map +0 -1
- package/dist/teams/boundSpec.js +0 -67
- package/dist/teams/boundSpec.js.map +0 -1
- 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:
|
|
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
|
|
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)
|
|
341
|
-
(
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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 } =
|
|
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({
|
|
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: () =>
|
|
99
|
+
target: { closest: () => currentTarget },
|
|
100
|
+
currentTarget,
|
|
95
101
|
preventDefault: vi.fn(),
|
|
96
102
|
});
|
|
97
103
|
onKeyDown({
|
|
98
104
|
key: " ",
|
|
99
|
-
target: { closest: () =>
|
|
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({
|
|
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({
|
|
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({
|
|
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: () =>
|
|
214
|
+
target: { closest: () => currentTarget },
|
|
215
|
+
currentTarget,
|
|
190
216
|
preventDefault,
|
|
191
217
|
});
|
|
192
218
|
expect(handler).toHaveBeenCalledWith({ payload: { type: "open" } });
|
package/src/vocabulary/list.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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"}
|
package/dist/teams/boundSpec.js
DELETED
|
@@ -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"}
|
package/src/teams/boundSpec.ts
DELETED
|
@@ -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
|
-
}
|