@zakkster/lite-project 1.4.0 → 1.4.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 +29 -0
- package/Project.d.ts +1 -1
- package/Project.js +35 -20
- package/llms.txt +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
All notable changes to `@zakkster/lite-project` are documented here. The format
|
|
4
4
|
follows Keep a Changelog; this project adheres to semantic versioning.
|
|
5
5
|
|
|
6
|
+
## [1.4.1] - 2026-09-05
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Hot-path transient allocation (~40 B/op on `get`/`peek`/`set`).** The slot-
|
|
11
|
+
creation closure lived inline in `slotFor`'s cold miss branch and captured
|
|
12
|
+
`key`, so V8 allocated a context object on EVERY `slotFor` call -- hit or
|
|
13
|
+
miss -- taxing the three hottest operations ~40 B/op each (measured: warm
|
|
14
|
+
`get` 2,002,808 B over 50,000 ops). `peek` had the same defect twice over via
|
|
15
|
+
inline `untrack(() => source.get(key))` closures, allocating even on the warm
|
|
16
|
+
overlaid path that never takes the fallthrough; `reconcileAll` once per
|
|
17
|
+
overlaid key. Fixes: slot creation hoisted to `_createSlot(key)` (context now
|
|
18
|
+
allocated only on the cold miss), `peek` and `reconcileAll` ride the hoisted
|
|
19
|
+
`_pk`/`_readSrc` scratch that `forEachPatch` already used. Measured after:
|
|
20
|
+
0.04-0.15 B/op fixed noise across all warm windows. No API or behaviour
|
|
21
|
+
change.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **T6 Proof 0, the transient witness.** Warm `get` / `peek` / `set` /
|
|
26
|
+
`set-clear toggle` / `get+set+clear` triangle windows are now hard-gated by
|
|
27
|
+
the V8 new-space used-bytes delta over a GC-free 50,000-op window
|
|
28
|
+
(<= 16,384 B total each). Every prior lane -- the gc-profiler heap gate, the
|
|
29
|
+
retained-bytes bracket, the pool census -- is structurally blind to per-op
|
|
30
|
+
garbage that never survives a collection, which is how the 40 B/op defect
|
|
31
|
+
above passed the full gate. The GATE line now reports `transient=<n> B/op`
|
|
32
|
+
(triangle; measured 0.131 B/op). Falsified: reverting the fix makes the gate
|
|
33
|
+
exit 1 naming the window.
|
|
34
|
+
|
|
6
35
|
## [1.4.0] - 2026-09-05
|
|
7
36
|
|
|
8
37
|
### Added
|
package/Project.d.ts
CHANGED
package/Project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-project v1.4.
|
|
2
|
+
* @zakkster/lite-project v1.4.1 -- zero-GC projections for @zakkster/lite-signal.
|
|
3
3
|
* -----------------------------------------------------------------------------
|
|
4
4
|
* A projection is a granular, derived, NON-MUTATING reactive view over a keyed
|
|
5
5
|
* source: a lens that can carry ephemeral overlays (optimistic edits, merges,
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
hasObservers as _hasObservers,
|
|
90
90
|
} from "@zakkster/lite-signal";
|
|
91
91
|
|
|
92
|
-
export const VERSION = "1.4.
|
|
92
|
+
export const VERSION = "1.4.1";
|
|
93
93
|
|
|
94
94
|
// Module-level sentinel for "this key has no overlay". A unique symbol, never a
|
|
95
95
|
// per-operation allocation. Stored directly in the overlay signal's value slot, so
|
|
@@ -201,23 +201,29 @@ export function createProjector(reg) {
|
|
|
201
201
|
// Invariant: exp !== 0 implies overlaid, so prune() never orphans a deadline.
|
|
202
202
|
const slots = new Map();
|
|
203
203
|
|
|
204
|
+
// Slot creation lives in its OWN function, never inline in slotFor: the
|
|
205
|
+
// creation closure captures `key`, and a capture inside slotFor's scope
|
|
206
|
+
// would make V8 allocate a context object on EVERY slotFor call -- hit or
|
|
207
|
+
// miss -- taxing get/peek/set ~40 B/op. Here the context is allocated only
|
|
208
|
+
// on the cold miss. (Bytes in a hot body: a closure in a cold branch still
|
|
209
|
+
// costs the hot branch its context.)
|
|
210
|
+
const _createSlot = (key) => {
|
|
211
|
+
// Detach owner+observer for creation: these nodes outlive the consumer
|
|
212
|
+
// that first reads `key`, and the projection -- not that consumer --
|
|
213
|
+
// owns their disposal. (See header: OWNERSHIP.)
|
|
214
|
+
return createRoot(() => {
|
|
215
|
+
const ov = signal(ABSENT);
|
|
216
|
+
const read = computed(() => {
|
|
217
|
+
const o = ov(); // track the overlay
|
|
218
|
+
const base = source.get(key); // track the source cell too
|
|
219
|
+
return o === ABSENT ? base : o;
|
|
220
|
+
});
|
|
221
|
+
return { ov, read, exp: 0 };
|
|
222
|
+
});
|
|
223
|
+
};
|
|
204
224
|
const slotFor = (key) => {
|
|
205
225
|
let s = slots.get(key);
|
|
206
|
-
if (s === undefined) {
|
|
207
|
-
// Detach owner+observer for creation: these nodes outlive the consumer
|
|
208
|
-
// that first reads `key`, and the projection -- not that consumer --
|
|
209
|
-
// owns their disposal. (See header: OWNERSHIP.)
|
|
210
|
-
s = createRoot(() => {
|
|
211
|
-
const ov = signal(ABSENT);
|
|
212
|
-
const read = computed(() => {
|
|
213
|
-
const o = ov(); // track the overlay
|
|
214
|
-
const base = source.get(key); // track the source cell too
|
|
215
|
-
return o === ABSENT ? base : o;
|
|
216
|
-
});
|
|
217
|
-
return { ov, read, exp: 0 };
|
|
218
|
-
});
|
|
219
|
-
slots.set(key, s);
|
|
220
|
-
}
|
|
226
|
+
if (s === undefined) { s = _createSlot(key); slots.set(key, s); }
|
|
221
227
|
return s;
|
|
222
228
|
};
|
|
223
229
|
|
|
@@ -371,11 +377,17 @@ export function createProjector(reg) {
|
|
|
371
377
|
isDirty: () => dirtySig() > 0,
|
|
372
378
|
// Untracked effective read (overlay if set, else source) -- for
|
|
373
379
|
// reconciliation policies and imperative inspection, without subscribing.
|
|
380
|
+
// Source fallthrough rides the hoisted _pk/_readSrc scratch (the
|
|
381
|
+
// forEachPatch precedent): an inline untrack closure would capture
|
|
382
|
+
// `key` and cost EVERY peek a context allocation -- including the warm
|
|
383
|
+
// overlaid path that never takes the fallthrough.
|
|
374
384
|
peek: (key) => {
|
|
375
385
|
const s = slots.get(key);
|
|
376
|
-
if (s === undefined) return untrack(
|
|
386
|
+
if (s === undefined) { _pk = key; return untrack(_readSrc); }
|
|
377
387
|
const o = s.ov.peek();
|
|
378
|
-
|
|
388
|
+
if (o !== ABSENT) return o;
|
|
389
|
+
_pk = key;
|
|
390
|
+
return untrack(_readSrc);
|
|
379
391
|
},
|
|
380
392
|
// Iterate currently-overlaid keys (untracked). Cold path.
|
|
381
393
|
forEachOverlay: (fn) => {
|
|
@@ -407,7 +419,10 @@ export function createProjector(reg) {
|
|
|
407
419
|
for (const [key, s] of slots) {
|
|
408
420
|
const o = s.ov.peek();
|
|
409
421
|
if (o !== ABSENT) {
|
|
410
|
-
|
|
422
|
+
// _pk/_readSrc scratch (the forEachPatch precedent): an
|
|
423
|
+
// inline untrack closure would allocate per overlaid key.
|
|
424
|
+
_pk = key;
|
|
425
|
+
const authoritative = untrack(_readSrc);
|
|
411
426
|
if (pol(authoritative, o, key)) { s.ov.set(ABSENT); _dropExp(s); dropped++; }
|
|
412
427
|
}
|
|
413
428
|
}
|
package/llms.txt
CHANGED
|
@@ -147,7 +147,11 @@ prune() -> number [1.1] [release slots that are BOTH un-overlaid AND unobserved;
|
|
|
147
147
|
|
|
148
148
|
Steady state: re-overlaying a warmed key reuses pooled nodes (200k toggles ->
|
|
149
149
|
poolGrowths/totalAllocations flat). First touch of a NEW key allocates its slot,
|
|
150
|
-
a Map entry, and two pooled nodes. Warm the keys you churn.
|
|
150
|
+
a Map entry, and two pooled nodes. Warm the keys you churn. Gated transient-clean
|
|
151
|
+
since 1.4.1: warm get/peek/set/toggle windows measured by V8 new-space delta
|
|
152
|
+
(<= 16384 B total per 50k-op window; measured ~0.13 B/op triangle noise). 1.4.0
|
|
153
|
+
allocated ~40 B/op on get/peek/set via a hot-path closure context -- fixed and
|
|
154
|
+
now impossible to reintroduce silently.
|
|
151
155
|
|
|
152
156
|
## Gotchas
|
|
153
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-project",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Zero-GC projections for @zakkster/lite-signal: granular, derived, non-mutating reactive overlays with commit / revert / reconcile, and draft adapters for lite-store and lite-room.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./Project.js",
|