@solidrt/core 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.28"
30
+ "@solidrt/flux-types": "0.0.29"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@solidjs/signals": "2.0.0-beta.17",
package/src/renderer.ts CHANGED
@@ -88,6 +88,46 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
88
88
  }
89
89
  }
90
90
 
91
+ // ------ Leak sentinel (dev only) --------
92
+
93
+ // A node created but never inserted is unreachable by the remove -> destroy
94
+ // sweep, so it leaks permanently, natively and in the maps here. The usual
95
+ // cause is an element-valued prop read more than once: every read builds a
96
+ // fresh subtree and only the mounted one is ever freed. Rather than
97
+ // bookkeeping on the hot create/insert paths, orphans are derived from the
98
+ // proxy map itself: parentless, not the window root, and not awaiting the
99
+ // destroy sweep. window.ts runs the scan on a rendered frame every few
100
+ // seconds; dev bundles only (srt always defines process.env.NODE_ENV, so a
101
+ // production bundle folds the check into a constant early return).
102
+ const SENTINEL_INTERVAL_MS = 5000
103
+ let sentinelDue = 0
104
+ let warnedLeakTypes = new Set<string>()
105
+
106
+ export function scanForOrphans(now: number): void {
107
+ if (process.env.NODE_ENV === "production") return
108
+ if (now < sentinelDue) return
109
+ sentinelDue = now + SENTINEL_INTERVAL_MS
110
+ let counts = new Map<string, number>()
111
+ let total = 0
112
+ for (let node of nodes.values()) {
113
+ if (node.parent !== undefined || node.elementType === "window" || pendingDestroy.has(node.id)) continue
114
+ total += 1
115
+ counts.set(node.elementType, (counts.get(node.elementType) ?? 0) + 1)
116
+ }
117
+ if (total === 0) return
118
+ let fresh = [...counts].filter(([type]) => !warnedLeakTypes.has(type))
119
+ if (fresh.length === 0) return
120
+ for (let [type] of fresh) warnedLeakTypes.add(type)
121
+ let list = fresh.map(([type, n]) => `<${type}> x${n}`).join(", ")
122
+ console.warn(
123
+ `Leak sentinel: ${total} nodes are unreachable and will never be freed: ${list}. ` +
124
+ `The usual cause is reading an element-valued prop more than once (every read ` +
125
+ `builds a new subtree); read it once where it mounts, or resolve it with ` +
126
+ `children(). If these nodes are intentionally kept for later mounting, ignore ` +
127
+ `this. Element types already reported are not reported again.`,
128
+ )
129
+ }
130
+
91
131
  // A property the native tree rejected must not take down the reactive system:
92
132
  // a typo'd or not-yet-implemented prop poisons only itself. Warn once per
93
133
  // element kind + property with a stack (the dev server remaps its frames to
package/src/window.ts CHANGED
@@ -3,6 +3,7 @@ import { requestFrame } from "flux:rendertree"
3
3
  import { renderFrame } from "srt:render"
4
4
  import { on, once } from "srt:events"
5
5
  import { getEventHandler, getFocusedNodeId, setFocus } from "./core"
6
+ import { scanForOrphans } from "./renderer"
6
7
 
7
8
  // ------ Pointer capture -----------------
8
9
 
@@ -212,6 +213,7 @@ export function attachWindow(_nodeId: number) {
212
213
  for (let fn of frames.values()) fn(t, frame, refreshRate)
213
214
  }
214
215
  flush()
216
+ scanForOrphans(t)
215
217
  renderFrame()
216
218
  }
217
219