@zakkster/lite-observe 1.0.3 → 1.1.1
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/CHANGELOG.md +58 -0
- package/README.md +17 -1
- package/llms.txt +11 -2
- package/package.json +1 -1
- package/src/Resize.js +81 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1 -- Late mixed-box joiner reads the last delivered size, not a stale zero
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A late `observeResize` mixed-box joiner no longer reads 0 until the next
|
|
8
|
+
resize (B-4).** 1.1.0's lazy box-pair mint was correct for cost but wrong for
|
|
9
|
+
value: when a second consumer asked for the other box on an element already
|
|
10
|
+
under observation, its pair was minted fresh at `signal(0)` and nothing wrote
|
|
11
|
+
it until the element next resized -- on a settled layout, potentially never, so
|
|
12
|
+
a border-box reader mounted into a stable layout saw a zero-size element with
|
|
13
|
+
no error and no warning. `drainResize` now parks the last delivered box
|
|
14
|
+
dimensions on the slot as four primitives (`lastCW`/`lastCH`/`lastBW`/`lastBH`)
|
|
15
|
+
-- numbers only, no browser object retained, no allocation added to the hot
|
|
16
|
+
path -- and a lazily minted pair is seeded from them (`=== null ? 0 : x`, so a
|
|
17
|
+
parked 0 survives and a never-delivered box still reads 0). Before any delivery
|
|
18
|
+
the parked fields are null and a freshly minted pair reads 0, unchanged. The
|
|
19
|
+
slot literal births the four fields as `null` for hidden-class stability. No
|
|
20
|
+
API change; the drain path stays zero-allocation (Phase A `maxMajor=0` holds)
|
|
21
|
+
and retention stays 0 per cycle.
|
|
22
|
+
|
|
23
|
+
## 1.1.0 -- Lazy resize box-pair allocation + pooled-node census docs
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`observeResize` allocates only the box pair the first caller asks for
|
|
28
|
+
(B-2).** Previously every `observeResize` slot minted four signal nodes
|
|
29
|
+
(content width/height + border width/height) regardless of the `box`
|
|
30
|
+
requested. It now mints only the requested pair -- 2 nodes in the common
|
|
31
|
+
case -- and lazily adds the other pair only when a mixed-box consumer asks
|
|
32
|
+
for it on the same element (then 4). Shared-slot refcounting and mixed-box
|
|
33
|
+
cooperation are unchanged: a content consumer and a border consumer on one
|
|
34
|
+
element still share a single underlying `ResizeObserver` observation and
|
|
35
|
+
each reads its own pair. `drainResize` null-guards each pair write and stays
|
|
36
|
+
zero-allocation; `dispose` frees only the pairs that were minted. The slot
|
|
37
|
+
literal keeps all four fields present at birth (initialized `null`) so the
|
|
38
|
+
hot-path hidden class stays stable. Backward-compatible: the handle surface
|
|
39
|
+
and the shared-observation contract are unchanged; only the pooled-node
|
|
40
|
+
footprint of a single-box consumer drops from 4 to 2.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- **Pooled-node cost documented per entry point (B-2).** README and `llms.txt`
|
|
45
|
+
now state the steady-state (while-observed) pooled signal-node cost next to
|
|
46
|
+
every entry point: `observeResize` 2 (4 mixed-box), `observeIntersection` 2
|
|
47
|
+
per (element, options-key), `observeMutation` 1, `observeMutationSelector` 4
|
|
48
|
+
(added + removed + inner tick + effect), `observeMedia` 1 per query string,
|
|
49
|
+
`documentVisible` 1 module-scope. All are freed on final dispose. A new
|
|
50
|
+
"Pooled-registry census" note in Design notes explains that a consumer
|
|
51
|
+
sizing a capped registry via `createRegistry({ maxNodes, prealloc,
|
|
52
|
+
onCapacityExceeded })` must sum these counts over concurrently observed
|
|
53
|
+
elements/keys/queries; guessing low throws `CapacityError` from inside the
|
|
54
|
+
library.
|
|
55
|
+
- **`observeMedia` documented as the reactive replacement for one-shot
|
|
56
|
+
`matchMedia` reads.** A README line notes that
|
|
57
|
+
`observeMedia('(prefers-reduced-motion: reduce)')` (and similar) is the
|
|
58
|
+
reactive answer to hand-rolled `matchMedia` reads that never update when the
|
|
59
|
+
OS setting changes mid-session.
|
|
60
|
+
|
|
3
61
|
## 1.0.3 -- Batched paired writes + peer range bump
|
|
4
62
|
|
|
5
63
|
### Changed
|
package/README.md
CHANGED
|
@@ -146,7 +146,9 @@ interface ResizeHandle {
|
|
|
146
146
|
|
|
147
147
|
Width and height are independent fine-grained signals. N consumers of the same element share one underlying `ResizeObserver.observe()` call; the element is unobserved when the last handle is disposed.
|
|
148
148
|
|
|
149
|
-
`options.box` selects which box the `width` / `height` signals surface: `'content'` (default) mirrors `entry.contentRect`; `'border'` mirrors `entry.borderBoxSize` (includes padding + border, matching `getBoundingClientRect()`). Mixed-box consumers of the *same* element still share a single underlying observation -- one `ResizeObserver` fires both, and each handle reads the pair it asked for. On legacy runtimes without `borderBoxSize`, a border-box reader sees content-box values until the next layout.
|
|
149
|
+
`options.box` selects which box the `width` / `height` signals surface: `'content'` (default) mirrors `entry.contentRect`; `'border'` mirrors `entry.borderBoxSize` (includes padding + border, matching `getBoundingClientRect()`). Mixed-box consumers of the *same* element still share a single underlying observation -- one `ResizeObserver` fires both, and each handle reads the pair it asked for. A consumer that joins an already-observed element after a delivery has happened is seeded from that element's last delivered size for its box (the slot parks those numbers), so it reads the real size at once rather than a zero that would persist until the next resize; before any delivery has arrived, a freshly minted pair reads 0. On legacy runtimes without `borderBoxSize`, a border-box reader sees content-box values until the next layout.
|
|
150
|
+
|
|
151
|
+
Pooled signal nodes: 2 while observed (the requested box pair; 4 if both content- and border-box consumers observe the same element). Freed on last dispose.
|
|
150
152
|
|
|
151
153
|
### `observeIntersection(element, options?)` -> `IntersectionHandle`
|
|
152
154
|
|
|
@@ -160,6 +162,8 @@ interface IntersectionHandle {
|
|
|
160
162
|
|
|
161
163
|
Equivalent options share one underlying observer. Equivalence is `(root by reference, rootMargin by string, threshold by value/order)`. Different options spawn distinct observers, per spec.
|
|
162
164
|
|
|
165
|
+
Pooled signal nodes: 2 per (element, options-key) while observed (`isIntersecting` + `ratio`). Freed on last dispose.
|
|
166
|
+
|
|
163
167
|
### `observeMutation(element, options)` -> `MutationHandle`
|
|
164
168
|
|
|
165
169
|
```ts
|
|
@@ -174,6 +178,8 @@ Mutations are events, not state: surfacing the latest `MutationRecord[]` as a si
|
|
|
174
178
|
|
|
175
179
|
Consumers asking for the same element with the same option set share one observer; differing options spawn distinct observers.
|
|
176
180
|
|
|
181
|
+
Pooled signal nodes: 1 while observed (the `tick`). Freed on last dispose.
|
|
182
|
+
|
|
177
183
|
### `observeMutationSelector(root, selector, options?)` -> `MutationSelectorHandle`
|
|
178
184
|
|
|
179
185
|
```ts
|
|
@@ -192,14 +198,22 @@ With `subtree: true`, the walk also runs `querySelectorAll(selector)` against ad
|
|
|
192
198
|
|
|
193
199
|
**Allocation note.** `added` / `removed` arrays are allocated fresh per batch that has at least one matching mutation, so consumers may safely retain a reference for later inspection. Batches with zero matches return the same frozen empty array (no allocation). This is the one deliberate departure from strict library-side zero-GC, made for consumer-ergonomics; the underlying `observeMutation` dispatch it builds on stays zero-alloc.
|
|
194
200
|
|
|
201
|
+
Pooled signal nodes: 4 while observed (`added` + `removed`, plus the inner `observeMutation` tick and one effect). Freed on last dispose.
|
|
202
|
+
|
|
195
203
|
### `observeMedia(query)` -> `Signal<boolean>`
|
|
196
204
|
|
|
197
205
|
Returns a boolean signal that reflects the live match state of the given media query. The same query string returns the same signal node across calls. The underlying `change` listener is attached lazily on first read (via lite-signal's observer-lifecycle hook) and detached when no consumer is reading -- an imported-but-unread query costs only the signal node and a Map entry.
|
|
198
206
|
|
|
207
|
+
`observeMedia(query)` is the reactive replacement for hand-rolled one-shot `matchMedia('(prefers-reduced-motion: reduce)')` reads, which never update when the OS setting changes mid-session -- likely the single most common reason to reach for this package.
|
|
208
|
+
|
|
209
|
+
Pooled signal nodes: 1, shared per query string. Freed on last dispose.
|
|
210
|
+
|
|
199
211
|
### `documentVisible: Signal<boolean>`
|
|
200
212
|
|
|
201
213
|
Module-level signal. True iff `document.visibilityState === 'visible'`. Listener attached lazily on first read, detached on last unsubscribe.
|
|
202
214
|
|
|
215
|
+
Pooled signal nodes: 1, module-scope (lives for the module's lifetime).
|
|
216
|
+
|
|
203
217
|
## Design notes
|
|
204
218
|
|
|
205
219
|
**Fine-grained, not coarse.** A coarse-grained API would return a single `rect` signal carrying `{ width, height }`. That makes any read wake on any change, defeating the point. The fine-grained shape leans on lite-signal's `Object.is` equality gate to halt propagation at unchanged sources.
|
|
@@ -212,6 +226,8 @@ Module-level signal. True iff `document.visibilityState === 'visible'`. Listener
|
|
|
212
226
|
|
|
213
227
|
**Zero-GC steady state.** Library-side allocations happen at registration; the callback path uses indexed `for` loops, no iterators, no closures created per callback. Browser-allocated entries (the `ResizeObserverEntry[]`, the `MutationRecord[]`) we cannot avoid; we add nothing to them.
|
|
214
228
|
|
|
229
|
+
**Pooled-registry census.** A consumer running `createRegistry({ maxNodes, prealloc, onCapacityExceeded })` (lite-signal 1.5.0) must size the pool from a census of every signal the app allocates. A library minting signals on the default registry is part of that census -- lite-observe is no exception. The per-entry-point counts above (2 per observed element for resize, up to 4 mixed-box; 2 per intersection key; 1 per mutation; 4 per mutation-selector; 1 per media query; 1 module-scope visibility) are the inputs to that sum: add the steady-state count for each entry point times the number of elements/keys/queries observed concurrently. These are steady-state (while-observed) costs, freed on final dispose, so the census is of peak concurrency, not lifetime turnover. Guessing low throws `CapacityError` at runtime from inside the library.
|
|
230
|
+
|
|
215
231
|
## Benchmarks
|
|
216
232
|
|
|
217
233
|
The bench drives each subsystem's dispatch path synthetically (browsers don't run in Node) with one live subscriber per signal so writes actually propagate, and measures heap growth and minor-GC (scavenge) count under load. The gate **fails the build** if any path shows a single scavenge or more than 1 byte/call. That threshold being zero is the point: one accidental allocation regresses the gate.
|
package/llms.txt
CHANGED
|
@@ -12,16 +12,22 @@ registry: N consumers of the same element share one underlying observer.
|
|
|
12
12
|
independent signals (Object.is-gated). First emission async. `options.box`
|
|
13
13
|
selects `'content'` (default, contentRect) or `'border'` (borderBoxSize,
|
|
14
14
|
matches getBoundingClientRect). Mixed-box consumers of one element still
|
|
15
|
-
share a single observation
|
|
15
|
+
share a single observation; a late joiner is seeded from the element's
|
|
16
|
+
last delivered size for its box (0 if nothing has been delivered yet),
|
|
17
|
+
not a stale zero until the next resize.
|
|
18
|
+
Pooled nodes: 2 while observed (the requested box pair; 4 if both content-
|
|
19
|
+
and border-box consumers observe the same element). Freed on last dispose.
|
|
16
20
|
|
|
17
21
|
- `observeIntersection(el, options?)` -> `{ isIntersecting: Signal<boolean>, ratio: Signal<number>, dispose }`
|
|
18
22
|
Observers keyed by (root, rootMargin, threshold). Equivalent options
|
|
19
23
|
share one observer. Different options spawn distinct ones (per spec).
|
|
24
|
+
Pooled nodes: 2 per (element, options-key) while observed. Freed on last dispose.
|
|
20
25
|
|
|
21
26
|
- `observeMutation(el, options)` -> `{ tick: Signal<number>, peekRecords(), dispose }`
|
|
22
27
|
Tick is a monotonic counter; increments per delivered batch.
|
|
23
28
|
peekRecords() returns the latest MutationRecord[] without tracking and
|
|
24
29
|
without copying. One observer per (element, optionsKey).
|
|
30
|
+
Pooled nodes: 1 while observed (the tick). Freed on last dispose.
|
|
25
31
|
|
|
26
32
|
- `observeMutationSelector(root, selector, options?)` -> `{ added: Signal<Element[]>, removed: Signal<Element[]>, tick: Signal<number>, peekRecords(), dispose }`
|
|
27
33
|
Selector-filtered wrapper over observeMutation. Surfaces matching elements
|
|
@@ -31,14 +37,17 @@ registry: N consumers of the same element share one underlying observer.
|
|
|
31
37
|
container are caught. added/removed allocate fresh arrays only on batches
|
|
32
38
|
with a match (so consumers may retain them); zero-match batches reuse a
|
|
33
39
|
frozen empty array. Composes the inner handle's FinalizationRegistry safety.
|
|
40
|
+
Pooled nodes: 4 while observed (added + removed, plus the inner
|
|
41
|
+
observeMutation tick and one effect). Freed on last dispose.
|
|
34
42
|
|
|
35
43
|
- `observeMedia(query)` -> `Signal<boolean>`
|
|
36
44
|
One signal per query string, cached. Listener attached lazily via
|
|
37
45
|
lite-signal observeObservers; detached when no consumer is reading.
|
|
46
|
+
Pooled nodes: 1, shared per query string. Freed on last dispose.
|
|
38
47
|
|
|
39
48
|
- `documentVisible: Signal<boolean>`
|
|
40
49
|
Module-level. True iff document.visibilityState === 'visible'.
|
|
41
|
-
Lazy listener.
|
|
50
|
+
Lazy listener. Pooled nodes: 1, module-scope.
|
|
42
51
|
|
|
43
52
|
## Orphan-safety utility (public)
|
|
44
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-observe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
5
5
|
"description": "Zero-GC reactive bridge for the DOM observer APIs. ResizeObserver, IntersectionObserver, MutationObserver, matchMedia, and Page Visibility collapsed to fine-grained lite-signal signals. Shared registry: N components observing the same element pay one observer cost.",
|
|
6
6
|
"type": "module",
|
package/src/Resize.js
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* source. ResizeObserver fires with sub-pixel precision; we forward the raw
|
|
13
13
|
* `contentRect.width` / `contentRect.height` values.
|
|
14
14
|
*
|
|
15
|
+
* Each slot also parks the last delivered box dimensions as four primitives
|
|
16
|
+
* (no browser object retained, no allocation on the drain path). A late
|
|
17
|
+
* mixed-box joiner that mints the other pair after delivery has already
|
|
18
|
+
* happened reads those last numbers immediately, not a stale zero that would
|
|
19
|
+
* persist until the element next resizes; before any delivery the parked
|
|
20
|
+
* fields are null and a freshly minted pair reads 0.
|
|
21
|
+
*
|
|
15
22
|
* SSR-safe. In a Node environment without `ResizeObserver`, signals are
|
|
16
23
|
* created and stay at zero; observe is a no-op.
|
|
17
24
|
*/
|
|
@@ -37,12 +44,13 @@ const slots = new Map();
|
|
|
37
44
|
*
|
|
38
45
|
* `entries` is allocated by the browser; we cannot avoid that. We iterate
|
|
39
46
|
* with an indexed for-loop (no iterator allocation) and look up each entry's
|
|
40
|
-
* target in the slot map.
|
|
41
|
-
* height and border width/height);
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
47
|
+
* target in the slot map. A slot carries only the box pair(s) its consumers
|
|
48
|
+
* asked for (content width/height and/or border width/height); each pair is
|
|
49
|
+
* written only when present (null-guarded), so an element observed for
|
|
50
|
+
* content-box alone never mints or writes border-box nodes. Setting a signal
|
|
51
|
+
* to its current value is a no-op under lite-signal's Object.is equality, so
|
|
52
|
+
* consumers reading only one pair do not wake when the other pair moved -- and
|
|
53
|
+
* consumers reading only `width` do not wake when only `height` moved.
|
|
46
54
|
*
|
|
47
55
|
* @param {ResizeObserverEntry[]} entries
|
|
48
56
|
*/
|
|
@@ -55,27 +63,38 @@ function drainResize() {
|
|
|
55
63
|
const entry = entries[i];
|
|
56
64
|
const slot = slots.get(entry.target);
|
|
57
65
|
if (slot === undefined) continue;
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// padding + border.
|
|
66
|
+
// rect is a field read, not an allocation. Hoisted here because the
|
|
67
|
+
// border fallback reads it too; content-box writes use it directly.
|
|
61
68
|
const rect = entry.contentRect;
|
|
62
|
-
slot.contentWidth.set(rect.width);
|
|
63
|
-
slot.contentHeight.set(rect.height);
|
|
64
69
|
// Border box: includes padding + border. Per spec the field is an
|
|
65
70
|
// array (to support multi-column layout); we read the first entry.
|
|
66
|
-
// Older browsers without borderBoxSize fall back to contentRect
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// values until the next layout pass.
|
|
71
|
+
// Older browsers without borderBoxSize fall back to contentRect; for
|
|
72
|
+
// zero-alloc cleanliness we just mirror content here when it is absent,
|
|
73
|
+
// so a legacy-runtime border consumer sees content-box values until the
|
|
74
|
+
// next layout pass. Resolve both dimensions into locals once (numbers,
|
|
75
|
+
// not references) so the parking and the guarded write below reuse them.
|
|
72
76
|
const bbs = entry.borderBoxSize;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const bw = (bbs && bbs.length > 0) ? bbs[0].inlineSize : rect.width;
|
|
78
|
+
const bh = (bbs && bbs.length > 0) ? bbs[0].blockSize : rect.height;
|
|
79
|
+
// Park the last delivered numbers on the slot unconditionally so a late
|
|
80
|
+
// mixed-box joiner can seed its lazily-minted pair from the element's
|
|
81
|
+
// real size instead of a stale zero (B-4). Four primitives: zero
|
|
82
|
+
// allocation, no browser object retained.
|
|
83
|
+
slot.lastCW = rect.width;
|
|
84
|
+
slot.lastCH = rect.height;
|
|
85
|
+
slot.lastBW = bw;
|
|
86
|
+
slot.lastBH = bh;
|
|
87
|
+
// Content box: the historical default. Matches the rect a consumer
|
|
88
|
+
// would derive from `getBoundingClientRect` after subtracting
|
|
89
|
+
// padding + border. Written only if a content-box consumer minted it.
|
|
90
|
+
if (slot.contentWidth !== null) {
|
|
91
|
+
slot.contentWidth.set(rect.width);
|
|
92
|
+
slot.contentHeight.set(rect.height);
|
|
93
|
+
}
|
|
94
|
+
// Border box: written only if a border-box consumer minted the pair.
|
|
95
|
+
if (slot.borderWidth !== null) {
|
|
96
|
+
slot.borderWidth.set(bw);
|
|
97
|
+
slot.borderHeight.set(bh);
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
}
|
|
@@ -119,7 +138,9 @@ function ensureObserver() {
|
|
|
119
138
|
* its own dispose. The element stays observed until every handle has been
|
|
120
139
|
* disposed. Consumers requesting different boxes for the same element
|
|
121
140
|
* cooperate transparently: a single ResizeObserver fires both, and each
|
|
122
|
-
* handle surfaces the pair it asked for.
|
|
141
|
+
* handle surfaces the pair it asked for. A handle that joins an already-
|
|
142
|
+
* observed element after a delivery is seeded from that element's last
|
|
143
|
+
* delivered size for its box; before any delivery it reads 0.
|
|
123
144
|
*
|
|
124
145
|
* @param {Element} element
|
|
125
146
|
* @param {ResizeOptions} [options]
|
|
@@ -129,17 +150,45 @@ export function observeResize(element, options) {
|
|
|
129
150
|
const box = (options && options.box === 'border') ? 'border' : 'content';
|
|
130
151
|
let slot = slots.get(element);
|
|
131
152
|
if (slot === undefined) {
|
|
153
|
+
// Keep all four fields present for hidden-class stability, but leave
|
|
154
|
+
// the unrequested pair null: we mint only the box a caller asks for.
|
|
132
155
|
slot = {
|
|
133
|
-
contentWidth:
|
|
134
|
-
contentHeight:
|
|
135
|
-
borderWidth:
|
|
136
|
-
borderHeight:
|
|
156
|
+
contentWidth: null,
|
|
157
|
+
contentHeight: null,
|
|
158
|
+
borderWidth: null,
|
|
159
|
+
borderHeight: null,
|
|
160
|
+
// Last delivered box dimensions, parked as primitives by
|
|
161
|
+
// drainResize so a late mixed-box joiner seeds its pair from the
|
|
162
|
+
// element's real size, not a stale zero (B-4). null until the first
|
|
163
|
+
// delivery; a null-check (not `??`) reads that as 0 at mint time.
|
|
164
|
+
lastCW: null,
|
|
165
|
+
lastCH: null,
|
|
166
|
+
lastBW: null,
|
|
167
|
+
lastBH: null,
|
|
137
168
|
refCount: 0
|
|
138
169
|
};
|
|
139
170
|
slots.set(element, slot);
|
|
140
171
|
const obs = ensureObserver();
|
|
141
172
|
if (obs !== null) obs.observe(element);
|
|
142
173
|
}
|
|
174
|
+
// Lazily mint the requested box pair if absent. Placed before refCount++
|
|
175
|
+
// so it also covers a later mixed-box caller joining an existing slot that
|
|
176
|
+
// only holds the other pair. Seed from the last delivered numbers so a late
|
|
177
|
+
// joiner reads the element's real size at once, not a stale zero until the
|
|
178
|
+
// next resize (B-4). Explicit `=== null ? 0 : x` rather than `?? 0`: only
|
|
179
|
+
// null (never delivered) means "no size yet" and reads 0; any delivered
|
|
180
|
+
// number, 0 included, passes through untouched. Law: null is not zero.
|
|
181
|
+
if (box === 'border') {
|
|
182
|
+
if (slot.borderWidth === null) {
|
|
183
|
+
slot.borderWidth = signal(slot.lastBW === null ? 0 : slot.lastBW);
|
|
184
|
+
slot.borderHeight = signal(slot.lastBH === null ? 0 : slot.lastBH);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
if (slot.contentWidth === null) {
|
|
188
|
+
slot.contentWidth = signal(slot.lastCW === null ? 0 : slot.lastCW);
|
|
189
|
+
slot.contentHeight = signal(slot.lastCH === null ? 0 : slot.lastCH);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
143
192
|
slot.refCount++;
|
|
144
193
|
|
|
145
194
|
let disposed = false;
|
|
@@ -163,11 +212,10 @@ export function observeResize(element, options) {
|
|
|
163
212
|
}
|
|
164
213
|
// Last consumer left: free the slot's pooled signal nodes so an
|
|
165
214
|
// observe/dispose cycle does not permanently retain them (which
|
|
166
|
-
// throws CapacityError under a capped registry).
|
|
167
|
-
|
|
168
|
-
disposeNode(slot.contentHeight);
|
|
169
|
-
disposeNode(slot.borderWidth);
|
|
170
|
-
disposeNode(slot.borderHeight);
|
|
215
|
+
// throws CapacityError under a capped registry). Only the pairs
|
|
216
|
+
// that were actually minted exist to free.
|
|
217
|
+
if (slot.contentWidth !== null) { disposeNode(slot.contentWidth); disposeNode(slot.contentHeight); }
|
|
218
|
+
if (slot.borderWidth !== null) { disposeNode(slot.borderWidth); disposeNode(slot.borderHeight); }
|
|
171
219
|
}
|
|
172
220
|
}
|
|
173
221
|
|