silvery 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/Text-Lq0dmj8-.mjs +239 -0
- package/dist/Text-Lq0dmj8-.mjs.map +1 -0
- package/dist/UPNG-Bo33r8rA.mjs +3 -0
- package/dist/UPNG-DosRPdF4.mjs +5075 -0
- package/dist/UPNG-DosRPdF4.mjs.map +1 -0
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs +6 -0
- package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs.map +1 -0
- package/dist/{animation-Cn64yepo.mjs → animation-ZMN2_XKv.mjs} +2 -2
- package/dist/animation-ZMN2_XKv.mjs.map +1 -0
- package/dist/{ansi-Cc33mW54.d.mts → ansi-2Xn0yatP.d.mts} +1 -1
- package/dist/{ansi-Cc33mW54.d.mts.map → ansi-2Xn0yatP.d.mts.map} +1 -1
- package/dist/{ansi-CLOitHKx.mjs → ansi-D1KQMAbf.mjs} +1 -1
- package/dist/{ansi-CLOitHKx.mjs.map → ansi-D1KQMAbf.mjs.map} +1 -1
- package/dist/ansi-yC4RyBNY.mjs +22441 -0
- package/dist/ansi-yC4RyBNY.mjs.map +1 -0
- package/dist/apng-CR08rIaH.mjs +58 -0
- package/dist/apng-CR08rIaH.mjs.map +1 -0
- package/dist/apng-DaHfVaVI.mjs +3 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/assets/skia.darwin-arm64-DQs5sT6N.node +0 -0
- package/dist/backend-B-WYLUib.mjs +13396 -0
- package/dist/backend-B-WYLUib.mjs.map +1 -0
- package/dist/backends-CUtan80W.mjs +3 -0
- package/dist/backends-DIVYzKqd.mjs +1083 -0
- package/dist/backends-DIVYzKqd.mjs.map +1 -0
- package/dist/bound-term-0sPrrzH1.d.mts +4640 -0
- package/dist/bound-term-0sPrrzH1.d.mts.map +1 -0
- package/dist/canvas-1v7dPT-_.mjs +3 -0
- package/dist/canvas-CSuPOMNt.mjs +1442 -0
- package/dist/canvas-CSuPOMNt.mjs.map +1 -0
- package/dist/{chunk-Vs_PY4HZ.mjs → chunk-BSw8zbkd.mjs} +1 -1
- package/dist/cli-dvo0r2fs.mjs +4 -0
- package/dist/compare-CQodSH4G.mjs +376 -0
- package/dist/compare-CQodSH4G.mjs.map +1 -0
- package/dist/compare-DHlcxEYA.mjs +3 -0
- package/dist/context-BU5LkkIy.mjs.map +1 -1
- package/dist/devtools-CJdt5H0X.mjs +2 -0
- package/dist/{devtools-DxkSLXDA.mjs → devtools-DcQjgyjL.mjs} +5 -4
- package/dist/{devtools-DxkSLXDA.mjs.map → devtools-DcQjgyjL.mjs.map} +1 -1
- package/dist/easing-BI-ASGMO.d.mts +24 -0
- package/dist/easing-BI-ASGMO.d.mts.map +1 -0
- package/dist/{eta-Bb3RH3wh.mjs → eta-CJlGH06n.mjs} +1 -1
- package/dist/{eta-Bb3RH3wh.mjs.map → eta-CJlGH06n.mjs.map} +1 -1
- package/dist/flexily-zero-adapter-C3Vj0fPt.mjs +306 -0
- package/dist/flexily-zero-adapter-C3Vj0fPt.mjs.map +1 -0
- package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-C4lW_Ov5.mjs} +1 -1
- package/dist/fonts-BFmhXDv7.mjs +88 -0
- package/dist/fonts-BFmhXDv7.mjs.map +1 -0
- package/dist/gif-C_AjaT9d.mjs +188 -0
- package/dist/gif-C_AjaT9d.mjs.map +1 -0
- package/dist/gif-DaC4XrxA.mjs +3 -0
- package/dist/gifenc-BOUT-KFB.mjs +730 -0
- package/dist/gifenc-BOUT-KFB.mjs.map +1 -0
- package/dist/image-C2Birh2x.mjs +1252 -0
- package/dist/image-C2Birh2x.mjs.map +1 -0
- package/dist/index-BUMxS65f.d.mts +453 -0
- package/dist/index-BUMxS65f.d.mts.map +1 -0
- package/dist/{index-D3saHouR.d.mts → index-CSQf13CI.d.mts} +1057 -1133
- package/dist/index-CSQf13CI.d.mts.map +1 -0
- package/dist/{index-BXslOebb.d.mts → index-Cl9KKjQ_.d.mts} +4919 -3921
- package/dist/index-Cl9KKjQ_.d.mts.map +1 -0
- package/dist/index-XbNrPhWl.d.mts +336 -0
- package/dist/index-XbNrPhWl.d.mts.map +1 -0
- package/dist/index.d.mts +8 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14 -12
- package/dist/index.mjs.map +1 -1
- package/dist/key-mapping-CS-YD_cD.mjs +132 -0
- package/dist/key-mapping-CS-YD_cD.mjs.map +1 -0
- package/dist/key-mapping-Yn-Jgrij.mjs +3 -0
- package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-C07LEXWT.mjs} +1 -1
- package/dist/layout-engine-C2px0RJE.mjs +67 -0
- package/dist/layout-engine-C2px0RJE.mjs.map +1 -0
- package/dist/layout-signals-Cnw6xk8Q.mjs +988 -0
- package/dist/layout-signals-Cnw6xk8Q.mjs.map +1 -0
- package/dist/mouse-events-Dki3ISIp.mjs +1044 -0
- package/dist/mouse-events-Dki3ISIp.mjs.map +1 -0
- package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CIRjrzma.mjs} +3 -3
- package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CIRjrzma.mjs.map} +1 -1
- package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-DHZ2xUT2.d.mts} +2 -2
- package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-DHZ2xUT2.d.mts.map} +1 -1
- package/dist/{node-BeWlnCPY.mjs → node-CjM5Rt-M.mjs} +4 -4
- package/dist/node-CjM5Rt-M.mjs.map +1 -0
- package/dist/playwright-D5YiZcNS.mjs +76397 -0
- package/dist/playwright-D5YiZcNS.mjs.map +1 -0
- package/dist/png-codec-Dp84742B.mjs +36 -0
- package/dist/png-codec-Dp84742B.mjs.map +1 -0
- package/dist/png-codec-QwOtJ8Zs.mjs +3 -0
- package/dist/progress-DB_Xo071.mjs +675 -0
- package/dist/progress-DB_Xo071.mjs.map +1 -0
- package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-oJwq22CR.mjs} +4 -4
- package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-oJwq22CR.mjs.map} +1 -1
- package/dist/rasterizer-BRXrDdWx.mjs +3 -0
- package/dist/rasterizer-CpEhJvdR.mjs +296 -0
- package/dist/rasterizer-CpEhJvdR.mjs.map +1 -0
- package/dist/reconciler-DldIJB93.mjs +2083 -0
- package/dist/reconciler-DldIJB93.mjs.map +1 -0
- package/dist/{render-string-CDCeYkS3.mjs → render-string-BcoCpjCB.mjs} +1 -1
- package/dist/{render-string-Darrg7ku.mjs → render-string-DkQacASz.mjs} +2707 -549
- package/dist/render-string-DkQacASz.mjs.map +1 -0
- package/dist/resvg-js-DkOndZI3.mjs +203 -0
- package/dist/resvg-js-DkOndZI3.mjs.map +1 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +3 -3
- package/dist/schemes-JjNp4aSl.mjs +2611 -0
- package/dist/schemes-JjNp4aSl.mjs.map +1 -0
- package/dist/{spinner-CGo34vyR.d.mts → spinner-CZINHpkV.d.mts} +2 -2
- package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CZINHpkV.d.mts.map} +1 -1
- package/dist/{spinner-CeOmcuw_.mjs → spinner-D9lrHr8s.mjs} +7 -7
- package/dist/spinner-D9lrHr8s.mjs.map +1 -0
- package/dist/src-5w9QR6_8.mjs +1071 -0
- package/dist/src-5w9QR6_8.mjs.map +1 -0
- package/dist/src-BNTToU7l.mjs +4387 -0
- package/dist/src-BNTToU7l.mjs.map +1 -0
- package/dist/{src-CF-6UN01.mjs → src-BR4xNwdG.mjs} +10436 -2622
- package/dist/src-BR4xNwdG.mjs.map +1 -0
- package/dist/{types-Bk2yw9Qj.mjs → src-DKp-_OFG.mjs} +34 -94
- package/dist/src-DKp-_OFG.mjs.map +1 -0
- package/dist/src-bt8wSrfJ.mjs +258 -0
- package/dist/src-bt8wSrfJ.mjs.map +1 -0
- package/dist/src-e33Y6kNJ.mjs +3 -0
- package/dist/src-iDwu25UD.mjs +1814 -0
- package/dist/src-iDwu25UD.mjs.map +1 -0
- package/dist/steps-Bp2uNqnn.d.mts +202 -0
- package/dist/steps-Bp2uNqnn.d.mts.map +1 -0
- package/dist/svg-15lZZzxq.mjs +486 -0
- package/dist/svg-15lZZzxq.mjs.map +1 -0
- package/dist/svg-Cz0UXcDj.mjs +255 -0
- package/dist/svg-Cz0UXcDj.mjs.map +1 -0
- package/dist/svg-DY72a4HK.mjs +3 -0
- package/dist/svg-g1D6ErwR.d.mts +82 -0
- package/dist/svg-g1D6ErwR.d.mts.map +1 -0
- package/dist/term.d.mts +3 -0
- package/dist/term.mjs +9 -0
- package/dist/term.mjs.map +1 -0
- package/dist/theme.d.mts +95 -2
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +9 -3
- package/dist/theme.mjs.map +1 -0
- package/dist/{types-BH_v3iMT.d.mts → types-kt_fKR37.d.mts} +2 -15
- package/dist/types-kt_fKR37.d.mts.map +1 -0
- package/dist/ui/animation.d.mts +2 -1
- package/dist/ui/animation.mjs +1 -1
- package/dist/ui/ansi.d.mts +1 -1
- package/dist/ui/ansi.mjs +1 -1
- package/dist/ui/cli.d.mts +3 -3
- package/dist/ui/cli.mjs +5 -5
- package/dist/ui/display.d.mts +1 -1
- package/dist/ui/image.d.mts +2 -2
- package/dist/ui/image.mjs +2 -2
- package/dist/ui/input.d.mts +1 -1
- package/dist/ui/input.mjs +4 -2
- package/dist/ui/input.mjs.map +1 -1
- package/dist/ui/progress.d.mts +5 -249
- package/dist/ui/progress.mjs +5 -858
- package/dist/ui/react.d.mts +1 -1
- package/dist/ui/react.mjs +2 -2
- package/dist/ui/recording-chrome-react.d.mts +21 -0
- package/dist/ui/recording-chrome-react.d.mts.map +1 -0
- package/dist/ui/recording-chrome-react.mjs +105 -0
- package/dist/ui/recording-chrome-react.mjs.map +1 -0
- package/dist/ui/recording-chrome.d.mts +2 -0
- package/dist/ui/recording-chrome.mjs +2 -0
- package/dist/ui/utils.mjs +1 -1
- package/dist/ui/wrappers.d.mts +3 -3
- package/dist/ui/wrappers.mjs +2 -2
- package/dist/ui.d.mts +7 -6
- package/dist/ui.mjs +8 -7
- package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DRDDVwjh.d.mts} +5 -25
- package/dist/useLatest-DRDDVwjh.d.mts.map +1 -0
- package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-YeohVLeo.d.mts} +4 -55
- package/dist/with-text-input-YeohVLeo.d.mts.map +1 -0
- package/dist/wrapper-C70ATkVv.mjs +3527 -0
- package/dist/wrapper-C70ATkVv.mjs.map +1 -0
- package/dist/{wrappers-UTADQkSY.mjs → wrappers-BCUYITrY.mjs} +5 -157
- package/dist/wrappers-BCUYITrY.mjs.map +1 -0
- package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-BnZX1PAY.mjs} +28 -2
- package/dist/yoga-adapter-BnZX1PAY.mjs.map +1 -0
- package/dist/yoga-adapter-DxgsQ_gg.mjs +2 -0
- package/dist/zipBundle-3nqeDRtm.mjs +3 -0
- package/dist/zipBundle-VNAYFmqJ.mjs +2003 -0
- package/dist/zipBundle-VNAYFmqJ.mjs.map +1 -0
- package/package.json +20 -9
- package/dist/animation-Cn64yepo.mjs.map +0 -1
- package/dist/cli-BKp0YtBD.mjs +0 -4
- package/dist/devtools-9QY4teqI.mjs +0 -2
- package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
- package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
- package/dist/image-CTII5QWI.mjs +0 -477
- package/dist/image-CTII5QWI.mjs.map +0 -1
- package/dist/index-BXslOebb.d.mts.map +0 -1
- package/dist/index-BnA7mNpo.d.mts +0 -175
- package/dist/index-BnA7mNpo.d.mts.map +0 -1
- package/dist/index-D3saHouR.d.mts.map +0 -1
- package/dist/layout-engine-ClUgv6jB.mjs +0 -50
- package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
- package/dist/node-BeWlnCPY.mjs.map +0 -1
- package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
- package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
- package/dist/render-string-Darrg7ku.mjs.map +0 -1
- package/dist/spinner-CeOmcuw_.mjs.map +0 -1
- package/dist/src-B5GjfG7g.mjs +0 -4305
- package/dist/src-B5GjfG7g.mjs.map +0 -1
- package/dist/src-CChwjk0Z.mjs +0 -738
- package/dist/src-CChwjk0Z.mjs.map +0 -1
- package/dist/src-CF-6UN01.mjs.map +0 -1
- package/dist/src-NCKb8kE5.mjs +0 -2660
- package/dist/src-NCKb8kE5.mjs.map +0 -1
- package/dist/types-BH_v3iMT.d.mts.map +0 -1
- package/dist/types-Bk2yw9Qj.mjs.map +0 -1
- package/dist/ui/progress.d.mts.map +0 -1
- package/dist/ui/progress.mjs.map +0 -1
- package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
- package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
- package/dist/wrappers-UTADQkSY.mjs.map +0 -1
- package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
- package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
|
@@ -0,0 +1,2083 @@
|
|
|
1
|
+
import { d as syncTextContentSignal } from "./layout-signals-Cnw6xk8Q.mjs";
|
|
2
|
+
import { k as warnOnce, t as init_src } from "./src-BNTToU7l.mjs";
|
|
3
|
+
import { A as getActiveLineHeight, D as displayWidth, rt as wrapText, z as isSoftBreakPoint } from "./ansi-yC4RyBNY.mjs";
|
|
4
|
+
import { a as requireCapability, n as getConstants, r as getLayoutEngine } from "./layout-engine-C2px0RJE.mjs";
|
|
5
|
+
import { createContext } from "react";
|
|
6
|
+
import { createLogger } from "loggily";
|
|
7
|
+
import Reconciler from "react-reconciler";
|
|
8
|
+
import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority } from "react-reconciler/constants.js";
|
|
9
|
+
//#region packages/ag/src/dirty-tracking.ts
|
|
10
|
+
/**
|
|
11
|
+
* Nodes with any content/style dirty flag. Written by reconciler,
|
|
12
|
+
* read by render phase for targeted subtree entry.
|
|
13
|
+
*/
|
|
14
|
+
const contentDirtyNodes = /* @__PURE__ */ new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Nodes where ONLY style props changed (no content, layout, or children changes).
|
|
17
|
+
* These are eligible for the style-only fast path in the render phase, which
|
|
18
|
+
* updates cell styles without re-collecting text or re-computing layout.
|
|
19
|
+
*
|
|
20
|
+
* A node is style-only when commitUpdate classifies contentChanged="style"
|
|
21
|
+
* AND layoutChanged=false. The render phase checks this set to decide whether
|
|
22
|
+
* to use restyleRegion() instead of full renderText()/renderBox().
|
|
23
|
+
*/
|
|
24
|
+
const styleOnlyDirtyNodes = /* @__PURE__ */ new Set();
|
|
25
|
+
/**
|
|
26
|
+
* Nodes where scrollTo/scrollOffset changed. These don't affect Flexily layout
|
|
27
|
+
* dimensions, but the scroll, sticky, scrollRect, and notify phases must still
|
|
28
|
+
* run to update visible children positions.
|
|
29
|
+
*
|
|
30
|
+
* Written by reconciler (host-config.ts commitUpdate), read by ag.ts
|
|
31
|
+
* layout-on-demand gate.
|
|
32
|
+
*/
|
|
33
|
+
const scrollDirtyNodes = /* @__PURE__ */ new Set();
|
|
34
|
+
/** Mark a node as content-dirty. Called when content/style flags are set. */
|
|
35
|
+
function trackContentDirty(node) {
|
|
36
|
+
contentDirtyNodes.add(node);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mark a node as style-only dirty. Called when commitUpdate sees
|
|
40
|
+
* contentChanged="style" AND layoutChanged=false.
|
|
41
|
+
* If a node is later marked with contentDirty, the render phase ignores
|
|
42
|
+
* the style-only flag (full path takes precedence).
|
|
43
|
+
*/
|
|
44
|
+
function trackStyleOnlyDirty(node) {
|
|
45
|
+
styleOnlyDirtyNodes.add(node);
|
|
46
|
+
}
|
|
47
|
+
/** Mark a node as scroll-dirty. Called when scrollTo/scrollOffset props change. */
|
|
48
|
+
function trackScrollDirty(node) {
|
|
49
|
+
scrollDirtyNodes.add(node);
|
|
50
|
+
}
|
|
51
|
+
/** O(1) check: are there any scroll-dirty nodes? */
|
|
52
|
+
function hasScrollDirty() {
|
|
53
|
+
return scrollDirtyNodes.size > 0;
|
|
54
|
+
}
|
|
55
|
+
/** Clear all dirty tracking. Called after each render pass completes. */
|
|
56
|
+
function clearDirtyTracking() {
|
|
57
|
+
contentDirtyNodes.clear();
|
|
58
|
+
styleOnlyDirtyNodes.clear();
|
|
59
|
+
scrollDirtyNodes.clear();
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region packages/ag/src/epoch.ts
|
|
63
|
+
/**
|
|
64
|
+
* The current render epoch. Incremented after each render pass.
|
|
65
|
+
* Reconciler stamps dirty nodes with this value; render phase checks equality.
|
|
66
|
+
*/
|
|
67
|
+
let renderEpoch = 0;
|
|
68
|
+
/** Get the current render epoch value. */
|
|
69
|
+
function getRenderEpoch() {
|
|
70
|
+
return renderEpoch;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Advance the render epoch. Called once at the end of each render pass.
|
|
74
|
+
* All nodes stamped with the old epoch instantly become "not dirty".
|
|
75
|
+
*/
|
|
76
|
+
function advanceRenderEpoch() {
|
|
77
|
+
renderEpoch++;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if an epoch stamp matches the current render epoch (i.e., "is dirty").
|
|
81
|
+
*/
|
|
82
|
+
function isCurrentEpoch(epoch) {
|
|
83
|
+
return epoch === renderEpoch;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a specific dirty bit is set for the current epoch.
|
|
87
|
+
* Returns true if dirtyEpoch matches the current render epoch AND the bit is set.
|
|
88
|
+
*/
|
|
89
|
+
function isDirty(dirtyBits, dirtyEpoch, bit) {
|
|
90
|
+
return dirtyEpoch === renderEpoch && (dirtyBits & bit) !== 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if ANY dirty bit is set for the current epoch.
|
|
94
|
+
*/
|
|
95
|
+
function isAnyDirty(dirtyBits, dirtyEpoch) {
|
|
96
|
+
return dirtyEpoch === renderEpoch && dirtyBits !== 0;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region packages/scope/src/trace.ts
|
|
100
|
+
/**
|
|
101
|
+
* @silvery/scope/trace — opt-in leak detector for scopes and disposables.
|
|
102
|
+
*
|
|
103
|
+
* Gated by `SILVERY_SCOPE_TRACE=1`. When enabled, every `createScope()`
|
|
104
|
+
* and `disposable()` call is recorded with its creation stack; dispose
|
|
105
|
+
* unregisters; at process exit any remaining entries are logged.
|
|
106
|
+
*
|
|
107
|
+
* Zero overhead when disabled — the public functions are no-ops behind
|
|
108
|
+
* an early-return guard. The trace registry isn't even allocated.
|
|
109
|
+
*
|
|
110
|
+
* Why: silvery's `Scope` (Phase 0/1/2 of `km-silvery.lifecycle-scope`)
|
|
111
|
+
* makes resource ownership explicit, but adoption is opt-in. This is the
|
|
112
|
+
* runtime backstop for what the ESLint `no-raw-lifecycle` rule can't see
|
|
113
|
+
* (dynamic call sites, third-party paths). Together they make
|
|
114
|
+
* convention-driven leaks structurally impossible.
|
|
115
|
+
*
|
|
116
|
+
* Usage in tests / CI:
|
|
117
|
+
* ```bash
|
|
118
|
+
* SILVERY_SCOPE_TRACE=1 bun run test
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* The detector logs to stderr at process exit (or `getTraceSnapshot()`
|
|
122
|
+
* is callable any time for in-test assertions). Production builds — no
|
|
123
|
+
* env var set — skip every code path.
|
|
124
|
+
*
|
|
125
|
+
* @packageDocumentation
|
|
126
|
+
*/
|
|
127
|
+
const TRACE_ENV = "SILVERY_SCOPE_TRACE";
|
|
128
|
+
function envEnabled() {
|
|
129
|
+
try {
|
|
130
|
+
return !!globalThis.process?.env?.[TRACE_ENV];
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const traceEnabled = envEnabled();
|
|
136
|
+
const live = traceEnabled ? /* @__PURE__ */ new Map() : null;
|
|
137
|
+
/** Internal — register a handle on creation. No-op when tracing is off. */
|
|
138
|
+
function _trackCreate(handle, kind, name) {
|
|
139
|
+
if (!live) return;
|
|
140
|
+
const stack = (/* @__PURE__ */ new Error("(creation stack)")).stack ?? "";
|
|
141
|
+
live.set(handle, {
|
|
142
|
+
kind,
|
|
143
|
+
name,
|
|
144
|
+
createdAt: stack
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Internal — unregister a handle on dispose. No-op when tracing is off. */
|
|
148
|
+
function _trackDispose(handle) {
|
|
149
|
+
if (!live) return;
|
|
150
|
+
live.delete(handle);
|
|
151
|
+
}
|
|
152
|
+
/** Force the at-exit report to fire now (for tests / manual diagnostics).
|
|
153
|
+
* Always logs the count, even if zero. No-op when tracing is off. */
|
|
154
|
+
function reportTraceLeaks() {
|
|
155
|
+
if (!live) return 0;
|
|
156
|
+
const count = live.size;
|
|
157
|
+
if (count === 0) {
|
|
158
|
+
console.error("[silvery:scope:trace] no undisposed handles");
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
console.error(`[silvery:scope:trace] ${count} undisposed handle(s):`);
|
|
162
|
+
for (const entry of live.values()) {
|
|
163
|
+
const label = entry.kind + (entry.name ? `(${entry.name})` : "");
|
|
164
|
+
console.error(` - ${label}`);
|
|
165
|
+
console.error(entry.createdAt);
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Print a per-scope handle-delta diagnostic at scope close. Called from
|
|
171
|
+
* `Scope[Symbol.asyncDispose]()` AFTER the inherited stack drains. Per
|
|
172
|
+
* km-silvery.lifecycle-leak-detection Phase 2: previously the trace only
|
|
173
|
+
* fired at process exit, which left in-test scope-close leaks invisible
|
|
174
|
+
* until the worker terminated.
|
|
175
|
+
*
|
|
176
|
+
* `pre` and `post` are snapshot counts of `getAdoptedHandles(scope).length`
|
|
177
|
+
* before and after scope-close. The delta is reported to stderr when
|
|
178
|
+
* tracing is enabled and the scope did NOT balance.
|
|
179
|
+
*/
|
|
180
|
+
function reportScopeDelta(scopeName, pre, post) {
|
|
181
|
+
if (!traceEnabled) return;
|
|
182
|
+
if (post === 0) return;
|
|
183
|
+
const label = scopeName ? `Scope(${scopeName})` : "Scope";
|
|
184
|
+
console.error(`[silvery:scope:trace] ${label} close-delta: ${pre} adopted → ${post} undisposed (${pre - post} disposed cleanly)`);
|
|
185
|
+
}
|
|
186
|
+
if (traceEnabled) {
|
|
187
|
+
const proc = globalThis.process;
|
|
188
|
+
if (proc?.on) proc.on("exit", () => {
|
|
189
|
+
reportTraceLeaks();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region packages/scope/src/handle.ts
|
|
194
|
+
/**
|
|
195
|
+
* Module-private set of every legitimate handle the factory has produced.
|
|
196
|
+
* `WeakSet` keys by reference, so disposed-and-GC'd handles drop out
|
|
197
|
+
* automatically; this is the canonical source of truth for "is this a real
|
|
198
|
+
* silvery handle?" Forged values (created via `as TickHandle` or by hand)
|
|
199
|
+
* are NOT in this set, so `adoptHandle()` and `Scope.use()` reject them.
|
|
200
|
+
*/
|
|
201
|
+
const branded = /* @__PURE__ */ new WeakSet();
|
|
202
|
+
const metadata = /* @__PURE__ */ new WeakMap();
|
|
203
|
+
/**
|
|
204
|
+
* Global count of handles currently alive (created but not yet disposed).
|
|
205
|
+
* Incremented in `defineHandle().create()`, decremented once on first
|
|
206
|
+
* dispose (idempotent). WeakSet alone cannot give a count; this integer
|
|
207
|
+
* counter is the parallel deterministic accounting layer.
|
|
208
|
+
*
|
|
209
|
+
* Used by tests to assert structural lifecycle invariants without GC:
|
|
210
|
+
* - After dispose-all: `getActiveHandleCount() === 0`
|
|
211
|
+
* - During N creates: `getActiveHandleCount() === N`
|
|
212
|
+
*
|
|
213
|
+
* Not per-scope (use `getAdoptedHandles(scope)` for per-scope accounting).
|
|
214
|
+
*/
|
|
215
|
+
let _activeHandleCount = 0;
|
|
216
|
+
/** True iff `value` was minted by `defineHandle(...).create(...)`. */
|
|
217
|
+
function isBrandedHandle(value) {
|
|
218
|
+
return typeof value === "object" && value !== null && branded.has(value);
|
|
219
|
+
}
|
|
220
|
+
function defineHandle(kind) {
|
|
221
|
+
Symbol(`silvery.handle:${kind}`);
|
|
222
|
+
return {
|
|
223
|
+
kind,
|
|
224
|
+
create(_value, dispose) {
|
|
225
|
+
const handle = Object.create(null);
|
|
226
|
+
let _disposed = false;
|
|
227
|
+
Object.defineProperty(handle, Symbol.asyncDispose, {
|
|
228
|
+
value: async () => {
|
|
229
|
+
if (!_disposed) {
|
|
230
|
+
_disposed = true;
|
|
231
|
+
_activeHandleCount--;
|
|
232
|
+
}
|
|
233
|
+
await dispose();
|
|
234
|
+
},
|
|
235
|
+
enumerable: false,
|
|
236
|
+
writable: false,
|
|
237
|
+
configurable: false
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(handle, Symbol.dispose, {
|
|
240
|
+
value: () => {
|
|
241
|
+
if (!_disposed) {
|
|
242
|
+
_disposed = true;
|
|
243
|
+
_activeHandleCount--;
|
|
244
|
+
}
|
|
245
|
+
dispose();
|
|
246
|
+
},
|
|
247
|
+
enumerable: false,
|
|
248
|
+
writable: false,
|
|
249
|
+
configurable: false
|
|
250
|
+
});
|
|
251
|
+
branded.add(handle);
|
|
252
|
+
metadata.set(handle, { kind });
|
|
253
|
+
_activeHandleCount++;
|
|
254
|
+
return handle;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Finalise a handle's public surface and freeze it. Call this from the
|
|
260
|
+
* resource factory AFTER attaching all public properties (e.g. `iterable`,
|
|
261
|
+
* `emitted`) and BEFORE returning to the consumer.
|
|
262
|
+
*
|
|
263
|
+
* @param handle The branded handle returned by `<defineHandle().create>`.
|
|
264
|
+
* @param surface Public properties to attach (each becomes non-enumerable,
|
|
265
|
+
* non-writable, non-configurable so the freeze is real).
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* const handle = Tick.create(internal, stop)
|
|
270
|
+
* finaliseHandle(handle, { iterable, emitted: () => count })
|
|
271
|
+
* return handle as TickHandle
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
function finaliseHandle(handle, surface) {
|
|
275
|
+
if (!branded.has(handle)) throw new TypeError("finaliseHandle: not a branded handle (call defineHandle().create first)");
|
|
276
|
+
for (const key of Object.keys(surface)) Object.defineProperty(handle, key, {
|
|
277
|
+
value: surface[key],
|
|
278
|
+
enumerable: true,
|
|
279
|
+
writable: false,
|
|
280
|
+
configurable: false
|
|
281
|
+
});
|
|
282
|
+
for (const sym of Object.getOwnPropertySymbols(surface)) Object.defineProperty(handle, sym, {
|
|
283
|
+
value: surface[sym],
|
|
284
|
+
enumerable: false,
|
|
285
|
+
writable: false,
|
|
286
|
+
configurable: false
|
|
287
|
+
});
|
|
288
|
+
Object.freeze(handle);
|
|
289
|
+
return handle;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Per-scope handle accounting. Keyed by `Scope` instance via WeakMap so we
|
|
293
|
+
* don't have to modify the Scope class signature for the minimum-invasive
|
|
294
|
+
* Phase 1 prototype.
|
|
295
|
+
*/
|
|
296
|
+
const ownedHandles = /* @__PURE__ */ new WeakMap();
|
|
297
|
+
const handleOrigins = /* @__PURE__ */ new WeakMap();
|
|
298
|
+
/**
|
|
299
|
+
* Adopt a `Handle` into the given scope's ownership registry. The handle:
|
|
300
|
+
*
|
|
301
|
+
* - is rejected if it isn't in the module-private `branded` WeakSet
|
|
302
|
+
* (forged values fail here)
|
|
303
|
+
* - is rejected if already owned by a different scope
|
|
304
|
+
* - is registered with `scope.use(...)` for LIFO disposal
|
|
305
|
+
* - subscribes to early manual disposal so the registry stays accurate
|
|
306
|
+
* if the consumer calls `handle[Symbol.asyncDispose]()` directly
|
|
307
|
+
*
|
|
308
|
+
* Idempotent: adopting the same handle twice into the same scope is a no-op.
|
|
309
|
+
*/
|
|
310
|
+
function adoptHandle(scope, handle) {
|
|
311
|
+
if (scope.disposed) throw new ReferenceError("Cannot adopt handle into a disposed scope");
|
|
312
|
+
if (!branded.has(handle)) throw new TypeError("adoptHandle: value is not a silvery handle (forged via 'as' or wrong factory). Use the resource's createX(scope, ...) factory.");
|
|
313
|
+
let owned = ownedHandles.get(scope);
|
|
314
|
+
if (!owned) {
|
|
315
|
+
owned = /* @__PURE__ */ new Set();
|
|
316
|
+
ownedHandles.set(scope, owned);
|
|
317
|
+
}
|
|
318
|
+
const existingOwner = handleOrigins.get(handle);
|
|
319
|
+
if (existingOwner && existingOwner !== scope) throw new TypeError("Handle already owned by another scope; create a new handle for this scope instead");
|
|
320
|
+
if (owned.has(handle)) return;
|
|
321
|
+
owned.add(handle);
|
|
322
|
+
handleOrigins.set(handle, scope);
|
|
323
|
+
const existing = metadata.get(handle);
|
|
324
|
+
if (existing && !existing.createdAt) metadata.set(handle, {
|
|
325
|
+
...existing,
|
|
326
|
+
createdAt: captureCreationStack()
|
|
327
|
+
});
|
|
328
|
+
let disposedFlag = false;
|
|
329
|
+
const removeFromRegistry = () => {
|
|
330
|
+
if (disposedFlag) return;
|
|
331
|
+
disposedFlag = true;
|
|
332
|
+
owned.delete(handle);
|
|
333
|
+
handleOrigins.delete(handle);
|
|
334
|
+
};
|
|
335
|
+
scope.use({ [Symbol.asyncDispose]: async () => {
|
|
336
|
+
try {
|
|
337
|
+
if (!disposedFlag) await handle[Symbol.asyncDispose]();
|
|
338
|
+
} finally {
|
|
339
|
+
removeFromRegistry();
|
|
340
|
+
}
|
|
341
|
+
} });
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Snapshot the current set of handles still adopted into this scope.
|
|
345
|
+
* Empty when the scope was never used or all handles are disposed.
|
|
346
|
+
*/
|
|
347
|
+
function getAdoptedHandles(scope) {
|
|
348
|
+
const owned = ownedHandles.get(scope);
|
|
349
|
+
if (!owned || owned.size === 0) return [];
|
|
350
|
+
const result = [];
|
|
351
|
+
for (const h of owned) {
|
|
352
|
+
const meta = metadata.get(h);
|
|
353
|
+
result.push({
|
|
354
|
+
kind: meta?.kind ?? "unknown",
|
|
355
|
+
createdAt: meta?.createdAt
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function captureCreationStack() {
|
|
361
|
+
return ((/* @__PURE__ */ new Error("(handle adoption)")).stack ?? "").split("\n").slice(3).join("\n");
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region packages/scope/src/index.ts
|
|
365
|
+
/**
|
|
366
|
+
* @silvery/scope — Structured concurrency scopes for silvery apps.
|
|
367
|
+
*
|
|
368
|
+
* `Scope` is a subclass of TC39's `AsyncDisposableStack` that adds:
|
|
369
|
+
* - an `AbortSignal` that aborts on disposal (and links to a parent's signal)
|
|
370
|
+
* - a `child(name?)` method that creates child scopes with cascade disposal
|
|
371
|
+
* - an overridden `[Symbol.asyncDispose]()` that disposes children before the
|
|
372
|
+
* inherited user disposer stack
|
|
373
|
+
*
|
|
374
|
+
* All disposer-stack semantics (LIFO, async-await, idempotent dispose,
|
|
375
|
+
* `SuppressedError` on multi-throw, post-dispose `ReferenceError`) come
|
|
376
|
+
* from `AsyncDisposableStack` directly.
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* using scope = createScope("app")
|
|
381
|
+
* const proc = scope.use(disposable(
|
|
382
|
+
* child_process.spawn("claude"),
|
|
383
|
+
* p => p.kill("SIGTERM"),
|
|
384
|
+
* ))
|
|
385
|
+
* ```
|
|
386
|
+
*
|
|
387
|
+
* @packageDocumentation
|
|
388
|
+
*/
|
|
389
|
+
var Scope = class Scope extends AsyncDisposableStack {
|
|
390
|
+
signal;
|
|
391
|
+
name;
|
|
392
|
+
#children = /* @__PURE__ */ new Set();
|
|
393
|
+
#parent;
|
|
394
|
+
constructor(parent, name) {
|
|
395
|
+
super();
|
|
396
|
+
this.name = name;
|
|
397
|
+
this.#parent = parent;
|
|
398
|
+
_trackCreate(this, "scope", name);
|
|
399
|
+
const controller = new AbortController();
|
|
400
|
+
this.signal = controller.signal;
|
|
401
|
+
this.defer(() => controller.abort());
|
|
402
|
+
if (parent) {
|
|
403
|
+
if (parent.disposed) throw new ReferenceError("Cannot create child of disposed scope");
|
|
404
|
+
if (parent.signal.aborted) controller.abort();
|
|
405
|
+
else {
|
|
406
|
+
const onAbort = () => controller.abort();
|
|
407
|
+
parent.signal.addEventListener("abort", onAbort, { once: true });
|
|
408
|
+
this.defer(() => parent.signal.removeEventListener("abort", onAbort));
|
|
409
|
+
}
|
|
410
|
+
parent.#children.add(this);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Create a child scope. Child's signal aborts when this scope's signal does. */
|
|
414
|
+
child(name) {
|
|
415
|
+
return new Scope(this, name);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Schedule a one-shot callback owned by this scope. Disposing the scope
|
|
419
|
+
* clears the timer if it has not fired yet. The returned function cancels
|
|
420
|
+
* the timer early and is idempotent.
|
|
421
|
+
*/
|
|
422
|
+
timeout(callback, ms, opts) {
|
|
423
|
+
let active = true;
|
|
424
|
+
const timer = setTimeout(() => {
|
|
425
|
+
if (!active) return;
|
|
426
|
+
active = false;
|
|
427
|
+
callback();
|
|
428
|
+
}, ms);
|
|
429
|
+
if (opts?.unref === true) timer.unref?.();
|
|
430
|
+
const cancel = () => {
|
|
431
|
+
if (!active) return;
|
|
432
|
+
active = false;
|
|
433
|
+
clearTimeout(timer);
|
|
434
|
+
};
|
|
435
|
+
this.defer(cancel);
|
|
436
|
+
return cancel;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Schedule a repeating callback owned by this scope. Disposing the scope
|
|
440
|
+
* clears the interval. The returned function cancels the interval early
|
|
441
|
+
* and is idempotent.
|
|
442
|
+
*/
|
|
443
|
+
interval(callback, ms, opts) {
|
|
444
|
+
let active = true;
|
|
445
|
+
const timer = setInterval(() => {
|
|
446
|
+
if (active) callback();
|
|
447
|
+
}, ms);
|
|
448
|
+
if (opts?.unref === true) timer.unref?.();
|
|
449
|
+
const cancel = () => {
|
|
450
|
+
if (!active) return;
|
|
451
|
+
active = false;
|
|
452
|
+
clearInterval(timer);
|
|
453
|
+
};
|
|
454
|
+
this.defer(cancel);
|
|
455
|
+
return cancel;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Cancellation-aware sleep. Resolves when the timer fires, when the scope
|
|
459
|
+
* is disposed, or immediately if the scope is already aborted.
|
|
460
|
+
*/
|
|
461
|
+
sleep(ms, opts) {
|
|
462
|
+
return new Promise((resolve) => {
|
|
463
|
+
if (this.signal.aborted) {
|
|
464
|
+
resolve();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let done = false;
|
|
468
|
+
let cancelTimer = null;
|
|
469
|
+
const finish = () => {
|
|
470
|
+
if (done) return;
|
|
471
|
+
done = true;
|
|
472
|
+
cancelTimer?.();
|
|
473
|
+
this.signal.removeEventListener("abort", finish);
|
|
474
|
+
resolve();
|
|
475
|
+
};
|
|
476
|
+
this.signal.addEventListener("abort", finish, { once: true });
|
|
477
|
+
cancelTimer = this.timeout(finish, ms, opts);
|
|
478
|
+
this.defer(finish);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A debounced wrapper around `fn`, owned by this scope. Each call cancels the
|
|
483
|
+
* pending invocation and reschedules `ms` out, so only the last call in a
|
|
484
|
+
* burst fires. Uses a SINGLE timer slot — no per-call defer accumulation
|
|
485
|
+
* (unlike repeatedly calling {@link timeout}), which is what makes it the
|
|
486
|
+
* right primitive for keystroke/hover debounces. Disposing the scope cancels
|
|
487
|
+
* any pending call; the returned function exposes `.cancel()` to drop a
|
|
488
|
+
* pending call early. Resolves the structured-concurrency adoption need for
|
|
489
|
+
* cancel-and-reschedule timers (@km/all/structured-concurrency).
|
|
490
|
+
*/
|
|
491
|
+
debounce(fn, ms, opts) {
|
|
492
|
+
let timer;
|
|
493
|
+
const cancel = () => {
|
|
494
|
+
if (timer !== void 0) {
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
timer = void 0;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
this.defer(cancel);
|
|
500
|
+
const debounced = (...args) => {
|
|
501
|
+
if (this.signal.aborted) return;
|
|
502
|
+
cancel();
|
|
503
|
+
timer = setTimeout(() => {
|
|
504
|
+
timer = void 0;
|
|
505
|
+
fn(...args);
|
|
506
|
+
}, ms);
|
|
507
|
+
if (opts?.unref === true) timer.unref?.();
|
|
508
|
+
};
|
|
509
|
+
debounced.cancel = cancel;
|
|
510
|
+
return debounced;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Adopt an opaque {@link Handle} (from `defineHandle()`) into this scope's
|
|
514
|
+
* ownership registry. The handle is added to the inherited disposer stack
|
|
515
|
+
* (LIFO disposal) and tracked separately so {@link assertScopeBalance}
|
|
516
|
+
* can detect leaked handles per-scope without depending on global GC.
|
|
517
|
+
*
|
|
518
|
+
* See `./handle.ts` for the brand-+-registry pattern.
|
|
519
|
+
*/
|
|
520
|
+
adoptHandle(handle) {
|
|
521
|
+
adoptHandle(this, handle);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Adopt an `AsyncDisposable` for LIFO teardown.
|
|
525
|
+
*
|
|
526
|
+
* Branded handles (from `defineHandle()`) are routed through
|
|
527
|
+
* {@link adoptHandle} so per-scope accounting catches them.
|
|
528
|
+
* Non-branded `AsyncDisposable` values use the inherited stack directly.
|
|
529
|
+
*
|
|
530
|
+
* This closes the pro/Kimi-flagged "scope.use(handle) bypasses ownership"
|
|
531
|
+
* hole — a forged or hand-rolled `AsyncDisposable` claiming to be a
|
|
532
|
+
* branded handle still goes through the runtime authenticity gate in
|
|
533
|
+
* `adoptHandle`.
|
|
534
|
+
*/
|
|
535
|
+
use(value) {
|
|
536
|
+
if (value !== null && value !== void 0 && typeof value === "object" && isBrandedHandle(value)) {
|
|
537
|
+
adoptHandle(this, value);
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
return super.use(value);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Dispose children first, then the inherited user disposer stack.
|
|
544
|
+
* Collects errors across the tree and surfaces them as `SuppressedError`.
|
|
545
|
+
*
|
|
546
|
+
* When `SILVERY_SCOPE_TRACE=1`, prints a per-scope handle-delta to
|
|
547
|
+
* stderr if any adopted handles remained undisposed after the inherited
|
|
548
|
+
* stack ran (km-silvery.lifecycle-leak-detection Phase 2).
|
|
549
|
+
*/
|
|
550
|
+
async [Symbol.asyncDispose]() {
|
|
551
|
+
if (this.disposed) return;
|
|
552
|
+
const errors = [];
|
|
553
|
+
const preCount = getAdoptedHandles(this).length;
|
|
554
|
+
const children = [...this.#children].reverse();
|
|
555
|
+
this.#children.clear();
|
|
556
|
+
for (const c of children) try {
|
|
557
|
+
await c[Symbol.asyncDispose]();
|
|
558
|
+
} catch (e) {
|
|
559
|
+
errors.push(e);
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
await super[Symbol.asyncDispose]();
|
|
563
|
+
} catch (e) {
|
|
564
|
+
errors.push(e);
|
|
565
|
+
}
|
|
566
|
+
const postCount = getAdoptedHandles(this).length;
|
|
567
|
+
reportScopeDelta(this.name, preCount, postCount);
|
|
568
|
+
if (this.#parent) this.#parent.#children.delete(this);
|
|
569
|
+
_trackDispose(this);
|
|
570
|
+
if (errors.length === 1) throw errors[0];
|
|
571
|
+
if (errors.length > 1) throw errors.reduce((suppressed, e) => new SuppressedError(e, suppressed, "multiple dispose errors"));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* `AsyncDisposableStack.move()` returns a plain stack that loses Scope's
|
|
575
|
+
* `signal`, `name`, and child registry. Throw rather than silently
|
|
576
|
+
* corrupting invariants. Create a new scope and re-register resources
|
|
577
|
+
* explicitly if you need to relocate ownership.
|
|
578
|
+
*/
|
|
579
|
+
move() {
|
|
580
|
+
throw new TypeError("Scope.move() is not supported — create a new scope and re-register resources explicitly");
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
/** Create a root scope. Use `scope.child(name?)` for descendants. */
|
|
584
|
+
function createScope(name) {
|
|
585
|
+
return new Scope(void 0, name);
|
|
586
|
+
}
|
|
587
|
+
const SINK_KEY = Symbol.for("@silvery/scope/disposeErrorSink");
|
|
588
|
+
const defaultSink = (error, context) => {
|
|
589
|
+
const name = context.scope?.name ?? "?";
|
|
590
|
+
console.error(`[scope dispose error] phase=${context.phase} scope=${name}`, error);
|
|
591
|
+
};
|
|
592
|
+
const sinkHost = globalThis;
|
|
593
|
+
if (!sinkHost[SINK_KEY]) sinkHost[SINK_KEY] = { sink: defaultSink };
|
|
594
|
+
/**
|
|
595
|
+
* Report a disposal failure from a fire-and-forget context (React unmount,
|
|
596
|
+
* signal handler, app-exit hook). Best-effort; never throws.
|
|
597
|
+
*/
|
|
598
|
+
function reportDisposeError(error, context) {
|
|
599
|
+
try {
|
|
600
|
+
sinkHost[SINK_KEY].sink(error, context);
|
|
601
|
+
} catch {}
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region packages/ag-term/src/pipeline/collect-text.ts
|
|
605
|
+
/**
|
|
606
|
+
* Collect plain text from a node tree, applying internal_transform.
|
|
607
|
+
*
|
|
608
|
+
* This is the base traversal used by:
|
|
609
|
+
* - measure-phase.ts (fit-content measurement)
|
|
610
|
+
* - render-text.ts (DOM-level truncation budget)
|
|
611
|
+
*
|
|
612
|
+
* Does NOT filter hidden or display:none nodes — those are handled by
|
|
613
|
+
* the layout engine (display:none gets 0x0 size) or by other phases.
|
|
614
|
+
*/
|
|
615
|
+
function collectPlainText(node) {
|
|
616
|
+
if (node.textContent !== void 0) return node.textContent;
|
|
617
|
+
let result = "";
|
|
618
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
619
|
+
const child = node.children[i];
|
|
620
|
+
let childText = collectPlainText(child);
|
|
621
|
+
if (childText.length > 0 && child.props.internal_transform) childText = child.props.internal_transform(childText, i);
|
|
622
|
+
result += childText;
|
|
623
|
+
}
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Collect plain text from a node tree, skipping hidden children.
|
|
628
|
+
*
|
|
629
|
+
* Used by the reconciler's Yoga measure function where hidden nodes
|
|
630
|
+
* (from Suspense) must not contribute to measured size.
|
|
631
|
+
*
|
|
632
|
+
* Identical to collectPlainText except for the hidden check.
|
|
633
|
+
*/
|
|
634
|
+
function collectPlainTextSkipHidden(node) {
|
|
635
|
+
if (node.textContent !== void 0) return node.textContent;
|
|
636
|
+
let result = "";
|
|
637
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
638
|
+
const child = node.children[i];
|
|
639
|
+
if (child.hidden) continue;
|
|
640
|
+
let childText = collectPlainTextSkipHidden(child);
|
|
641
|
+
if (childText.length > 0 && child.props.internal_transform) childText = child.props.internal_transform(childText, i);
|
|
642
|
+
result += childText;
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region packages/ag-term/src/pipeline/measure-stats.ts
|
|
648
|
+
/**
|
|
649
|
+
* Profiling counters for measure function performance analysis (dev only).
|
|
650
|
+
*
|
|
651
|
+
* Shared between @silvery/ag-react/reconciler/nodes (where measure happens)
|
|
652
|
+
* and @silvery/ag-term/pipeline/layout-phase (where stats are logged).
|
|
653
|
+
*
|
|
654
|
+
* Lives in @silvery/ag-term to keep the @silvery/ag-term barrel React-free.
|
|
655
|
+
*/
|
|
656
|
+
const measureStats = {
|
|
657
|
+
calls: 0,
|
|
658
|
+
cacheHits: 0,
|
|
659
|
+
textCollects: 0,
|
|
660
|
+
displayWidthCalls: 0,
|
|
661
|
+
reset() {
|
|
662
|
+
this.calls = 0;
|
|
663
|
+
this.cacheHits = 0;
|
|
664
|
+
this.textCollects = 0;
|
|
665
|
+
this.displayWidthCalls = 0;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region packages/ag-react/src/reconciler/helpers.ts
|
|
670
|
+
/**
|
|
671
|
+
* Reconciler Helper Functions
|
|
672
|
+
*
|
|
673
|
+
* Utility functions for props comparison and change detection
|
|
674
|
+
* used by the React reconciler during updates.
|
|
675
|
+
*/
|
|
676
|
+
/**
|
|
677
|
+
* Set of layout-affecting props.
|
|
678
|
+
*/
|
|
679
|
+
const LAYOUT_PROPS = new Set([
|
|
680
|
+
"width",
|
|
681
|
+
"height",
|
|
682
|
+
"minWidth",
|
|
683
|
+
"minHeight",
|
|
684
|
+
"maxWidth",
|
|
685
|
+
"maxHeight",
|
|
686
|
+
"flexDirection",
|
|
687
|
+
"flexWrap",
|
|
688
|
+
"justifyContent",
|
|
689
|
+
"alignItems",
|
|
690
|
+
"alignContent",
|
|
691
|
+
"alignSelf",
|
|
692
|
+
"flexGrow",
|
|
693
|
+
"flexShrink",
|
|
694
|
+
"flexBasis",
|
|
695
|
+
"padding",
|
|
696
|
+
"paddingX",
|
|
697
|
+
"paddingY",
|
|
698
|
+
"paddingTop",
|
|
699
|
+
"paddingBottom",
|
|
700
|
+
"paddingLeft",
|
|
701
|
+
"paddingRight",
|
|
702
|
+
"margin",
|
|
703
|
+
"marginX",
|
|
704
|
+
"marginY",
|
|
705
|
+
"marginTop",
|
|
706
|
+
"marginBottom",
|
|
707
|
+
"marginLeft",
|
|
708
|
+
"marginRight",
|
|
709
|
+
"gap",
|
|
710
|
+
"columnGap",
|
|
711
|
+
"rowGap",
|
|
712
|
+
"borderStyle",
|
|
713
|
+
"borderTop",
|
|
714
|
+
"borderBottom",
|
|
715
|
+
"borderLeft",
|
|
716
|
+
"borderRight",
|
|
717
|
+
"display",
|
|
718
|
+
"position",
|
|
719
|
+
"top",
|
|
720
|
+
"left",
|
|
721
|
+
"bottom",
|
|
722
|
+
"right",
|
|
723
|
+
"aspectRatio",
|
|
724
|
+
"overflow",
|
|
725
|
+
"overflowX",
|
|
726
|
+
"overflowY",
|
|
727
|
+
"cols",
|
|
728
|
+
"rows"
|
|
729
|
+
]);
|
|
730
|
+
/**
|
|
731
|
+
* Set of content props that affect layout dimensions (trigger contentDirty + Flexily markDirty()).
|
|
732
|
+
* wrap changes text line count; internal_transform changes text width.
|
|
733
|
+
*/
|
|
734
|
+
const TEXT_CONTENT_PROPS = new Set(["wrap", "internal_transform"]);
|
|
735
|
+
/**
|
|
736
|
+
* Set of style props that affect content (paint) but NOT layout dimensions.
|
|
737
|
+
* borderColor, color, bold, etc. don't change how much space a node takes.
|
|
738
|
+
* borderStyle is also a layout prop (affects border widths), but it's included
|
|
739
|
+
* here so stylePropsDirty is set — otherwise border add/remove doesn't trigger
|
|
740
|
+
* renderBox to draw/clear border characters.
|
|
741
|
+
*/
|
|
742
|
+
const STYLE_PROPS = new Set([
|
|
743
|
+
"color",
|
|
744
|
+
"backgroundColor",
|
|
745
|
+
"bold",
|
|
746
|
+
"italic",
|
|
747
|
+
"underline",
|
|
748
|
+
"underlineStyle",
|
|
749
|
+
"underlineColor",
|
|
750
|
+
"overline",
|
|
751
|
+
"strikethrough",
|
|
752
|
+
"inverse",
|
|
753
|
+
"borderColor",
|
|
754
|
+
"borderBackgroundColor",
|
|
755
|
+
"borderTopBackgroundColor",
|
|
756
|
+
"borderBottomBackgroundColor",
|
|
757
|
+
"borderLeftBackgroundColor",
|
|
758
|
+
"borderRightBackgroundColor",
|
|
759
|
+
"borderStyle",
|
|
760
|
+
"outlineStyle",
|
|
761
|
+
"outlineColor",
|
|
762
|
+
"outlineDimColor",
|
|
763
|
+
"outlineTop",
|
|
764
|
+
"outlineBottom",
|
|
765
|
+
"outlineLeft",
|
|
766
|
+
"outlineRight",
|
|
767
|
+
"theme"
|
|
768
|
+
]);
|
|
769
|
+
/** Shared singleton for the no-changes fast path (identity check). */
|
|
770
|
+
const NO_CHANGES = {
|
|
771
|
+
anyChanged: false,
|
|
772
|
+
layoutChanged: false,
|
|
773
|
+
contentChanged: false
|
|
774
|
+
};
|
|
775
|
+
/**
|
|
776
|
+
* Classify all prop changes in a single pass over the union of old and new keys.
|
|
777
|
+
*
|
|
778
|
+
* Replaces the previous 3-pass approach (propsEqual + layoutPropsChanged +
|
|
779
|
+
* contentPropsChanged) with one iteration. Includes an identity fast path
|
|
780
|
+
* and early exit when both layout and content flags are fully determined.
|
|
781
|
+
*/
|
|
782
|
+
function classifyPropChanges(oldProps, newProps) {
|
|
783
|
+
if (oldProps === newProps) return NO_CHANGES;
|
|
784
|
+
const keysA = Object.keys(oldProps);
|
|
785
|
+
const keysB = Object.keys(newProps);
|
|
786
|
+
const sameKeyCount = keysA.length === keysB.length;
|
|
787
|
+
let layoutChanged = false;
|
|
788
|
+
let contentChanged = false;
|
|
789
|
+
let anyChanged = false;
|
|
790
|
+
for (const key of keysA) if (oldProps[key] !== newProps[key]) {
|
|
791
|
+
anyChanged = true;
|
|
792
|
+
if (LAYOUT_PROPS.has(key)) layoutChanged = true;
|
|
793
|
+
if (contentChanged !== "text") {
|
|
794
|
+
if (key === "children") {
|
|
795
|
+
const oldIsPrimitive = typeof oldProps[key] === "string" || typeof oldProps[key] === "number";
|
|
796
|
+
const newIsPrimitive = typeof newProps[key] === "string" || typeof newProps[key] === "number";
|
|
797
|
+
if (oldIsPrimitive || newIsPrimitive) contentChanged = "text";
|
|
798
|
+
} else if (TEXT_CONTENT_PROPS.has(key)) contentChanged = "text";
|
|
799
|
+
else if (contentChanged !== "style" && STYLE_PROPS.has(key)) contentChanged = "style";
|
|
800
|
+
}
|
|
801
|
+
if (layoutChanged && contentChanged === "text") break;
|
|
802
|
+
}
|
|
803
|
+
if (!sameKeyCount) {
|
|
804
|
+
for (const key of keysB) if (!(key in oldProps)) {
|
|
805
|
+
anyChanged = true;
|
|
806
|
+
if (LAYOUT_PROPS.has(key)) layoutChanged = true;
|
|
807
|
+
if (contentChanged !== "text") {
|
|
808
|
+
if (key === "children") {
|
|
809
|
+
if (typeof newProps[key] === "string" || typeof newProps[key] === "number") contentChanged = "text";
|
|
810
|
+
} else if (TEXT_CONTENT_PROPS.has(key)) contentChanged = "text";
|
|
811
|
+
else if (contentChanged !== "style" && STYLE_PROPS.has(key)) contentChanged = "style";
|
|
812
|
+
}
|
|
813
|
+
if (layoutChanged && contentChanged === "text") break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (!anyChanged && !sameKeyCount) anyChanged = true;
|
|
817
|
+
if (!anyChanged) return NO_CHANGES;
|
|
818
|
+
return {
|
|
819
|
+
anyChanged,
|
|
820
|
+
layoutChanged,
|
|
821
|
+
contentChanged
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region packages/ag-react/src/reconciler/nodes.ts
|
|
826
|
+
/**
|
|
827
|
+
* Node Creation and Layout Application
|
|
828
|
+
*
|
|
829
|
+
* Functions for creating SilveryNodes and applying layout properties.
|
|
830
|
+
*/
|
|
831
|
+
const measureLog = createLogger("silvery:measure");
|
|
832
|
+
/**
|
|
833
|
+
* Create a new SilveryNode with a fresh layout node.
|
|
834
|
+
*/
|
|
835
|
+
function createNode(type, props, measurer) {
|
|
836
|
+
const layoutNode = getLayoutEngine().createNode();
|
|
837
|
+
const epoch = getRenderEpoch();
|
|
838
|
+
const node = {
|
|
839
|
+
type,
|
|
840
|
+
props,
|
|
841
|
+
children: [],
|
|
842
|
+
parent: null,
|
|
843
|
+
layoutNode,
|
|
844
|
+
boxRect: null,
|
|
845
|
+
scrollRect: null,
|
|
846
|
+
screenRect: null,
|
|
847
|
+
prevLayout: null,
|
|
848
|
+
prevScrollRect: null,
|
|
849
|
+
prevScreenRect: null,
|
|
850
|
+
layoutChangedThisFrame: epoch,
|
|
851
|
+
dirtyBits: 31,
|
|
852
|
+
dirtyEpoch: epoch
|
|
853
|
+
};
|
|
854
|
+
if (type === "silvery-box") applyBoxProps(layoutNode, props);
|
|
855
|
+
else if (type === "silvery-text") applyTextFlexItemProps(layoutNode, props);
|
|
856
|
+
else if (type === "silvery-viewport") applyViewportProps(layoutNode, props);
|
|
857
|
+
else if (type === "silvery-island") applyIslandProps(layoutNode, props);
|
|
858
|
+
if (type === "silvery-text") {
|
|
859
|
+
let cachedText = null;
|
|
860
|
+
const measureCache = /* @__PURE__ */ new Map();
|
|
861
|
+
layoutNode.setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
862
|
+
measureStats.calls++;
|
|
863
|
+
measureLog.debug?.(`measure "${collectPlainTextSkipHidden(node).slice(0, 40)}" width=${width} widthMode=${widthMode} height=${height} heightMode=${heightMode}`);
|
|
864
|
+
const cacheKey = `${width}|${widthMode}|${height}|${heightMode}`;
|
|
865
|
+
const cached = measureCache.get(cacheKey);
|
|
866
|
+
if (cached && cachedText !== null && !isDirty(node.dirtyBits, node.dirtyEpoch, 1)) {
|
|
867
|
+
measureStats.cacheHits++;
|
|
868
|
+
return cached;
|
|
869
|
+
}
|
|
870
|
+
let text;
|
|
871
|
+
if (cachedText !== null && !isDirty(node.dirtyBits, node.dirtyEpoch, 1)) text = cachedText;
|
|
872
|
+
else {
|
|
873
|
+
measureStats.textCollects++;
|
|
874
|
+
const newText = collectPlainTextSkipHidden(node);
|
|
875
|
+
if (newText !== cachedText) measureCache.clear();
|
|
876
|
+
text = newText;
|
|
877
|
+
cachedText = text;
|
|
878
|
+
node.dirtyBits &= -2;
|
|
879
|
+
}
|
|
880
|
+
if (!text) return {
|
|
881
|
+
width: 0,
|
|
882
|
+
height: 0
|
|
883
|
+
};
|
|
884
|
+
const cachedAfterCollect = measureCache.get(cacheKey);
|
|
885
|
+
if (cachedAfterCollect) {
|
|
886
|
+
measureStats.cacheHits++;
|
|
887
|
+
return cachedAfterCollect;
|
|
888
|
+
}
|
|
889
|
+
const lines = text.split("\n");
|
|
890
|
+
const isMinContentQuery = widthMode === "min-content";
|
|
891
|
+
const maxWidth = widthMode === "undefined" || Number.isNaN(width) ? Number.POSITIVE_INFINITY : isMinContentQuery ? 0 : width;
|
|
892
|
+
const { wrap } = node.props;
|
|
893
|
+
const isTruncate = wrap === "truncate" || wrap === "truncate-start" || wrap === "truncate-middle" || wrap === "truncate-end" || wrap === "clip" || wrap === false;
|
|
894
|
+
const isHardWrap = wrap === "hard";
|
|
895
|
+
const isWrapTruncate = wrap === "wrap-truncate";
|
|
896
|
+
const internalTransform = node.props.internal_transform;
|
|
897
|
+
let totalHeight = 0;
|
|
898
|
+
let actualWidth = 0;
|
|
899
|
+
const dw = measurer ? measurer.displayWidth.bind(measurer) : displayWidth;
|
|
900
|
+
const wt = measurer ? measurer.wrapText.bind(measurer) : wrapText;
|
|
901
|
+
const lh = getActiveLineHeight();
|
|
902
|
+
let renderedLineIndex = 0;
|
|
903
|
+
const widthFor = (line) => {
|
|
904
|
+
if (!internalTransform) return dw(line);
|
|
905
|
+
return dw(internalTransform(line, renderedLineIndex));
|
|
906
|
+
};
|
|
907
|
+
for (const line of lines) {
|
|
908
|
+
measureStats.displayWidthCalls++;
|
|
909
|
+
const lineWidth = dw(line);
|
|
910
|
+
if (isTruncate) {
|
|
911
|
+
totalHeight += lh;
|
|
912
|
+
if (isMinContentQuery) actualWidth = Math.max(actualWidth, lineWidth > 0 ? 1 : 0);
|
|
913
|
+
else actualWidth = Math.max(actualWidth, widthFor(line));
|
|
914
|
+
renderedLineIndex++;
|
|
915
|
+
} else if (isHardWrap) if (isMinContentQuery) {
|
|
916
|
+
totalHeight += lh;
|
|
917
|
+
actualWidth = Math.max(actualWidth, lineWidth > 0 ? 1 : 0);
|
|
918
|
+
renderedLineIndex++;
|
|
919
|
+
} else if (lineWidth <= maxWidth) {
|
|
920
|
+
totalHeight += lh;
|
|
921
|
+
actualWidth = Math.max(actualWidth, widthFor(line));
|
|
922
|
+
renderedLineIndex++;
|
|
923
|
+
} else if (Number.isFinite(maxWidth) && maxWidth > 0) {
|
|
924
|
+
const wrappedCount = Math.ceil(lineWidth / maxWidth);
|
|
925
|
+
totalHeight += wrappedCount * lh;
|
|
926
|
+
actualWidth = Math.max(actualWidth, maxWidth);
|
|
927
|
+
renderedLineIndex += wrappedCount;
|
|
928
|
+
} else {
|
|
929
|
+
totalHeight += lh;
|
|
930
|
+
actualWidth = Math.max(actualWidth, widthFor(line));
|
|
931
|
+
renderedLineIndex++;
|
|
932
|
+
}
|
|
933
|
+
else if (isMinContentQuery) {
|
|
934
|
+
let longestSegment = 0;
|
|
935
|
+
let segmentWidth = 0;
|
|
936
|
+
for (let pos = 0; pos < line.length; pos++) {
|
|
937
|
+
const ch = line[pos];
|
|
938
|
+
if (ch === " " || ch === " " || ch === "-") {
|
|
939
|
+
if (segmentWidth > longestSegment) longestSegment = segmentWidth;
|
|
940
|
+
segmentWidth = 0;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
segmentWidth += dw(ch);
|
|
944
|
+
if (isSoftBreakPoint(ch)) {
|
|
945
|
+
if (segmentWidth > longestSegment) longestSegment = segmentWidth;
|
|
946
|
+
segmentWidth = 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (segmentWidth > longestSegment) longestSegment = segmentWidth;
|
|
950
|
+
totalHeight += lh;
|
|
951
|
+
actualWidth = Math.max(actualWidth, longestSegment);
|
|
952
|
+
renderedLineIndex++;
|
|
953
|
+
} else if (lineWidth <= maxWidth) {
|
|
954
|
+
totalHeight += lh;
|
|
955
|
+
actualWidth = Math.max(actualWidth, widthFor(line));
|
|
956
|
+
renderedLineIndex++;
|
|
957
|
+
} else {
|
|
958
|
+
const wrapped = wt(line, maxWidth, false, true, isWrapTruncate);
|
|
959
|
+
totalHeight += wrapped.length * lh;
|
|
960
|
+
for (const wl of wrapped) {
|
|
961
|
+
actualWidth = Math.max(actualWidth, widthFor(wl));
|
|
962
|
+
renderedLineIndex++;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
let resultHeight = Math.max(lh, totalHeight);
|
|
967
|
+
if (heightMode === "exactly" && Number.isFinite(height)) resultHeight = height;
|
|
968
|
+
else if (heightMode === "at-most" && Number.isFinite(height)) resultHeight = Math.min(resultHeight, height);
|
|
969
|
+
const result = {
|
|
970
|
+
width: isMinContentQuery ? actualWidth : isTruncate && true ? actualWidth : Math.min(actualWidth, maxWidth),
|
|
971
|
+
height: resultHeight
|
|
972
|
+
};
|
|
973
|
+
measureCache.set(cacheKey, result);
|
|
974
|
+
return result;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return node;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Create the root node for the Silvery tree.
|
|
981
|
+
* Root is always column (document flow is top-to-bottom), regardless of
|
|
982
|
+
* flexily's default flexDirection.
|
|
983
|
+
*/
|
|
984
|
+
function createRootNode() {
|
|
985
|
+
const node = createNode("silvery-root", {});
|
|
986
|
+
const c = getConstants();
|
|
987
|
+
node.layoutNode.setFlexDirection(c.FLEX_DIRECTION_COLUMN);
|
|
988
|
+
return node;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Create a virtual text node (for nested text elements).
|
|
992
|
+
* Virtual text nodes don't have layout nodes and don't participate in layout.
|
|
993
|
+
* They're used when Text is nested inside another Text.
|
|
994
|
+
*/
|
|
995
|
+
function createVirtualTextNode(props) {
|
|
996
|
+
return {
|
|
997
|
+
type: "silvery-text",
|
|
998
|
+
props,
|
|
999
|
+
children: [],
|
|
1000
|
+
parent: null,
|
|
1001
|
+
layoutNode: null,
|
|
1002
|
+
boxRect: null,
|
|
1003
|
+
scrollRect: null,
|
|
1004
|
+
screenRect: null,
|
|
1005
|
+
prevLayout: null,
|
|
1006
|
+
prevScrollRect: null,
|
|
1007
|
+
prevScreenRect: null,
|
|
1008
|
+
layoutChangedThisFrame: -1,
|
|
1009
|
+
dirtyBits: 23,
|
|
1010
|
+
dirtyEpoch: getRenderEpoch(),
|
|
1011
|
+
isRawText: false,
|
|
1012
|
+
inlineRects: null
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Apply ViewportProps to a viewport node's layout node.
|
|
1017
|
+
*
|
|
1018
|
+
* A `<Viewport>` is a leaf with fixed `cols`×`rows` — no flex children, no
|
|
1019
|
+
* measure function. We pin width/height from the props so the parent layout
|
|
1020
|
+
* positions the viewport rect deterministically; if the parent rect is
|
|
1021
|
+
* narrower, flexbox + parent overflow="hidden" clips at paint time.
|
|
1022
|
+
*
|
|
1023
|
+
* See bead `@km/silvery/15513-surface-nested-composition-primitive`.
|
|
1024
|
+
*/
|
|
1025
|
+
function applyViewportProps(layoutNode, props, oldProps) {
|
|
1026
|
+
if (props.cols !== void 0) layoutNode.setWidth(props.cols);
|
|
1027
|
+
else if (oldProps?.cols !== void 0) layoutNode.setWidthAuto();
|
|
1028
|
+
if (props.rows !== void 0) layoutNode.setHeight(props.rows);
|
|
1029
|
+
else if (oldProps?.rows !== void 0) layoutNode.setHeightAuto();
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Apply IslandProps to an island node's layout node.
|
|
1033
|
+
*
|
|
1034
|
+
* `<Island>` is a leaf — guest content lives off the AgNode's `islandState`
|
|
1035
|
+
* slot, blitted by the render phase at boxRect. Layout-dim resolution:
|
|
1036
|
+
* explicit `width` / `height` wins; otherwise fall back to `cols` / `rows`.
|
|
1037
|
+
*
|
|
1038
|
+
* See bead `@km/silvery/15646-islands` for the two-phase resize protocol
|
|
1039
|
+
* (guest acks via `IslandSizeOwner` after host writes new dims).
|
|
1040
|
+
*/
|
|
1041
|
+
function applyIslandProps(layoutNode, props, oldProps) {
|
|
1042
|
+
const c = getConstants();
|
|
1043
|
+
const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
|
|
1044
|
+
if (props.width !== void 0) {
|
|
1045
|
+
if (typeof props.width === "string" && props.width.endsWith("%")) layoutNode.setWidthPercent(Number.parseFloat(props.width));
|
|
1046
|
+
else if (typeof props.width === "number") layoutNode.setWidth(props.width);
|
|
1047
|
+
} else if (props.cols !== void 0) layoutNode.setWidth(props.cols);
|
|
1048
|
+
else if (wasRemoved("width") || wasRemoved("cols")) layoutNode.setWidthAuto();
|
|
1049
|
+
if (props.height !== void 0) {
|
|
1050
|
+
if (typeof props.height === "string" && props.height.endsWith("%")) layoutNode.setHeightPercent(Number.parseFloat(props.height));
|
|
1051
|
+
else if (typeof props.height === "number") layoutNode.setHeight(props.height);
|
|
1052
|
+
} else if (props.rows !== void 0) layoutNode.setHeight(props.rows);
|
|
1053
|
+
else if (wasRemoved("height") || wasRemoved("rows")) layoutNode.setHeightAuto();
|
|
1054
|
+
if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
|
|
1055
|
+
else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
|
|
1056
|
+
if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
|
|
1057
|
+
else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
|
|
1058
|
+
if (props.flexBasis !== void 0) {
|
|
1059
|
+
if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
|
|
1060
|
+
else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
|
|
1061
|
+
else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
|
|
1062
|
+
} else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
|
|
1063
|
+
if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1064
|
+
else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
|
|
1065
|
+
else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1066
|
+
if (props.minWidth !== void 0) {
|
|
1067
|
+
if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
|
|
1068
|
+
else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
|
|
1069
|
+
} else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
|
|
1070
|
+
if (props.minHeight !== void 0) {
|
|
1071
|
+
if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
|
|
1072
|
+
else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
|
|
1073
|
+
} else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
|
|
1074
|
+
if (props.maxWidth !== void 0) {
|
|
1075
|
+
if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
|
|
1076
|
+
else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
|
|
1077
|
+
} else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
|
|
1078
|
+
if (props.maxHeight !== void 0) {
|
|
1079
|
+
if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
|
|
1080
|
+
else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
|
|
1081
|
+
} else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Apply TextFlexItemProps to a Text node's layout node.
|
|
1085
|
+
*
|
|
1086
|
+
* Text is a leaf flex item — it accepts the subset of FlexboxProps that
|
|
1087
|
+
* affect how it participates as a flex item (sizing, growth, shrink),
|
|
1088
|
+
* not the props that affect how it lays out children. This is the
|
|
1089
|
+
* canonical CSS escape hatch: use `flexShrink={0}` to keep a Text rigid,
|
|
1090
|
+
* or `minWidth={0}` to let it shrink fully under a tight parent.
|
|
1091
|
+
*
|
|
1092
|
+
* See `TextFlexItemProps` in @silvery/ag/types.
|
|
1093
|
+
*/
|
|
1094
|
+
function applyTextFlexItemProps(layoutNode, props, oldProps) {
|
|
1095
|
+
const c = getConstants();
|
|
1096
|
+
const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
|
|
1097
|
+
if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
|
|
1098
|
+
else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
|
|
1099
|
+
if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
|
|
1100
|
+
else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
|
|
1101
|
+
if (props.flexBasis !== void 0) {
|
|
1102
|
+
if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
|
|
1103
|
+
else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
|
|
1104
|
+
else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
|
|
1105
|
+
} else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
|
|
1106
|
+
if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1107
|
+
else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
|
|
1108
|
+
else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1109
|
+
if (props.minWidth !== void 0) {
|
|
1110
|
+
if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
|
|
1111
|
+
else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
|
|
1112
|
+
} else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
|
|
1113
|
+
if (props.minHeight !== void 0) {
|
|
1114
|
+
if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
|
|
1115
|
+
else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
|
|
1116
|
+
} else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
|
|
1117
|
+
if (props.maxWidth !== void 0) {
|
|
1118
|
+
if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
|
|
1119
|
+
else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
|
|
1120
|
+
} else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
|
|
1121
|
+
if (props.maxHeight !== void 0) {
|
|
1122
|
+
if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
|
|
1123
|
+
else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
|
|
1124
|
+
} else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Parse a `fitWidth` entry from user-facing form (number | string) to the
|
|
1128
|
+
* engine's FitWidthLane shape (number | { value, unit: "cqi" | "cqmin" }).
|
|
1129
|
+
*
|
|
1130
|
+
* Accepted strings: `"100cqi"`, `"50cqmin"` — decimal and integer values both
|
|
1131
|
+
* fine. Numbers pass through unchanged. Invalid strings throw with a clear
|
|
1132
|
+
* pointer to the bug — the layout engine consumes only the parsed form, so
|
|
1133
|
+
* parse-fail at the React seam is the right place.
|
|
1134
|
+
*/
|
|
1135
|
+
function parseFitWidthEntry(entry) {
|
|
1136
|
+
if (typeof entry === "number") return entry;
|
|
1137
|
+
const cqiMatch = entry.match(/^(\d+(?:\.\d+)?)cqi$/);
|
|
1138
|
+
if (cqiMatch) return {
|
|
1139
|
+
value: Number.parseFloat(cqiMatch[1]),
|
|
1140
|
+
unit: "cqi"
|
|
1141
|
+
};
|
|
1142
|
+
const cqminMatch = entry.match(/^(\d+(?:\.\d+)?)cqmin$/);
|
|
1143
|
+
if (cqminMatch) return {
|
|
1144
|
+
value: Number.parseFloat(cqminMatch[1]),
|
|
1145
|
+
unit: "cqmin"
|
|
1146
|
+
};
|
|
1147
|
+
throw new Error(`<Box fitWidth>: invalid lane entry ${JSON.stringify(entry)}. Expected a number (cells) or a string like "100cqi" / "50cqmin".`);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Apply BoxProps to a layout node.
|
|
1151
|
+
* This maps Ink/Silvery props to the layout engine API.
|
|
1152
|
+
*/
|
|
1153
|
+
function applyBoxProps(layoutNode, props, oldProps) {
|
|
1154
|
+
const c = getConstants();
|
|
1155
|
+
const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
|
|
1156
|
+
if (props.width !== void 0) {
|
|
1157
|
+
if (typeof props.width === "string" && props.width.endsWith("%")) layoutNode.setWidthPercent(Number.parseFloat(props.width));
|
|
1158
|
+
else if (typeof props.width === "number") layoutNode.setWidth(props.width);
|
|
1159
|
+
else if (props.width === "auto") layoutNode.setWidthAuto();
|
|
1160
|
+
else if (props.width === "fit-content") layoutNode.setWidthFitContent();
|
|
1161
|
+
else if (props.width === "snug-content") layoutNode.setWidthSnugContent();
|
|
1162
|
+
} else if (wasRemoved("width")) layoutNode.setWidthAuto();
|
|
1163
|
+
if (props.height !== void 0) {
|
|
1164
|
+
if (typeof props.height === "string" && props.height.endsWith("%")) layoutNode.setHeightPercent(Number.parseFloat(props.height));
|
|
1165
|
+
else if (typeof props.height === "number") layoutNode.setHeight(props.height);
|
|
1166
|
+
else if (props.height === "auto") layoutNode.setHeightAuto();
|
|
1167
|
+
} else if (wasRemoved("height")) layoutNode.setHeightAuto();
|
|
1168
|
+
if (props.minWidth !== void 0) {
|
|
1169
|
+
if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
|
|
1170
|
+
else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
|
|
1171
|
+
} else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
|
|
1172
|
+
if (props.minHeight !== void 0) {
|
|
1173
|
+
if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
|
|
1174
|
+
else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
|
|
1175
|
+
} else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
|
|
1176
|
+
if (props.maxWidth !== void 0) {
|
|
1177
|
+
if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
|
|
1178
|
+
else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
|
|
1179
|
+
} else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
|
|
1180
|
+
if (props.maxHeight !== void 0) {
|
|
1181
|
+
if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
|
|
1182
|
+
else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
|
|
1183
|
+
} else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
|
|
1184
|
+
if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
|
|
1185
|
+
else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
|
|
1186
|
+
if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
|
|
1187
|
+
else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
|
|
1188
|
+
if (props.flexBasis !== void 0) {
|
|
1189
|
+
if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
|
|
1190
|
+
else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
|
|
1191
|
+
else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
|
|
1192
|
+
} else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
|
|
1193
|
+
if (props.flexDirection !== void 0) {
|
|
1194
|
+
const directionMap = {
|
|
1195
|
+
row: c.FLEX_DIRECTION_ROW,
|
|
1196
|
+
column: c.FLEX_DIRECTION_COLUMN,
|
|
1197
|
+
"row-reverse": c.FLEX_DIRECTION_ROW_REVERSE,
|
|
1198
|
+
"column-reverse": c.FLEX_DIRECTION_COLUMN_REVERSE
|
|
1199
|
+
};
|
|
1200
|
+
layoutNode.setFlexDirection(directionMap[props.flexDirection] ?? c.FLEX_DIRECTION_ROW);
|
|
1201
|
+
} else if (wasRemoved("flexDirection")) layoutNode.setFlexDirection(c.FLEX_DIRECTION_ROW);
|
|
1202
|
+
if (props.flexWrap !== void 0) {
|
|
1203
|
+
const wrapMap = {
|
|
1204
|
+
nowrap: c.WRAP_NO_WRAP,
|
|
1205
|
+
wrap: c.WRAP_WRAP,
|
|
1206
|
+
"wrap-reverse": c.WRAP_WRAP_REVERSE
|
|
1207
|
+
};
|
|
1208
|
+
layoutNode.setFlexWrap(wrapMap[props.flexWrap] ?? c.WRAP_NO_WRAP);
|
|
1209
|
+
} else if (wasRemoved("flexWrap")) layoutNode.setFlexWrap(c.WRAP_NO_WRAP);
|
|
1210
|
+
if (props.alignItems !== void 0) layoutNode.setAlignItems(alignToConstant(props.alignItems));
|
|
1211
|
+
else if (wasRemoved("alignItems")) layoutNode.setAlignItems(c.ALIGN_STRETCH);
|
|
1212
|
+
if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1213
|
+
else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
|
|
1214
|
+
else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
|
|
1215
|
+
if (props.alignContent !== void 0) layoutNode.setAlignContent(alignToConstant(props.alignContent));
|
|
1216
|
+
else if (wasRemoved("alignContent")) layoutNode.setAlignContent(c.ALIGN_FLEX_START);
|
|
1217
|
+
if (props.justifyContent !== void 0) layoutNode.setJustifyContent(justifyToConstant(props.justifyContent));
|
|
1218
|
+
else if (wasRemoved("justifyContent")) layoutNode.setJustifyContent(c.JUSTIFY_FLEX_START);
|
|
1219
|
+
applySpacing(layoutNode, "padding", props);
|
|
1220
|
+
applySpacing(layoutNode, "margin", props);
|
|
1221
|
+
if (props.gap !== void 0) layoutNode.setGap(c.GUTTER_ALL, props.gap);
|
|
1222
|
+
else if (wasRemoved("gap")) layoutNode.setGap(c.GUTTER_ALL, 0);
|
|
1223
|
+
if (props.columnGap !== void 0) layoutNode.setGap(c.GUTTER_COLUMN, props.columnGap);
|
|
1224
|
+
else if (wasRemoved("columnGap")) layoutNode.setGap(c.GUTTER_COLUMN, 0);
|
|
1225
|
+
if (props.rowGap !== void 0) layoutNode.setGap(c.GUTTER_ROW, props.rowGap);
|
|
1226
|
+
else if (wasRemoved("rowGap")) layoutNode.setGap(c.GUTTER_ROW, 0);
|
|
1227
|
+
if (props.display !== void 0) layoutNode.setDisplay(props.display === "none" ? c.DISPLAY_NONE : c.DISPLAY_FLEX);
|
|
1228
|
+
else if (wasRemoved("display")) layoutNode.setDisplay(c.DISPLAY_FLEX);
|
|
1229
|
+
if (props.position !== void 0) if (props.position === "absolute") layoutNode.setPositionType(c.POSITION_TYPE_ABSOLUTE);
|
|
1230
|
+
else if (props.position === "static") layoutNode.setPositionType(c.POSITION_TYPE_STATIC);
|
|
1231
|
+
else layoutNode.setPositionType(c.POSITION_TYPE_RELATIVE);
|
|
1232
|
+
else if (wasRemoved("position")) layoutNode.setPositionType(c.POSITION_TYPE_RELATIVE);
|
|
1233
|
+
if (props.position !== "static") {
|
|
1234
|
+
applyPositionOffset(layoutNode, c.EDGE_TOP, props.top);
|
|
1235
|
+
applyPositionOffset(layoutNode, c.EDGE_LEFT, props.left);
|
|
1236
|
+
applyPositionOffset(layoutNode, c.EDGE_BOTTOM, props.bottom);
|
|
1237
|
+
applyPositionOffset(layoutNode, c.EDGE_RIGHT, props.right);
|
|
1238
|
+
}
|
|
1239
|
+
if (props.aspectRatio !== void 0) layoutNode.setAspectRatio(props.aspectRatio);
|
|
1240
|
+
else if (wasRemoved("aspectRatio")) layoutNode.setAspectRatio(NaN);
|
|
1241
|
+
const effectiveOverflow = props.overflow ?? (props.overflowX === "hidden" || props.overflowY === "hidden" ? "hidden" : void 0);
|
|
1242
|
+
if (effectiveOverflow !== void 0) if (effectiveOverflow === "hidden") layoutNode.setOverflow(c.OVERFLOW_HIDDEN);
|
|
1243
|
+
else if (effectiveOverflow === "scroll") layoutNode.setOverflow(c.OVERFLOW_HIDDEN);
|
|
1244
|
+
else layoutNode.setOverflow(c.OVERFLOW_VISIBLE);
|
|
1245
|
+
else if (wasRemoved("overflow") || wasRemoved("overflowX") || wasRemoved("overflowY")) layoutNode.setOverflow(c.OVERFLOW_VISIBLE);
|
|
1246
|
+
if (props.containerType !== void 0) {
|
|
1247
|
+
requireCapability("containerQueries", "<Box containerType>");
|
|
1248
|
+
layoutNode.setContainerType(props.containerType === "inline-size" ? 1 : 0);
|
|
1249
|
+
} else if (wasRemoved("containerType")) layoutNode.setContainerType(0);
|
|
1250
|
+
if (props.containSize !== void 0) {
|
|
1251
|
+
requireCapability("containSize", "<Box containSize>");
|
|
1252
|
+
layoutNode.setContainSize(props.containSize);
|
|
1253
|
+
} else if (wasRemoved("containSize")) layoutNode.setContainSize(false);
|
|
1254
|
+
if (props.containerQueries !== void 0) requireCapability("containerQueries", "<Box containerQueries>");
|
|
1255
|
+
if (props.fitWidth !== void 0) {
|
|
1256
|
+
requireCapability("fitWidth", "<Box fitWidth>");
|
|
1257
|
+
layoutNode.setFitWidth(props.fitWidth.map(parseFitWidthEntry));
|
|
1258
|
+
} else if (wasRemoved("fitWidth")) layoutNode.setFitWidth(void 0);
|
|
1259
|
+
if (props.borderStyle) {
|
|
1260
|
+
const borderWidth = getActiveLineHeight() > 1 ? 0 : 1;
|
|
1261
|
+
if (props.borderTop !== false) layoutNode.setBorder(c.EDGE_TOP, borderWidth);
|
|
1262
|
+
else layoutNode.setBorder(c.EDGE_TOP, 0);
|
|
1263
|
+
if (props.borderBottom !== false) layoutNode.setBorder(c.EDGE_BOTTOM, borderWidth);
|
|
1264
|
+
else layoutNode.setBorder(c.EDGE_BOTTOM, 0);
|
|
1265
|
+
if (props.borderLeft !== false) layoutNode.setBorder(c.EDGE_LEFT, borderWidth);
|
|
1266
|
+
else layoutNode.setBorder(c.EDGE_LEFT, 0);
|
|
1267
|
+
if (props.borderRight !== false) layoutNode.setBorder(c.EDGE_RIGHT, borderWidth);
|
|
1268
|
+
else layoutNode.setBorder(c.EDGE_RIGHT, 0);
|
|
1269
|
+
} else {
|
|
1270
|
+
layoutNode.setBorder(c.EDGE_TOP, 0);
|
|
1271
|
+
layoutNode.setBorder(c.EDGE_BOTTOM, 0);
|
|
1272
|
+
layoutNode.setBorder(c.EDGE_LEFT, 0);
|
|
1273
|
+
layoutNode.setBorder(c.EDGE_RIGHT, 0);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Apply padding or margin to a layout node.
|
|
1278
|
+
*/
|
|
1279
|
+
function applySpacing(layoutNode, type, props) {
|
|
1280
|
+
const c = getConstants();
|
|
1281
|
+
const set = type === "padding" ? layoutNode.setPadding.bind(layoutNode) : layoutNode.setMargin.bind(layoutNode);
|
|
1282
|
+
const all = props[type];
|
|
1283
|
+
const x = props[`${type}X`];
|
|
1284
|
+
const yy = props[`${type}Y`];
|
|
1285
|
+
const top = props[`${type}Top`];
|
|
1286
|
+
const bottom = props[`${type}Bottom`];
|
|
1287
|
+
const left = props[`${type}Left`];
|
|
1288
|
+
const right = props[`${type}Right`];
|
|
1289
|
+
set(c.EDGE_TOP, top ?? yy ?? all ?? 0);
|
|
1290
|
+
set(c.EDGE_BOTTOM, bottom ?? yy ?? all ?? 0);
|
|
1291
|
+
set(c.EDGE_LEFT, left ?? x ?? all ?? 0);
|
|
1292
|
+
set(c.EDGE_RIGHT, right ?? x ?? all ?? 0);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Apply a position offset (top/left/bottom/right) to a layout node.
|
|
1296
|
+
* Supports both numeric (absolute) and percentage string values.
|
|
1297
|
+
*/
|
|
1298
|
+
function applyPositionOffset(layoutNode, edge, value) {
|
|
1299
|
+
if (value === void 0) {
|
|
1300
|
+
layoutNode.setPosition(edge, NaN);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof value === "string" && value.endsWith("%")) layoutNode.setPositionPercent(edge, Number.parseFloat(value));
|
|
1304
|
+
else if (typeof value === "number") layoutNode.setPosition(edge, value);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Convert align value to layout constant.
|
|
1308
|
+
*/
|
|
1309
|
+
function alignToConstant(align) {
|
|
1310
|
+
const c = getConstants();
|
|
1311
|
+
return {
|
|
1312
|
+
"flex-start": c.ALIGN_FLEX_START,
|
|
1313
|
+
"flex-end": c.ALIGN_FLEX_END,
|
|
1314
|
+
center: c.ALIGN_CENTER,
|
|
1315
|
+
stretch: c.ALIGN_STRETCH,
|
|
1316
|
+
baseline: c.ALIGN_BASELINE,
|
|
1317
|
+
"space-between": c.ALIGN_SPACE_BETWEEN,
|
|
1318
|
+
"space-around": c.ALIGN_SPACE_AROUND,
|
|
1319
|
+
"space-evenly": c.ALIGN_SPACE_EVENLY
|
|
1320
|
+
}[align] ?? c.ALIGN_STRETCH;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Convert justify value to layout constant.
|
|
1324
|
+
*/
|
|
1325
|
+
function justifyToConstant(justify) {
|
|
1326
|
+
const c = getConstants();
|
|
1327
|
+
return {
|
|
1328
|
+
"flex-start": c.JUSTIFY_FLEX_START,
|
|
1329
|
+
"flex-end": c.JUSTIFY_FLEX_END,
|
|
1330
|
+
center: c.JUSTIFY_CENTER,
|
|
1331
|
+
"space-between": c.JUSTIFY_SPACE_BETWEEN,
|
|
1332
|
+
"space-around": c.JUSTIFY_SPACE_AROUND,
|
|
1333
|
+
"space-evenly": c.JUSTIFY_SPACE_EVENLY
|
|
1334
|
+
}[justify] ?? c.JUSTIFY_FLEX_START;
|
|
1335
|
+
}
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region packages/ag-react/src/reconciler/host-config.ts
|
|
1338
|
+
/**
|
|
1339
|
+
* React Reconciler Host Config
|
|
1340
|
+
*
|
|
1341
|
+
* Defines how React creates, updates, and manages SilveryNodes.
|
|
1342
|
+
* This is the bridge between React's reconciliation algorithm
|
|
1343
|
+
* and our custom terminal node tree.
|
|
1344
|
+
*/
|
|
1345
|
+
init_src();
|
|
1346
|
+
const log = createLogger("silvery:reconciler");
|
|
1347
|
+
const mountLog = createLogger("silvery:mount");
|
|
1348
|
+
const DEBUG_PROP_NAMES = [
|
|
1349
|
+
"id",
|
|
1350
|
+
"testID",
|
|
1351
|
+
"data-component",
|
|
1352
|
+
"display",
|
|
1353
|
+
"position",
|
|
1354
|
+
"flexDirection",
|
|
1355
|
+
"width",
|
|
1356
|
+
"height",
|
|
1357
|
+
"minWidth",
|
|
1358
|
+
"minHeight",
|
|
1359
|
+
"flexGrow",
|
|
1360
|
+
"flexShrink",
|
|
1361
|
+
"overflow",
|
|
1362
|
+
"overflowX",
|
|
1363
|
+
"overflowY",
|
|
1364
|
+
"overflowIndicator",
|
|
1365
|
+
"scrollTo",
|
|
1366
|
+
"scrollOffset",
|
|
1367
|
+
"scrollbar",
|
|
1368
|
+
"scrollbarVisibility",
|
|
1369
|
+
"follow",
|
|
1370
|
+
"onWheel",
|
|
1371
|
+
"onClick",
|
|
1372
|
+
"onMouseDown",
|
|
1373
|
+
"onMouseUp",
|
|
1374
|
+
"onMouseMove"
|
|
1375
|
+
];
|
|
1376
|
+
function hostTypeLabel(type) {
|
|
1377
|
+
if (type === "silvery-box") return "Box";
|
|
1378
|
+
if (type === "silvery-text") return "Text";
|
|
1379
|
+
if (type === "silvery-viewport") return "Viewport";
|
|
1380
|
+
if (type === "silvery-island") return "Island";
|
|
1381
|
+
return type;
|
|
1382
|
+
}
|
|
1383
|
+
function getDebugComponentName(node) {
|
|
1384
|
+
const props = node.props;
|
|
1385
|
+
const explicitName = props["data-component"];
|
|
1386
|
+
if (typeof explicitName === "string" && explicitName.length > 0) return explicitName;
|
|
1387
|
+
const base = hostTypeLabel(node.type);
|
|
1388
|
+
const testID = props.testID;
|
|
1389
|
+
if (typeof testID === "string" && testID.length > 0) return `${base}#${testID}`;
|
|
1390
|
+
const id = props.id;
|
|
1391
|
+
if (typeof id === "string" && id.length > 0) return `${base}#${id}`;
|
|
1392
|
+
return base;
|
|
1393
|
+
}
|
|
1394
|
+
function summarizeDebugProp(value) {
|
|
1395
|
+
if (typeof value === "function") return true;
|
|
1396
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
1397
|
+
}
|
|
1398
|
+
function summarizeDebugProps(props) {
|
|
1399
|
+
const summary = {};
|
|
1400
|
+
for (const key of DEBUG_PROP_NAMES) {
|
|
1401
|
+
const value = summarizeDebugProp(props[key]);
|
|
1402
|
+
if (value !== void 0) summary[key] = value;
|
|
1403
|
+
}
|
|
1404
|
+
for (const key of Object.keys(props)) {
|
|
1405
|
+
if (!key.startsWith("data-") || key === "data-component") continue;
|
|
1406
|
+
const value = summarizeDebugProp(props[key]);
|
|
1407
|
+
if (value !== void 0) summary[key] = value;
|
|
1408
|
+
}
|
|
1409
|
+
return summary;
|
|
1410
|
+
}
|
|
1411
|
+
function logNodeLifecycle(event, node) {
|
|
1412
|
+
const props = node.props;
|
|
1413
|
+
const component = getDebugComponentName(node);
|
|
1414
|
+
const text = node.textContent;
|
|
1415
|
+
mountLog.debug?.(`${event} ${component}`, {
|
|
1416
|
+
event,
|
|
1417
|
+
component,
|
|
1418
|
+
type: node.type,
|
|
1419
|
+
props: summarizeDebugProps(props),
|
|
1420
|
+
propKeys: Object.keys(props).filter((key) => key !== "children").sort(),
|
|
1421
|
+
...text ? { text: text.length > 80 ? `${text.slice(0, 77)}...` : text } : {}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
function logUnmountSubtree(node) {
|
|
1425
|
+
logNodeLifecycle("unmount", node);
|
|
1426
|
+
for (const child of node.children) logUnmountSubtree(child);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Normalize Ink intrinsic element types to Silvery equivalents.
|
|
1430
|
+
* Ink uses `ink-box` / `ink-text` as intrinsic element names;
|
|
1431
|
+
* Silvery uses `silvery-box` / `silvery-text`.
|
|
1432
|
+
*/
|
|
1433
|
+
function normalizeNodeType(type) {
|
|
1434
|
+
if (type === "ink-box") return "silvery-box";
|
|
1435
|
+
if (type === "ink-text") return "silvery-text";
|
|
1436
|
+
return type;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Callback invoked when a node is removed from the tree.
|
|
1440
|
+
* Used by the app layer to coordinate focus cleanup — if the focused element
|
|
1441
|
+
* is within a removed subtree, focus must be cleared to prevent dangling
|
|
1442
|
+
* references and broken navigation (indexOf → -1, hasFocusWithin lies).
|
|
1443
|
+
*/
|
|
1444
|
+
let onNodeRemovedCallback = null;
|
|
1445
|
+
/**
|
|
1446
|
+
* Register a callback to be called when any node is removed from the tree.
|
|
1447
|
+
* Returns a cleanup function to unregister. Only one callback at a time.
|
|
1448
|
+
*/
|
|
1449
|
+
function setOnNodeRemoved(callback) {
|
|
1450
|
+
onNodeRemovedCallback = callback;
|
|
1451
|
+
}
|
|
1452
|
+
const NODE_SCOPES_KEY = Symbol.for("@silvery/ag-react/reconciler/nodeScopes");
|
|
1453
|
+
const nodeScopes = globalThis[NODE_SCOPES_KEY] ?? (globalThis[NODE_SCOPES_KEY] = /* @__PURE__ */ new WeakMap());
|
|
1454
|
+
/**
|
|
1455
|
+
* Dispose any scope attached to `node` and to every descendant. Called
|
|
1456
|
+
* from the reconciler's unmount paths. Fire-and-forget per the design
|
|
1457
|
+
* contract: react commit is synchronous, scope dispose is async, so we
|
|
1458
|
+
* kick off the promise and route rejections through `reportDisposeError`.
|
|
1459
|
+
*
|
|
1460
|
+
* Walks the subtree synchronously so all slots are detached *before* any
|
|
1461
|
+
* dispose promise resolves — this prevents a re-entrant render from
|
|
1462
|
+
* observing a partially torn-down tree with live scope slots.
|
|
1463
|
+
*/
|
|
1464
|
+
function disposeSubtreeScopes(node) {
|
|
1465
|
+
const scope = nodeScopes.get(node);
|
|
1466
|
+
if (scope) {
|
|
1467
|
+
nodeScopes.delete(node);
|
|
1468
|
+
scope[Symbol.asyncDispose]().catch((error) => reportDisposeError(error, {
|
|
1469
|
+
phase: "react-unmount",
|
|
1470
|
+
scope
|
|
1471
|
+
}));
|
|
1472
|
+
}
|
|
1473
|
+
for (const child of node.children) disposeSubtreeScopes(child);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Mark this node and all ancestors as having dirty content/layout.
|
|
1477
|
+
* Used to enable fast-path subtree skipping in renderPhase.
|
|
1478
|
+
*/
|
|
1479
|
+
function markSubtreeDirty(node) {
|
|
1480
|
+
const epoch = getRenderEpoch();
|
|
1481
|
+
while (node && !isDirty(node.dirtyBits, node.dirtyEpoch, 16)) {
|
|
1482
|
+
if (node.dirtyEpoch !== epoch) {
|
|
1483
|
+
node.dirtyBits = 16;
|
|
1484
|
+
node.dirtyEpoch = epoch;
|
|
1485
|
+
} else node.dirtyBits |= 16;
|
|
1486
|
+
node = node.parent;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* When a child change (append/remove/insert/text-update) occurs inside a
|
|
1491
|
+
* virtual text subtree (no layoutNode), the nearest layout ancestor must be
|
|
1492
|
+
* notified so its measure function re-collects descendant text and the layout
|
|
1493
|
+
* engine recalculates dimensions. Without this, the measure cache stays stale
|
|
1494
|
+
* and renderPhase renders at the wrong size / doesn't clear old content.
|
|
1495
|
+
*
|
|
1496
|
+
* No-op when the node already has a layoutNode (normal path handles it).
|
|
1497
|
+
*/
|
|
1498
|
+
function markLayoutAncestorDirty(node) {
|
|
1499
|
+
if (node.layoutNode) return;
|
|
1500
|
+
let ancestor = node.parent;
|
|
1501
|
+
while (ancestor && !ancestor.layoutNode) ancestor = ancestor.parent;
|
|
1502
|
+
if (ancestor?.layoutNode) {
|
|
1503
|
+
const epoch = getRenderEpoch();
|
|
1504
|
+
if (ancestor.dirtyEpoch !== epoch) {
|
|
1505
|
+
ancestor.dirtyBits = 3;
|
|
1506
|
+
ancestor.dirtyEpoch = epoch;
|
|
1507
|
+
} else ancestor.dirtyBits |= 3;
|
|
1508
|
+
ancestor.layoutNode.markDirty();
|
|
1509
|
+
trackContentDirty(ancestor);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const BOX_INSIDE_TEXT_WARNING_ID = "silvery/ag-react:box-in-text";
|
|
1513
|
+
let currentUpdatePriority = NoEventPriority;
|
|
1514
|
+
/**
|
|
1515
|
+
* Run a callback with DiscreteEventPriority so React treats state
|
|
1516
|
+
* updates inside it as user-interaction priority (synchronous commit).
|
|
1517
|
+
* Use this for keyboard input handling to prevent React's concurrent
|
|
1518
|
+
* scheduler from deferring the commit.
|
|
1519
|
+
*/
|
|
1520
|
+
function runWithDiscreteEvent(fn) {
|
|
1521
|
+
const prev = currentUpdatePriority;
|
|
1522
|
+
currentUpdatePriority = DiscreteEventPriority;
|
|
1523
|
+
try {
|
|
1524
|
+
fn();
|
|
1525
|
+
} finally {
|
|
1526
|
+
currentUpdatePriority = prev;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* The React Reconciler host config.
|
|
1531
|
+
* This defines how React creates, updates, and manages our custom SilveryNodes.
|
|
1532
|
+
*/
|
|
1533
|
+
const hostConfig = {
|
|
1534
|
+
rendererPackageName: "@silvery/ag-react",
|
|
1535
|
+
rendererVersion: "0.0.1",
|
|
1536
|
+
supportsMutation: true,
|
|
1537
|
+
supportsPersistence: false,
|
|
1538
|
+
supportsHydration: false,
|
|
1539
|
+
isPrimaryRenderer: true,
|
|
1540
|
+
scheduleTimeout: setTimeout,
|
|
1541
|
+
cancelTimeout: clearTimeout,
|
|
1542
|
+
noTimeout: -1,
|
|
1543
|
+
supportsMicrotasks: true,
|
|
1544
|
+
scheduleMicrotask: queueMicrotask,
|
|
1545
|
+
getRootHostContext() {
|
|
1546
|
+
return { isInsideText: false };
|
|
1547
|
+
},
|
|
1548
|
+
getChildHostContext(parentHostContext, type) {
|
|
1549
|
+
const normalizedType = normalizeNodeType(type);
|
|
1550
|
+
const isInsideText = parentHostContext.isInsideText || normalizedType === "silvery-text";
|
|
1551
|
+
if (isInsideText === parentHostContext.isInsideText) return parentHostContext;
|
|
1552
|
+
return { isInsideText };
|
|
1553
|
+
},
|
|
1554
|
+
createInstance(type, props, _rootContainer, hostContext) {
|
|
1555
|
+
type = normalizeNodeType(type);
|
|
1556
|
+
if ("style" in props && props.style && typeof props.style === "object") props = {
|
|
1557
|
+
...props.style,
|
|
1558
|
+
...props
|
|
1559
|
+
};
|
|
1560
|
+
if (type === "silvery-box" && hostContext.isInsideText) {
|
|
1561
|
+
if (process.env.NODE_ENV !== "production") warnOnce(BOX_INSIDE_TEXT_WARNING_ID, () => log.warn?.("<Box> cannot be nested inside <Text>. This produces undefined layout behavior."));
|
|
1562
|
+
}
|
|
1563
|
+
if (type === "silvery-text" && hostContext.isInsideText) {
|
|
1564
|
+
const node = createVirtualTextNode(props);
|
|
1565
|
+
logNodeLifecycle("mount", node);
|
|
1566
|
+
return node;
|
|
1567
|
+
}
|
|
1568
|
+
const node = createNode(type, props);
|
|
1569
|
+
logNodeLifecycle("mount", node);
|
|
1570
|
+
return node;
|
|
1571
|
+
},
|
|
1572
|
+
createTextInstance(text, _rootContainer, hostContext) {
|
|
1573
|
+
const epoch = getRenderEpoch();
|
|
1574
|
+
const node = {
|
|
1575
|
+
type: "silvery-text",
|
|
1576
|
+
props: { children: text },
|
|
1577
|
+
children: [],
|
|
1578
|
+
parent: null,
|
|
1579
|
+
layoutNode: null,
|
|
1580
|
+
boxRect: null,
|
|
1581
|
+
scrollRect: null,
|
|
1582
|
+
screenRect: null,
|
|
1583
|
+
prevLayout: null,
|
|
1584
|
+
prevScrollRect: null,
|
|
1585
|
+
prevScreenRect: null,
|
|
1586
|
+
layoutChangedThisFrame: -1,
|
|
1587
|
+
dirtyBits: 23,
|
|
1588
|
+
dirtyEpoch: epoch,
|
|
1589
|
+
textContent: text,
|
|
1590
|
+
isRawText: true
|
|
1591
|
+
};
|
|
1592
|
+
logNodeLifecycle("mount", node);
|
|
1593
|
+
return node;
|
|
1594
|
+
},
|
|
1595
|
+
appendChild(parentInstance, child) {
|
|
1596
|
+
const existingIndex = parentInstance.children.indexOf(child);
|
|
1597
|
+
if (existingIndex !== -1) {
|
|
1598
|
+
parentInstance.children.splice(existingIndex, 1);
|
|
1599
|
+
if (parentInstance.layoutNode && child.layoutNode) parentInstance.layoutNode.removeChild(child.layoutNode);
|
|
1600
|
+
}
|
|
1601
|
+
child.parent = parentInstance;
|
|
1602
|
+
parentInstance.children.push(child);
|
|
1603
|
+
if (parentInstance.layoutNode && child.layoutNode) {
|
|
1604
|
+
const layoutIndex = parentInstance.children.filter((c) => c.layoutNode !== null).length - 1;
|
|
1605
|
+
parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
|
|
1606
|
+
}
|
|
1607
|
+
{
|
|
1608
|
+
const epoch = getRenderEpoch();
|
|
1609
|
+
const bits = 9;
|
|
1610
|
+
parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
|
|
1611
|
+
parentInstance.dirtyEpoch = epoch;
|
|
1612
|
+
}
|
|
1613
|
+
parentInstance.layoutNode?.markDirty();
|
|
1614
|
+
trackContentDirty(parentInstance);
|
|
1615
|
+
markLayoutAncestorDirty(parentInstance);
|
|
1616
|
+
markSubtreeDirty(parentInstance);
|
|
1617
|
+
},
|
|
1618
|
+
appendInitialChild(parentInstance, child) {
|
|
1619
|
+
child.parent = parentInstance;
|
|
1620
|
+
parentInstance.children.push(child);
|
|
1621
|
+
if (parentInstance.layoutNode && child.layoutNode) {
|
|
1622
|
+
const layoutIndex = parentInstance.children.filter((c) => c.layoutNode !== null).length - 1;
|
|
1623
|
+
parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
appendChildToContainer(container, child) {
|
|
1627
|
+
const existingIndex = container.root.children.indexOf(child);
|
|
1628
|
+
if (existingIndex !== -1) {
|
|
1629
|
+
container.root.children.splice(existingIndex, 1);
|
|
1630
|
+
if (container.root.layoutNode && child.layoutNode) container.root.layoutNode.removeChild(child.layoutNode);
|
|
1631
|
+
}
|
|
1632
|
+
child.parent = container.root;
|
|
1633
|
+
container.root.children.push(child);
|
|
1634
|
+
if (container.root.layoutNode && child.layoutNode) {
|
|
1635
|
+
const layoutIndex = container.root.children.filter((c) => c.layoutNode !== null).length - 1;
|
|
1636
|
+
container.root.layoutNode.insertChild(child.layoutNode, layoutIndex);
|
|
1637
|
+
}
|
|
1638
|
+
{
|
|
1639
|
+
const epoch = getRenderEpoch();
|
|
1640
|
+
const bits = 9;
|
|
1641
|
+
container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
|
|
1642
|
+
container.root.dirtyEpoch = epoch;
|
|
1643
|
+
}
|
|
1644
|
+
container.root.layoutNode?.markDirty();
|
|
1645
|
+
trackContentDirty(container.root);
|
|
1646
|
+
markSubtreeDirty(container.root);
|
|
1647
|
+
},
|
|
1648
|
+
removeChild(parentInstance, child) {
|
|
1649
|
+
const index = parentInstance.children.indexOf(child);
|
|
1650
|
+
if (index !== -1) {
|
|
1651
|
+
onNodeRemovedCallback?.(child);
|
|
1652
|
+
logUnmountSubtree(child);
|
|
1653
|
+
disposeSubtreeScopes(child);
|
|
1654
|
+
parentInstance.children.splice(index, 1);
|
|
1655
|
+
if (parentInstance.layoutNode && child.layoutNode) {
|
|
1656
|
+
parentInstance.layoutNode.removeChild(child.layoutNode);
|
|
1657
|
+
child.layoutNode.free();
|
|
1658
|
+
}
|
|
1659
|
+
child.parent = null;
|
|
1660
|
+
{
|
|
1661
|
+
const epoch = getRenderEpoch();
|
|
1662
|
+
const bits = 9;
|
|
1663
|
+
parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
|
|
1664
|
+
parentInstance.dirtyEpoch = epoch;
|
|
1665
|
+
}
|
|
1666
|
+
parentInstance.layoutNode?.markDirty();
|
|
1667
|
+
trackContentDirty(parentInstance);
|
|
1668
|
+
markLayoutAncestorDirty(parentInstance);
|
|
1669
|
+
markSubtreeDirty(parentInstance);
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
removeChildFromContainer(container, child) {
|
|
1673
|
+
const index = container.root.children.indexOf(child);
|
|
1674
|
+
if (index !== -1) {
|
|
1675
|
+
onNodeRemovedCallback?.(child);
|
|
1676
|
+
logUnmountSubtree(child);
|
|
1677
|
+
disposeSubtreeScopes(child);
|
|
1678
|
+
container.root.children.splice(index, 1);
|
|
1679
|
+
if (container.root.layoutNode && child.layoutNode) {
|
|
1680
|
+
container.root.layoutNode.removeChild(child.layoutNode);
|
|
1681
|
+
child.layoutNode.free();
|
|
1682
|
+
}
|
|
1683
|
+
child.parent = null;
|
|
1684
|
+
{
|
|
1685
|
+
const epoch = getRenderEpoch();
|
|
1686
|
+
const bits = 9;
|
|
1687
|
+
container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
|
|
1688
|
+
container.root.dirtyEpoch = epoch;
|
|
1689
|
+
}
|
|
1690
|
+
container.root.layoutNode?.markDirty();
|
|
1691
|
+
trackContentDirty(container.root);
|
|
1692
|
+
markSubtreeDirty(container.root);
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
insertBefore(parentInstance, child, beforeChild) {
|
|
1696
|
+
const existingIndex = parentInstance.children.indexOf(child);
|
|
1697
|
+
if (existingIndex !== -1) {
|
|
1698
|
+
parentInstance.children.splice(existingIndex, 1);
|
|
1699
|
+
if (parentInstance.layoutNode && child.layoutNode) parentInstance.layoutNode.removeChild(child.layoutNode);
|
|
1700
|
+
}
|
|
1701
|
+
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
|
1702
|
+
if (beforeIndex !== -1) {
|
|
1703
|
+
child.parent = parentInstance;
|
|
1704
|
+
parentInstance.children.splice(beforeIndex, 0, child);
|
|
1705
|
+
if (parentInstance.layoutNode && child.layoutNode) {
|
|
1706
|
+
const layoutIndex = parentInstance.children.slice(0, beforeIndex).filter((c) => c.layoutNode !== null).length;
|
|
1707
|
+
parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
|
|
1708
|
+
}
|
|
1709
|
+
{
|
|
1710
|
+
const epoch = getRenderEpoch();
|
|
1711
|
+
const bits = 9;
|
|
1712
|
+
parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
|
|
1713
|
+
parentInstance.dirtyEpoch = epoch;
|
|
1714
|
+
}
|
|
1715
|
+
parentInstance.layoutNode?.markDirty();
|
|
1716
|
+
trackContentDirty(parentInstance);
|
|
1717
|
+
markLayoutAncestorDirty(parentInstance);
|
|
1718
|
+
markSubtreeDirty(parentInstance);
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
insertInContainerBefore(container, child, beforeChild) {
|
|
1722
|
+
const existingIndex = container.root.children.indexOf(child);
|
|
1723
|
+
if (existingIndex !== -1) {
|
|
1724
|
+
container.root.children.splice(existingIndex, 1);
|
|
1725
|
+
if (container.root.layoutNode && child.layoutNode) container.root.layoutNode.removeChild(child.layoutNode);
|
|
1726
|
+
}
|
|
1727
|
+
const beforeIndex = container.root.children.indexOf(beforeChild);
|
|
1728
|
+
if (beforeIndex !== -1) {
|
|
1729
|
+
child.parent = container.root;
|
|
1730
|
+
container.root.children.splice(beforeIndex, 0, child);
|
|
1731
|
+
if (container.root.layoutNode && child.layoutNode) {
|
|
1732
|
+
const layoutIndex = container.root.children.slice(0, beforeIndex).filter((c) => c.layoutNode !== null).length;
|
|
1733
|
+
container.root.layoutNode.insertChild(child.layoutNode, layoutIndex);
|
|
1734
|
+
}
|
|
1735
|
+
{
|
|
1736
|
+
const epoch = getRenderEpoch();
|
|
1737
|
+
const bits = 9;
|
|
1738
|
+
container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
|
|
1739
|
+
container.root.dirtyEpoch = epoch;
|
|
1740
|
+
}
|
|
1741
|
+
container.root.layoutNode?.markDirty();
|
|
1742
|
+
trackContentDirty(container.root);
|
|
1743
|
+
markSubtreeDirty(container.root);
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
prepareUpdate(_instance, _type, oldProps, newProps) {
|
|
1747
|
+
return classifyPropChanges(oldProps, newProps).anyChanged;
|
|
1748
|
+
},
|
|
1749
|
+
commitUpdate(instance, _type, oldProps, newProps, _finishedWork) {
|
|
1750
|
+
if ("style" in oldProps && oldProps.style && typeof oldProps.style === "object") oldProps = {
|
|
1751
|
+
...oldProps.style,
|
|
1752
|
+
...oldProps
|
|
1753
|
+
};
|
|
1754
|
+
if ("style" in newProps && newProps.style && typeof newProps.style === "object") newProps = {
|
|
1755
|
+
...newProps.style,
|
|
1756
|
+
...newProps
|
|
1757
|
+
};
|
|
1758
|
+
const { anyChanged, layoutChanged, contentChanged } = classifyPropChanges(oldProps, newProps);
|
|
1759
|
+
if (!anyChanged) {
|
|
1760
|
+
instance.props = newProps;
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (layoutChanged) {
|
|
1764
|
+
if (instance.layoutNode) {
|
|
1765
|
+
if (instance.type === "silvery-text") applyTextFlexItemProps(instance.layoutNode, newProps, oldProps);
|
|
1766
|
+
else if (instance.type === "silvery-viewport") applyViewportProps(instance.layoutNode, newProps, oldProps);
|
|
1767
|
+
else if (instance.type === "silvery-island") applyIslandProps(instance.layoutNode, newProps, oldProps);
|
|
1768
|
+
else applyBoxProps(instance.layoutNode, newProps, oldProps);
|
|
1769
|
+
instance.layoutNode.markDirty();
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (contentChanged) {
|
|
1773
|
+
const epoch = getRenderEpoch();
|
|
1774
|
+
let bits = 2;
|
|
1775
|
+
if (contentChanged === "text") {
|
|
1776
|
+
bits |= 1;
|
|
1777
|
+
if (instance.layoutNode) instance.layoutNode.markDirty();
|
|
1778
|
+
}
|
|
1779
|
+
if (oldProps.backgroundColor !== newProps.backgroundColor) bits |= 4;
|
|
1780
|
+
if (oldProps.borderStyle && !newProps.borderStyle) bits |= 4;
|
|
1781
|
+
if (oldProps.theme !== newProps.theme) bits |= 5;
|
|
1782
|
+
instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
|
|
1783
|
+
instance.dirtyEpoch = epoch;
|
|
1784
|
+
}
|
|
1785
|
+
if (contentChanged) trackContentDirty(instance);
|
|
1786
|
+
if (contentChanged === "style" && !layoutChanged && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 4) && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 1) && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 8)) trackStyleOnlyDirty(instance);
|
|
1787
|
+
instance.props = newProps;
|
|
1788
|
+
logNodeLifecycle("update", instance);
|
|
1789
|
+
const scrollToChanged = oldProps.scrollTo !== newProps.scrollTo;
|
|
1790
|
+
const scrollOffsetChanged = oldProps.scrollOffset !== newProps.scrollOffset;
|
|
1791
|
+
if (scrollToChanged || scrollOffsetChanged) trackScrollDirty(instance);
|
|
1792
|
+
if (layoutChanged || contentChanged || scrollToChanged || scrollOffsetChanged) {
|
|
1793
|
+
markLayoutAncestorDirty(instance);
|
|
1794
|
+
markSubtreeDirty(instance);
|
|
1795
|
+
}
|
|
1796
|
+
},
|
|
1797
|
+
commitTextUpdate(textInstance, _oldText, newText) {
|
|
1798
|
+
textInstance.textContent = newText;
|
|
1799
|
+
syncTextContentSignal(textInstance);
|
|
1800
|
+
textInstance.props = { children: newText };
|
|
1801
|
+
const epoch = getRenderEpoch();
|
|
1802
|
+
const bits = 3;
|
|
1803
|
+
textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
|
|
1804
|
+
textInstance.dirtyEpoch = epoch;
|
|
1805
|
+
trackContentDirty(textInstance);
|
|
1806
|
+
markLayoutAncestorDirty(textInstance);
|
|
1807
|
+
markSubtreeDirty(textInstance);
|
|
1808
|
+
},
|
|
1809
|
+
finalizeInitialChildren() {
|
|
1810
|
+
return false;
|
|
1811
|
+
},
|
|
1812
|
+
prepareForCommit() {
|
|
1813
|
+
return null;
|
|
1814
|
+
},
|
|
1815
|
+
resetAfterCommit(container) {
|
|
1816
|
+
const budgetEnv = typeof process !== "undefined" ? process.env.SILVERY_RENDER_BUDGET_MS : void 0;
|
|
1817
|
+
const budget = budgetEnv !== void 0 ? Number(budgetEnv) : 0;
|
|
1818
|
+
if (budget > 0 && Number.isFinite(budget)) {
|
|
1819
|
+
const start = performance.now();
|
|
1820
|
+
container.onRender();
|
|
1821
|
+
const elapsed = performance.now() - start;
|
|
1822
|
+
if (elapsed > budget) console.warn(`[silvery] render commit exceeded budget: ${elapsed.toFixed(0)}ms > ${budget}ms — see @km/board/projection-stability-improvements for cascade-prevention guidance`);
|
|
1823
|
+
} else container.onRender();
|
|
1824
|
+
},
|
|
1825
|
+
getPublicInstance(instance) {
|
|
1826
|
+
return instance;
|
|
1827
|
+
},
|
|
1828
|
+
shouldSetTextContent() {
|
|
1829
|
+
return false;
|
|
1830
|
+
},
|
|
1831
|
+
clearContainer(container) {
|
|
1832
|
+
for (const child of container.root.children) {
|
|
1833
|
+
onNodeRemovedCallback?.(child);
|
|
1834
|
+
logUnmountSubtree(child);
|
|
1835
|
+
}
|
|
1836
|
+
disposeSubtreeScopes(container.root);
|
|
1837
|
+
for (const child of container.root.children) if (container.root.layoutNode && child.layoutNode) {
|
|
1838
|
+
container.root.layoutNode.removeChild(child.layoutNode);
|
|
1839
|
+
child.layoutNode.free();
|
|
1840
|
+
}
|
|
1841
|
+
container.root.children = [];
|
|
1842
|
+
{
|
|
1843
|
+
const epoch = getRenderEpoch();
|
|
1844
|
+
const bits = 9;
|
|
1845
|
+
container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
|
|
1846
|
+
container.root.dirtyEpoch = epoch;
|
|
1847
|
+
}
|
|
1848
|
+
container.root.layoutNode?.markDirty();
|
|
1849
|
+
trackContentDirty(container.root);
|
|
1850
|
+
markSubtreeDirty(container.root);
|
|
1851
|
+
},
|
|
1852
|
+
preparePortalMount() {},
|
|
1853
|
+
getCurrentEventPriority() {
|
|
1854
|
+
if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
|
|
1855
|
+
return DefaultEventPriority;
|
|
1856
|
+
},
|
|
1857
|
+
getInstanceFromNode() {
|
|
1858
|
+
return null;
|
|
1859
|
+
},
|
|
1860
|
+
beforeActiveInstanceBlur() {},
|
|
1861
|
+
afterActiveInstanceBlur() {},
|
|
1862
|
+
prepareScopeUpdate() {},
|
|
1863
|
+
getInstanceFromScope() {
|
|
1864
|
+
return null;
|
|
1865
|
+
},
|
|
1866
|
+
detachDeletedInstance(node) {
|
|
1867
|
+
disposeSubtreeScopes(node);
|
|
1868
|
+
},
|
|
1869
|
+
setCurrentUpdatePriority(newPriority) {
|
|
1870
|
+
currentUpdatePriority = newPriority;
|
|
1871
|
+
},
|
|
1872
|
+
getCurrentUpdatePriority() {
|
|
1873
|
+
return currentUpdatePriority;
|
|
1874
|
+
},
|
|
1875
|
+
resolveUpdatePriority() {
|
|
1876
|
+
if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
|
|
1877
|
+
return DefaultEventPriority;
|
|
1878
|
+
},
|
|
1879
|
+
maySuspendCommit() {
|
|
1880
|
+
return false;
|
|
1881
|
+
},
|
|
1882
|
+
NotPendingTransition: null,
|
|
1883
|
+
HostTransitionContext: createContext(null),
|
|
1884
|
+
resetFormInstance() {},
|
|
1885
|
+
requestPostPaintCallback() {},
|
|
1886
|
+
shouldAttemptEagerTransition() {
|
|
1887
|
+
return false;
|
|
1888
|
+
},
|
|
1889
|
+
trackSchedulerEvent() {},
|
|
1890
|
+
resolveEventType() {
|
|
1891
|
+
return null;
|
|
1892
|
+
},
|
|
1893
|
+
resolveEventTimeStamp() {
|
|
1894
|
+
return -1.1;
|
|
1895
|
+
},
|
|
1896
|
+
preloadInstance() {
|
|
1897
|
+
return true;
|
|
1898
|
+
},
|
|
1899
|
+
startSuspendingCommit() {},
|
|
1900
|
+
suspendInstance() {},
|
|
1901
|
+
waitForCommitToBeReady() {
|
|
1902
|
+
return null;
|
|
1903
|
+
},
|
|
1904
|
+
hideInstance(instance) {
|
|
1905
|
+
instance.hidden = true;
|
|
1906
|
+
const epoch = getRenderEpoch();
|
|
1907
|
+
const bits = 3;
|
|
1908
|
+
instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
|
|
1909
|
+
instance.dirtyEpoch = epoch;
|
|
1910
|
+
if (instance.layoutNode) instance.layoutNode.markDirty();
|
|
1911
|
+
trackContentDirty(instance);
|
|
1912
|
+
if (instance.parent) {
|
|
1913
|
+
if (instance.parent.dirtyEpoch !== epoch) {
|
|
1914
|
+
instance.parent.dirtyBits = 1;
|
|
1915
|
+
instance.parent.dirtyEpoch = epoch;
|
|
1916
|
+
} else instance.parent.dirtyBits |= 1;
|
|
1917
|
+
trackContentDirty(instance.parent);
|
|
1918
|
+
}
|
|
1919
|
+
markLayoutAncestorDirty(instance);
|
|
1920
|
+
markSubtreeDirty(instance);
|
|
1921
|
+
},
|
|
1922
|
+
unhideInstance(instance, _props) {
|
|
1923
|
+
instance.hidden = false;
|
|
1924
|
+
const epoch = getRenderEpoch();
|
|
1925
|
+
const bits = 3;
|
|
1926
|
+
instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
|
|
1927
|
+
instance.dirtyEpoch = epoch;
|
|
1928
|
+
if (instance.layoutNode) instance.layoutNode.markDirty();
|
|
1929
|
+
trackContentDirty(instance);
|
|
1930
|
+
if (instance.parent) {
|
|
1931
|
+
if (instance.parent.dirtyEpoch !== epoch) {
|
|
1932
|
+
instance.parent.dirtyBits = 1;
|
|
1933
|
+
instance.parent.dirtyEpoch = epoch;
|
|
1934
|
+
} else instance.parent.dirtyBits |= 1;
|
|
1935
|
+
trackContentDirty(instance.parent);
|
|
1936
|
+
}
|
|
1937
|
+
markLayoutAncestorDirty(instance);
|
|
1938
|
+
markSubtreeDirty(instance);
|
|
1939
|
+
},
|
|
1940
|
+
hideTextInstance(textInstance) {
|
|
1941
|
+
textInstance.hidden = true;
|
|
1942
|
+
const epoch = getRenderEpoch();
|
|
1943
|
+
const bits = 3;
|
|
1944
|
+
textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
|
|
1945
|
+
textInstance.dirtyEpoch = epoch;
|
|
1946
|
+
trackContentDirty(textInstance);
|
|
1947
|
+
if (textInstance.parent) {
|
|
1948
|
+
if (textInstance.parent.dirtyEpoch !== epoch) {
|
|
1949
|
+
textInstance.parent.dirtyBits = 1;
|
|
1950
|
+
textInstance.parent.dirtyEpoch = epoch;
|
|
1951
|
+
} else textInstance.parent.dirtyBits |= 1;
|
|
1952
|
+
trackContentDirty(textInstance.parent);
|
|
1953
|
+
}
|
|
1954
|
+
markLayoutAncestorDirty(textInstance);
|
|
1955
|
+
markSubtreeDirty(textInstance);
|
|
1956
|
+
},
|
|
1957
|
+
unhideTextInstance(textInstance, _text) {
|
|
1958
|
+
textInstance.hidden = false;
|
|
1959
|
+
const epoch = getRenderEpoch();
|
|
1960
|
+
const bits = 3;
|
|
1961
|
+
textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
|
|
1962
|
+
textInstance.dirtyEpoch = epoch;
|
|
1963
|
+
trackContentDirty(textInstance);
|
|
1964
|
+
if (textInstance.parent) {
|
|
1965
|
+
if (textInstance.parent.dirtyEpoch !== epoch) {
|
|
1966
|
+
textInstance.parent.dirtyBits = 1;
|
|
1967
|
+
textInstance.parent.dirtyEpoch = epoch;
|
|
1968
|
+
} else textInstance.parent.dirtyBits |= 1;
|
|
1969
|
+
trackContentDirty(textInstance.parent);
|
|
1970
|
+
}
|
|
1971
|
+
markLayoutAncestorDirty(textInstance);
|
|
1972
|
+
markSubtreeDirty(textInstance);
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region packages/ag-react/src/reconciler/index.ts
|
|
1977
|
+
/**
|
|
1978
|
+
* Silvery React Reconciler
|
|
1979
|
+
*
|
|
1980
|
+
* Custom React reconciler that builds a tree of SilveryNodes, each with a Yoga layout node.
|
|
1981
|
+
* This is the core of Silvery's architecture - separating structure (React reconciliation)
|
|
1982
|
+
* from content (terminal rendering).
|
|
1983
|
+
*
|
|
1984
|
+
* The reconciler creates SilveryNodes during React's reconciliation phase,
|
|
1985
|
+
* but actual terminal content is rendered later after Yoga computes layout.
|
|
1986
|
+
*/
|
|
1987
|
+
/**
|
|
1988
|
+
* Create the React reconciler instance.
|
|
1989
|
+
*/
|
|
1990
|
+
const reconciler = Reconciler(hostConfig);
|
|
1991
|
+
/**
|
|
1992
|
+
* Create a container for rendering.
|
|
1993
|
+
*/
|
|
1994
|
+
function createContainer(onRender) {
|
|
1995
|
+
return {
|
|
1996
|
+
root: createRootNode(),
|
|
1997
|
+
onRender
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Create a React fiber root for a container (wraps the 10-argument reconciler call).
|
|
2002
|
+
*
|
|
2003
|
+
* Pass `options.onUncaughtError` to route React-thrown render errors back to
|
|
2004
|
+
* the host's panic path. Without it, render errors are silently swallowed
|
|
2005
|
+
* (the default `() => {}` callback) and the only surface for them is whatever
|
|
2006
|
+
* the host's `console.error` capture decides to do — typically an altscreen
|
|
2007
|
+
* overlay the user can't copy-paste or screenshot reliably.
|
|
2008
|
+
*/
|
|
2009
|
+
function createFiberRoot(container, options = {}) {
|
|
2010
|
+
return reconciler.createContainer(container, 1, null, false, null, "", options.onUncaughtError ?? (() => {}), options.onCaughtError ?? (() => {}), options.onRecoverableError ?? (() => {}), null);
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Get the root SilveryNode from a container.
|
|
2014
|
+
*/
|
|
2015
|
+
function getContainerRoot(container) {
|
|
2016
|
+
return container.root;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Synchronously unmount a fiber root and scrub the container so it can't
|
|
2020
|
+
* keep its closure-captured RenderInstance alive afterward.
|
|
2021
|
+
*
|
|
2022
|
+
* Why both steps are needed:
|
|
2023
|
+
*
|
|
2024
|
+
* 1. `createFiberRoot` uses `ConcurrentRoot` (mode 1). React's async
|
|
2025
|
+
* `updateContainer(null, fiberRoot, ...)` does NOT run layout-effect
|
|
2026
|
+
* cleanups before returning — useLayoutEffect / useBoxRect /
|
|
2027
|
+
* useBoxMetrics / signal-effect disposers are scheduled but may not
|
|
2028
|
+
* fire promptly. That keeps signal subscriptions alive past unmount,
|
|
2029
|
+
* which keeps the React tree reachable, which keeps the host
|
|
2030
|
+
* `RenderInstance` reachable. `updateContainerSync` + `flushSyncWork`
|
|
2031
|
+
* forces all cleanups to run inline.
|
|
2032
|
+
*
|
|
2033
|
+
* 2. Even after the React tree is detached, the `FiberRoot` keeps a
|
|
2034
|
+
* pointer to its `containerInfo` (our `Container`) for some time, and
|
|
2035
|
+
* `Container.onRender` typically closes over the entire enclosing
|
|
2036
|
+
* `RenderInstance`. Without nulling `onRender` and scrubbing the root
|
|
2037
|
+
* AgNode, the instance graph is still reachable through the FiberRoot's
|
|
2038
|
+
* container pointer.
|
|
2039
|
+
*
|
|
2040
|
+
* Call this in every unmount path that uses ConcurrentRoot. The previous
|
|
2041
|
+
* (async) pattern leaked across mount/unmount cycles in tests and likely
|
|
2042
|
+
* in production long-lived host applications too.
|
|
2043
|
+
*
|
|
2044
|
+
* Safe to call multiple times — `releaseContainer` is idempotent (the
|
|
2045
|
+
* scrub fields are nulled and `layoutNode.free()` is best-effort).
|
|
2046
|
+
*
|
|
2047
|
+
* @param fiberRoot — opaque React FiberRoot returned by `createFiberRoot`
|
|
2048
|
+
* @param container — the `Container` paired with that fiberRoot
|
|
2049
|
+
*/
|
|
2050
|
+
function unmountFiberRoot(fiberRoot, container) {
|
|
2051
|
+
reconciler.updateContainerSync(null, fiberRoot, null, null);
|
|
2052
|
+
reconciler.flushSyncWork();
|
|
2053
|
+
releaseContainer(container);
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Scrub a Container so it can't retain its enclosing render state after
|
|
2057
|
+
* the React tree has been unmounted. See {@link unmountFiberRoot} for the
|
|
2058
|
+
* full rationale; call this directly only if you've already run a sync
|
|
2059
|
+
* unmount through the reconciler and just need the post-commit scrub.
|
|
2060
|
+
*/
|
|
2061
|
+
function releaseContainer(container) {
|
|
2062
|
+
disposeSubtreeScopes(container.root);
|
|
2063
|
+
container.onRender = () => {};
|
|
2064
|
+
const root = container.root;
|
|
2065
|
+
root.children = [];
|
|
2066
|
+
root.parent = null;
|
|
2067
|
+
root.boxRect = null;
|
|
2068
|
+
root.scrollRect = null;
|
|
2069
|
+
root.screenRect = null;
|
|
2070
|
+
root.prevLayout = null;
|
|
2071
|
+
root.prevScrollRect = null;
|
|
2072
|
+
root.prevScreenRect = null;
|
|
2073
|
+
if (root.layoutNode) {
|
|
2074
|
+
try {
|
|
2075
|
+
root.layoutNode.free();
|
|
2076
|
+
} catch {}
|
|
2077
|
+
root.layoutNode = null;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
//#endregion
|
|
2081
|
+
export { trackContentDirty as S, isAnyDirty as _, unmountFiberRoot as a, clearDirtyTracking as b, setOnNodeRemoved as c, createScope as d, reportDisposeError as f, getRenderEpoch as g, advanceRenderEpoch as h, reconciler as i, measureStats as l, finaliseHandle as m, createFiberRoot as n, hostConfig as o, defineHandle as p, getContainerRoot as r, runWithDiscreteEvent as s, createContainer as t, collectPlainText as u, isCurrentEpoch as v, hasScrollDirty as x, isDirty as y };
|
|
2082
|
+
|
|
2083
|
+
//# sourceMappingURL=reconciler-DldIJB93.mjs.map
|