@zakkster/lite-project 1.1.0 → 1.2.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/CHANGELOG.md +85 -0
- package/Project.d.ts +34 -1
- package/Project.js +49 -1
- package/README.md +26 -0
- package/llms.txt +11 -0
- package/package.json +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,91 @@
|
|
|
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.2.0] - 2026-09-05
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`Projection.forEachPatch(fn, skip?)`** -- emit the staged drafts as a
|
|
11
|
+
`(key, from, to)` stream, where `from` is the **untracked** current source
|
|
12
|
+
value and `to` the staged overlay. Read-only and untracked: it touches neither
|
|
13
|
+
the source nor the overlays and subscribes the caller to nothing, so it is safe
|
|
14
|
+
inside an effect. Visits exactly the overlaid keys, in `forEachOverlay` order,
|
|
15
|
+
with a **zero-allocation** per-key body (the source read is hoisted through one
|
|
16
|
+
closure per projection, never one per key). The optional `skip` reuses the
|
|
17
|
+
`ReconcilePolicy` shape `(from, to, key) => boolean` -- pass `confirmOnEcho` to
|
|
18
|
+
drop unchanged drafts. A throwing `source.get` propagates on the offending key
|
|
19
|
+
with the overlay bag intact (no writes happen anywhere in the call).
|
|
20
|
+
- **`Projection.toPatch(skip?)`** -- the cold convenience that materializes the
|
|
21
|
+
same stream as `[{ key, from, to }, ...]` (same visit set, order, and values).
|
|
22
|
+
The per-key record is this form's documented allocation; reach for
|
|
23
|
+
`forEachPatch` when you need the zero-alloc callback. Both methods are present
|
|
24
|
+
on the `projectStore` / `projectRoom` / `projectQuery` handles (for
|
|
25
|
+
`projectQuery`, `from` is the cached record's field value; for `projectRoom`,
|
|
26
|
+
`room.storage.get`).
|
|
27
|
+
- **`Patch<K, V>`** interface (`{ key, from, to }`) exported from `Project.d.ts`.
|
|
28
|
+
|
|
29
|
+
**The decision -- unchanged drafts are emitted by default.** An overlaid key is
|
|
30
|
+
emitted whether or not `Object.is(from, to)`. This keeps the visit set
|
|
31
|
+
definitionally equal to `forEachOverlay`, `dirtyCount()`, and `commit()`'s write
|
|
32
|
+
set, so `toPatch().length === dirtyCount()` always holds; a patch consumer is a
|
|
33
|
+
protocol (an LWW-Map op, a CRDT timestamp bump, an HTTP PATCH field), not a diff
|
|
34
|
+
viewer, so dropping an unchanged key would be silent data loss one layer out.
|
|
35
|
+
Callers who want the filter pass `forEachPatch(fn, confirmOnEcho)`. Recorded in
|
|
36
|
+
`decisions/0001-patch-emission.md` (dev-only; not shipped).
|
|
37
|
+
|
|
38
|
+
### Verified
|
|
39
|
+
|
|
40
|
+
- 17 new `test/patch_test.mjs` cases (visit-set exactness + order, from/to vs
|
|
41
|
+
source and overlay, `toPatch()` == the callback stream, the emit-by-default and
|
|
42
|
+
echo-skip pins, the tracking contract, `__proto__` / symbol / numeric keys,
|
|
43
|
+
`undefined` / `NaN` / `-0` under `Object.is`, fail-closed on a throwing
|
|
44
|
+
`source.get`, patch-apply == commit, and all four adapter handles); **84 tests
|
|
45
|
+
total**, `node --test`.
|
|
46
|
+
- Torture green (default seed):
|
|
47
|
+
`leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 |
|
|
48
|
+
alloc=n/a retained=0.00 B/op growths=0`. T5 gains the metamorphic law (the
|
|
49
|
+
emitted patch applied to a fresh source copy == `commit()` into it, across the
|
|
50
|
+
fuzz corpus incl. object drafts). T6 gains Proof 4 -- `forEachPatch` over a
|
|
51
|
+
warm overlaid set passes both the heap gate (`maxMajor 0`, `maxPauseMs 4`,
|
|
52
|
+
`maxArrayBuffersGrowth 0`) and the zero-retention gate (`maxBytesPerCall 0`).
|
|
53
|
+
T9 gains control (g): a per-visit-allocating emitter body demonstrably trips the
|
|
54
|
+
retained-alloc gate through the same helper.
|
|
55
|
+
|
|
56
|
+
## [1.1.1] - 2026-09-03
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **`VERSION`** export from `Project.js` (declared in `Project.d.ts`), kept in
|
|
61
|
+
exact sync with `package.json`, so a consumer can read the shipped version at
|
|
62
|
+
runtime.
|
|
63
|
+
- Gated torture harness under `test/torture/` (dev-only; not shipped). Seven
|
|
64
|
+
tiers run strictly sequentially behind `node --expose-gc test/torture.mjs`:
|
|
65
|
+
T0 metamorphic laws, T1 degenerate inputs, T4 reconcile door, T5 oracle fuzz,
|
|
66
|
+
T6 zero-alloc gate, T7 retention soak, T9 controls. Gate RULES:
|
|
67
|
+
`maxMajor 0`, `maxPauseMs 4`, `maxArrayBuffersGrowth 0` under `stabilize:"deep"`.
|
|
68
|
+
Witnessed on a green run (default seed):
|
|
69
|
+
`leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 |
|
|
70
|
+
retained=0.00 B/op growths=0`
|
|
71
|
+
(the `alloc=` per-op heap-bracket reading prints as-is, `n/a` when
|
|
72
|
+
inconclusive; the binding alloc gates are `checkNoGc`, the zero-retention
|
|
73
|
+
gate below, and the structural deltas below).
|
|
74
|
+
The get/set/clear triangle on warmed keys leaves `poolGrowths` and
|
|
75
|
+
`totalAllocations` deltas both 0 over 200k toggles; 4096 build/tear-down cycles
|
|
76
|
+
return the leak tracker to `size()===0`; `prune()` reclaims >= 19990 of 20000
|
|
77
|
+
unbounded-read slots.
|
|
78
|
+
- Retained-allocation gate (`measureAllocs` + `checkAllocs` with
|
|
79
|
+
`maxBytesPerCall: 0`, the profiler's zero-retention assertion): the warmed
|
|
80
|
+
get/set/clear triangle retains 0 B/call (min over 8 batches), catching
|
|
81
|
+
arbitrary JS-object retention that the asynchronously-delivered `gc.major`
|
|
82
|
+
count cannot see; an unsettled or inconclusive reading fails the gate.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- Peer dependency floor is now `@zakkster/lite-signal ^1.5.0` (was a pinned
|
|
87
|
+
preview build). No behaviour change: 1.5.0 is the current stable line and
|
|
88
|
+
supplies every surface this package uses (`batch` 1.0.0, `hasObservers` 1.1.4,
|
|
89
|
+
`createRoot` 1.5.0).
|
|
90
|
+
|
|
6
91
|
## [1.1.0] - 2026-07-16
|
|
7
92
|
|
|
8
93
|
### Added
|
package/Project.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
// Type declarations for @zakkster/lite-project v1.
|
|
1
|
+
// Type declarations for @zakkster/lite-project v1.2.0
|
|
2
2
|
// Zero-GC projections for @zakkster/lite-signal.
|
|
3
3
|
// (c) 2026 Zahary Shinikchiev <shinikchiev@yahoo.com> -- MIT
|
|
4
4
|
|
|
5
|
+
export const VERSION: string;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* A reactive keyed source a projection can wrap. Any object with a reactive
|
|
7
9
|
* `get(key)` and a `set(key, value)` qualifies (the built-in `keyedStore`, a
|
|
@@ -23,6 +25,17 @@ export interface ProjectionSource<K extends PropertyKey = PropertyKey, V = unkno
|
|
|
23
25
|
export type ReconcilePolicy<K extends PropertyKey = PropertyKey, V = unknown> =
|
|
24
26
|
(authoritative: V, overlayValue: V, key: K) => boolean;
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* One staged draft as a patch entry: the current source value (`from`) and the
|
|
30
|
+
* staged overlay value (`to`) for `key`. The materialized shape returned by
|
|
31
|
+
* {@link Projection.toPatch}.
|
|
32
|
+
*/
|
|
33
|
+
export interface Patch<K extends PropertyKey = PropertyKey, V = unknown> {
|
|
34
|
+
key: K;
|
|
35
|
+
from: V;
|
|
36
|
+
to: V;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/**
|
|
27
40
|
* A projection handle: a granular, derived, non-mutating draft overlay over a
|
|
28
41
|
* keyed source. Each touched key owns one overlay signal + one projected
|
|
@@ -51,6 +64,26 @@ export interface Projection<K extends PropertyKey = PropertyKey, V = unknown> {
|
|
|
51
64
|
peek(key: K): V;
|
|
52
65
|
/** Iterate currently-overlaid keys with their overlay values (untracked). */
|
|
53
66
|
forEachOverlay(fn: (key: K, value: V) => void): void;
|
|
67
|
+
/**
|
|
68
|
+
* Emit the staged drafts as a patch stream `fn(key, from, to)` -- `from` is
|
|
69
|
+
* the UNTRACKED current source value, `to` the staged overlay. Read-only and
|
|
70
|
+
* untracked: it touches neither the source nor the overlays and subscribes the
|
|
71
|
+
* caller to nothing, so it is safe inside an effect. Visits exactly the
|
|
72
|
+
* overlaid keys, in {@link Projection.forEachOverlay} order, with a zero-alloc
|
|
73
|
+
* per-key body. An overlaid key is emitted whether or not `Object.is(from, to)`
|
|
74
|
+
* (the visit set stays equal to `dirtyCount()`); pass `skip` -- the same
|
|
75
|
+
* predicate shape reconcile uses, e.g. {@link confirmOnEcho} -- to drop
|
|
76
|
+
* unchanged drafts. A throwing `source.get` propagates on that key with the
|
|
77
|
+
* overlay bag intact (callers needing atomicity use {@link Projection.toPatch}).
|
|
78
|
+
*/
|
|
79
|
+
forEachPatch(fn: (key: K, from: V, to: V) => void, skip?: ReconcilePolicy<K, V>): void;
|
|
80
|
+
/**
|
|
81
|
+
* Cold convenience over {@link Projection.forEachPatch}: materialize the drafts
|
|
82
|
+
* as `[{ key, from, to }, ...]` -- same visit set, order, and values. The
|
|
83
|
+
* per-key record is this form's allocation; reach for `forEachPatch` when you
|
|
84
|
+
* need the zero-alloc callback.
|
|
85
|
+
*/
|
|
86
|
+
toPatch(skip?: ReconcilePolicy<K, V>): Array<Patch<K, V>>;
|
|
54
87
|
/**
|
|
55
88
|
* Full-snapshot reconciliation: drop every overlay the policy considers
|
|
56
89
|
* confirmed against the current (untracked) source value. Presentation-only --
|
package/Project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-project v1.
|
|
2
|
+
* @zakkster/lite-project v1.2.0 -- 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,
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* circuit -- does NOT churn downstream consumers. The optimistic value
|
|
22
22
|
* is stable under source noise.
|
|
23
23
|
*
|
|
24
|
+
* Patch emission (forEachPatch / toPatch) exposes the staged drafts as a
|
|
25
|
+
* (key, from, to) stream for a save/sync trigger. It is READ-ONLY and UNTRACKED:
|
|
26
|
+
* it never touches the source or the overlays and subscribes the caller to nothing.
|
|
27
|
+
*
|
|
24
28
|
* -- OWNERSHIP (why createRoot) --
|
|
25
29
|
* Per-key nodes are created LAZILY, on the first get/set of a key -- which happens
|
|
26
30
|
* inside whatever consumer effect first reads that key. Without detachment the
|
|
@@ -59,6 +63,8 @@ import {
|
|
|
59
63
|
hasObservers as _hasObservers,
|
|
60
64
|
} from "@zakkster/lite-signal";
|
|
61
65
|
|
|
66
|
+
export const VERSION = "1.2.0";
|
|
67
|
+
|
|
62
68
|
// Module-level sentinel for "this key has no overlay". A unique symbol, never a
|
|
63
69
|
// per-operation allocation. Stored directly in the overlay signal's value slot, so
|
|
64
70
|
// "absent" and "present with value V" share one node and one field -- no boxing.
|
|
@@ -127,6 +133,8 @@ export function createProjector(reg) {
|
|
|
127
133
|
* isDirty:()=>boolean, // TRACKED: any staged overlays? (reactive)
|
|
128
134
|
* peek:(key:PropertyKey)=>unknown, // untracked effective read (no subscribe)
|
|
129
135
|
* forEachOverlay:(fn:(key:PropertyKey, value:unknown)=>void)=>void, // iterate overlaid keys (untracked)
|
|
136
|
+
* forEachPatch:(fn:(key:PropertyKey, from:unknown, to:unknown)=>void, skip?:Function)=>void, // patch stream (untracked, read-only)
|
|
137
|
+
* toPatch:(skip?:Function)=>Array<{key:PropertyKey, from:unknown, to:unknown}>, // materialized patch (cold convenience)
|
|
130
138
|
* reconcileAll:(policy?:(authoritative:unknown, overlayValue:unknown, key:PropertyKey)=>boolean)=>void, // drop confirmed overlays
|
|
131
139
|
* commit:(key?:PropertyKey)=>void, // write one key's overlay, or all, into the source then clear
|
|
132
140
|
* revert:()=>void, // drop all overlays
|
|
@@ -158,6 +166,28 @@ export function createProjector(reg) {
|
|
|
158
166
|
return s;
|
|
159
167
|
};
|
|
160
168
|
|
|
169
|
+
// Patch emission: iterate exactly the overlaid keys, handing scalars
|
|
170
|
+
// (key, from, to) to `fn` -- `from` is the UNTRACKED current source value,
|
|
171
|
+
// `to` the staged overlay. Read-only: overlays via .peek(), source under
|
|
172
|
+
// untrack, so calling this inside an effect subscribes to nothing. The
|
|
173
|
+
// visit set / order is byte-identical to forEachOverlay. Optional `skip`
|
|
174
|
+
// reuses ReconcilePolicy: (from, to, key) => true drops that key from the
|
|
175
|
+
// stream (e.g. `forEachPatch(fn, confirmOnEcho)` skips echoes); default
|
|
176
|
+
// undefined emits every overlaid key, changed or not. A throwing
|
|
177
|
+
// source.get propagates on the offending key with no writes anywhere, so
|
|
178
|
+
// the overlay bag is intact by construction (fn may already have run for
|
|
179
|
+
// earlier keys; callers needing atomicity use toPatch()).
|
|
180
|
+
const forEachPatch = (fn, skip) => {
|
|
181
|
+
for (const [key, s] of slots) {
|
|
182
|
+
const to = s.ov.peek();
|
|
183
|
+
if (to === ABSENT) continue;
|
|
184
|
+
_pk = key;
|
|
185
|
+
const from = untrack(_readSrc);
|
|
186
|
+
if (skip !== undefined && skip(from, to, key)) continue;
|
|
187
|
+
fn(key, from, to);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
161
191
|
// Reactive dirty state. ONE fixed signal per projection (created detached so
|
|
162
192
|
// dispose() owns its teardown, like the per-key nodes). `dirty` is the
|
|
163
193
|
// source-of-truth count of staged overlays, mirrored into the signal on every
|
|
@@ -167,6 +197,12 @@ export function createProjector(reg) {
|
|
|
167
197
|
const dirtySig = createRoot(() => signal(0));
|
|
168
198
|
let dirty = 0;
|
|
169
199
|
|
|
200
|
+
// Hoisted scratch for forEachPatch's untracked source read: ONE closure
|
|
201
|
+
// per projection, never per key/call, so the per-key emit body allocates
|
|
202
|
+
// nothing. `untrack` needs a function; _readSrc is it.
|
|
203
|
+
let _pk;
|
|
204
|
+
const _readSrc = () => source.get(_pk);
|
|
205
|
+
|
|
170
206
|
return {
|
|
171
207
|
get: (key) => slotFor(key).read(),
|
|
172
208
|
set: (key, v) => {
|
|
@@ -206,6 +242,18 @@ export function createProjector(reg) {
|
|
|
206
242
|
if (o !== ABSENT) fn(key, o);
|
|
207
243
|
}
|
|
208
244
|
},
|
|
245
|
+
// Emit the overlaid keys as a patch stream fn(key, from, to). Untracked,
|
|
246
|
+
// read-only, zero-alloc per-key body. See forEachPatch above.
|
|
247
|
+
forEachPatch,
|
|
248
|
+
// Cold convenience over forEachPatch: materialize the drafts as
|
|
249
|
+
// [{ key, from, to }, ...] (same visit set, order, values). The
|
|
250
|
+
// per-key record is the documented allocation of this form; reach for
|
|
251
|
+
// forEachPatch when you need the zero-alloc callback.
|
|
252
|
+
toPatch: (skip) => {
|
|
253
|
+
const out = [];
|
|
254
|
+
forEachPatch((key, from, to) => { out.push({ key, from, to }); }, skip);
|
|
255
|
+
return out;
|
|
256
|
+
},
|
|
209
257
|
// Full-snapshot reconciliation: drop every overlay the policy considers
|
|
210
258
|
// confirmed against the CURRENT (untracked) source value. For sources that
|
|
211
259
|
// sync wholesale rather than per-key. Presentation-only -- the source owns
|
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ Bind the primitives to a lite-signal registry. Pass the default namespace for no
|
|
|
110
110
|
| `overlaidCount()` | untracked diagnostic: number of overlaid keys |
|
|
111
111
|
| `peek(key)` | untracked effective read (no subscribe) |
|
|
112
112
|
| `forEachOverlay(fn)` | iterate overlaid keys + values (untracked) |
|
|
113
|
+
| `forEachPatch(fn, skip?)` | emit staged drafts as a `(key, from, to)` stream (untracked, read-only, zero-alloc per key) <sub>1.2</sub> |
|
|
114
|
+
| `toPatch(skip?)` | materialize the drafts as `[{ key, from, to }, ...]` (cold convenience over `forEachPatch`) <sub>1.2</sub> |
|
|
113
115
|
| `reconcileAll(policy?)` | drop overlays the policy confirms against the current source |
|
|
114
116
|
| `prune()` | release slots for keys that are neither overlaid nor observed; returns how many were freed <sub>1.1</sub> |
|
|
115
117
|
| `dispose()` | recycle every projection-owned node back to the pool |
|
|
@@ -175,6 +177,30 @@ Projects a single query entry's data **object**, exposing its **fields** as the
|
|
|
175
177
|
|
|
176
178
|
The default merge copies **own enumerable** properties, symbols included, and defines them rather than assigning them. That matters for three field names you would otherwise lose silently: a field literally called `__proto__` lands as a real own key (assignment would retarget the prototype and drop it), inherited properties on `prev` are not absorbed into the record, and a symbol-keyed draft survives the commit instead of evaporating while `dirtyCount()` reports it saved. A custom `merge` is on its own for all three.
|
|
177
179
|
|
|
180
|
+
## Patch emission <sub>1.2</sub>
|
|
181
|
+
|
|
182
|
+
The overlay bag already knows every staged draft's `to`; `forEachPatch` adds the source's `from` so a draft can cross the wire without re-walking the view.
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const draft = project(source);
|
|
186
|
+
draft.set("name", "Ada");
|
|
187
|
+
draft.set("email", "ada@x.dev");
|
|
188
|
+
|
|
189
|
+
// Zero-alloc callback: hand each draft to a serializer / transport.
|
|
190
|
+
draft.forEachPatch((key, from, to) => {
|
|
191
|
+
wire.send({ op: "set", key, prev: from, next: to });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Cold convenience: materialize the same deltas as an array.
|
|
195
|
+
const patch = draft.toPatch(); // [{ key: "name", from: undefined, to: "Ada" }, ...]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`from` is the **untracked** current source value, `to` the staged overlay. Both methods are read-only and untracked -- calling them inside an effect subscribes it to nothing -- and visit exactly the overlaid keys, in `forEachOverlay` order. `forEachPatch` allocates nothing per key; `toPatch` is the cold convenience whose per-key record is its documented allocation.
|
|
199
|
+
|
|
200
|
+
An overlaid key is emitted whether or not `Object.is(from, to)`: the visit set stays equal to `dirtyCount()` and `commit()`'s write set, so a patch consumer (an LWW-Map op, a CRDT bump, an HTTP PATCH field) is never silently dropped. To suppress unchanged drafts, pass the same predicate shape reconcile uses -> `draft.forEachPatch(fn, confirmOnEcho)`. A throwing `source.get` propagates on the offending key with the overlay bag intact; callers needing atomicity use `toPatch()` (a partial array never escapes). The patch and `commit()` are two views of one delta: applying `toPatch()` to a copy of the source yields the same state `commit()` would write.
|
|
201
|
+
|
|
202
|
+
Present on the `projectStore` / `projectRoom` / `projectQuery` handles too (for `projectQuery`, `from` is the cached record's field value).
|
|
203
|
+
|
|
178
204
|
## Conventions
|
|
179
205
|
|
|
180
206
|
ESM only. ASCII source. `node:test`. MIT.
|
package/llms.txt
CHANGED
|
@@ -26,6 +26,7 @@ Peer dependency: @zakkster/lite-signal ^1.5.0 (uses createRoot). ESM only. MIT.
|
|
|
26
26
|
- createProjector(reg) -> { project, keyedStore } // bind to any lite-signal registry
|
|
27
27
|
- project(source) -> Projection // default registry
|
|
28
28
|
- keyedStore(initial?) -> { get, set, has, keys } // minimal built-in source
|
|
29
|
+
- VERSION -> string // shipped package version, synced to package.json
|
|
29
30
|
|
|
30
31
|
## Projection handle
|
|
31
32
|
|
|
@@ -33,6 +34,16 @@ get(key) | set(key,value) | clear(key) | commit(key?) [one key or all] | revert(
|
|
|
33
34
|
dirtyCount() [TRACKED reactive] | isDirty() [TRACKED reactive] |
|
|
34
35
|
isOverlaid(key) | overlaidCount() | peek(key) [untracked effective read] |
|
|
35
36
|
forEachOverlay(fn) | reconcileAll(policy?) | dispose() [recycle all owned nodes] |
|
|
37
|
+
forEachPatch(fn, skip?) [1.2] | toPatch(skip?) -> [{key,from,to}] [1.2]
|
|
38
|
+
[patch emission: iterate exactly the overlaid keys as fn(key, from, to) -- from =
|
|
39
|
+
UNTRACKED current source value, to = staged overlay. Read-only + untracked: safe
|
|
40
|
+
inside an effect, subscribes to nothing. Same visit set/order as forEachOverlay,
|
|
41
|
+
zero-alloc per-key body. Emits UNCHANGED drafts by default (visit set stays equal
|
|
42
|
+
to dirtyCount()); pass a reconcile-policy predicate (from,to,key)=>bool, e.g.
|
|
43
|
+
confirmOnEcho, to skip echoes. A throwing source.get propagates on that key with
|
|
44
|
+
the overlay bag intact. toPatch() is the cold convenience that materializes the
|
|
45
|
+
stream (its per-key record is the documented allocation). Present on the
|
|
46
|
+
projectStore/projectRoom/projectQuery handles too.] |
|
|
36
47
|
prune() -> number [1.1] [release slots that are BOTH un-overlaid AND unobserved;
|
|
37
48
|
returns how many were freed. A slot = 1 overlay signal + 1 projected computed,
|
|
38
49
|
created by the first READ of a key and retained until dispose() (its computed may
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-project",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -24,11 +24,18 @@
|
|
|
24
24
|
"LICENSE.txt"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"test": "node --test",
|
|
28
|
-
"
|
|
27
|
+
"test": "node --test test/*_test.mjs",
|
|
28
|
+
"torture": "node --expose-gc test/torture.mjs",
|
|
29
|
+
"verify": "npm test && npm run torture",
|
|
30
|
+
"prepublishOnly": "npm run verify"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
|
-
"@zakkster/lite-signal": "^1.
|
|
33
|
+
"@zakkster/lite-signal": "^1.5.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
37
|
+
"@zakkster/lite-leak": "^1.10.0",
|
|
38
|
+
"@zakkster/lite-signal": "^1.5.0"
|
|
32
39
|
},
|
|
33
40
|
"keywords": [
|
|
34
41
|
"reactive",
|
|
@@ -53,6 +60,10 @@
|
|
|
53
60
|
"url": "git+https://github.com/PeshoVurtoleta/lite-project.git"
|
|
54
61
|
},
|
|
55
62
|
"homepage": "https://github.com/PeshoVurtoleta/lite-project#readme",
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/PeshoVurtoleta/lite-project/issues",
|
|
65
|
+
"email": "shinikchiev@yahoo.com"
|
|
66
|
+
},
|
|
56
67
|
"funding": {
|
|
57
68
|
"type": "github",
|
|
58
69
|
"url": "https://github.com/sponsors/PeshoVurtoleta"
|