@mmstack/primitives 20.10.0 → 20.10.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/README.md +30 -1
- package/fesm2022/mmstack-primitives.mjs +216 -24
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +112 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install @mmstack/primitives
|
|
|
17
17
|
|
|
18
18
|
- [Writable signal variants](#writable-signal-variants) — `mutable`, `derived`, `store` / `mutableStore`, `forkStore`, `toWritable`
|
|
19
19
|
- [Timing & propagation](#timing--propagation) — `debounced`, `throttled`, `until`
|
|
20
|
-
- [Reactive collections](#reactive-collections) — `indexArray`, `keyArray`, `mapObject`
|
|
20
|
+
- [Reactive collections](#reactive-collections) — `indexArray`, `keyArray`, `mapObject`, `projection`
|
|
21
21
|
- [Effects](#effects) — `nestedEffect`
|
|
22
22
|
- [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*` / `providePausableOptions`, Suspense (`mm-suspense`), hold-and-swap (`*mmTransition`), per-element morphs (`mmViewTransitionName`), async derivations (`latest` / `use`), `deferredValue`, `startTransition` / `startTransaction`, `holdUntilReady`
|
|
23
23
|
- [History & persistence](#history--persistence) — `withHistory`, `stored`, `tabSync`, `opLog`
|
|
@@ -303,6 +303,32 @@ const controls = mapObject(
|
|
|
303
303
|
);
|
|
304
304
|
```
|
|
305
305
|
|
|
306
|
+
### `projection`
|
|
307
|
+
|
|
308
|
+
A derived **store**, the store-shaped counterpart to `computed`. Where `derived` slices one value out of a source and `indexArray` / `keyArray` map a list, `projection` derives a whole store subtree from a computation. `fn` receives a mutable draft seeded with the current value and either mutates it or returns new data; the result is reconciled against the previous value so unchanged object subtrees keep their reference and keyed array items keep their identity across recomputes. Reading through the returned store is per-leaf, so a `computed` over one field only recomputes when that field actually changes, even though the whole projection re-ran.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { projection } from '@mmstack/primitives';
|
|
312
|
+
|
|
313
|
+
const users = signal<User[]>([]);
|
|
314
|
+
|
|
315
|
+
// return form: derive a filtered collection, reconciled by id
|
|
316
|
+
const active = projection<User[]>(() => users().filter((u) => u.active), [], {
|
|
317
|
+
key: 'id',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// mutate form: update fields on the draft
|
|
321
|
+
const summary = projection<{ total: number; active: number }>(
|
|
322
|
+
(draft) => {
|
|
323
|
+
draft.total = users().length;
|
|
324
|
+
draft.active = users().filter((u) => u.active).length;
|
|
325
|
+
},
|
|
326
|
+
{ total: 0, active: 0 },
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Recompute is pull-based, exactly like `computed`: memoized, re-run on the first read after a dependency changes, coherent immediately after a write (no waiting on an effect flush), and skipped entirely while nobody reads. `fn` must be pure since it runs inside the reactive computation. Prefer `computed` for a plain value, and reach for `projection` when you want the per-property tracking of a store on top of a derivation. The standalone `reconcile(prev, next, key)` is exported too, for producing a reference-stable value by hand. Values must be structured-clonable (the draft is a clone of the current value). With an explicit store context (`createStoreContext()`) a projection is injector-free, so it also runs on a worker host.
|
|
331
|
+
|
|
306
332
|
## Effects
|
|
307
333
|
|
|
308
334
|
### `nestedEffect`
|
|
@@ -681,6 +707,7 @@ log.latest(); // Signal<OpBatch | null> — lossy sampling (devtools-style)
|
|
|
681
707
|
state.user.name.set('Bea');
|
|
682
708
|
// → { origin, version, ops: [{ kind: 'set', path: ['user','name'], next: 'Bea', prev: 'Ann' }] }
|
|
683
709
|
|
|
710
|
+
log.flush(); // synchronously emit any pending change now, instead of waiting for the tick. idempotent, no-op when clean.
|
|
684
711
|
log.apply(remoteBatch); // applies ops in ONE commit AND advances the diff baseline —
|
|
685
712
|
// so applying a remote batch emits no echo batch (sync loops terminate by construction)
|
|
686
713
|
invertBatch(batch); // prev-based inverse — undo is a data transform
|
|
@@ -688,6 +715,8 @@ invertBatch(batch); // prev-based inverse — undo is a data transform
|
|
|
688
715
|
|
|
689
716
|
Batching is per tick (two writes to one leaf in a tick emit one composed op), `prev` is always carried in-memory (structural sharing makes it free — wire serializers decide whether to keep it), arrays diff per-index at equal lengths and as whole-array ops on length change, and a `forkStore`'s `commit()` lands as a single batch — fork *is* the transaction primitive. Mutable stores are unsupported (in-place mutation defeats ref-identity diffing; dev warn). This is the substrate for worker mirrors, tab/mesh sync, persistence journals, and undo — one protocol, many consumers.
|
|
690
717
|
|
|
718
|
+
An `opLog` can also run with no Angular injector, which is what lets the graph mirror into a Web Worker. Pass `driver: microtaskOpLogDriver()` to drive emission off the microtask queue instead of an `effect()`, and build the store with `createStoreContext()` (a self-contained proxy cache) so `store` and `opLog` work in a worker or a plain Node process. The pure helpers `applyOps(root, ops)` and `diffOps(prev, next)` apply and produce batches without owning a log. [`@mmstack/worker`](https://www.npmjs.com/package/@mmstack/worker) is built directly on these seams.
|
|
719
|
+
|
|
691
720
|
## Performance helpers
|
|
692
721
|
|
|
693
722
|
### `chunked`
|
|
@@ -4290,7 +4290,11 @@ function buildChildNode(target, prop, isMutableSource, options) {
|
|
|
4290
4290
|
function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...rest } = {}) {
|
|
4291
4291
|
if (isStore(source))
|
|
4292
4292
|
return source;
|
|
4293
|
-
if
|
|
4293
|
+
// injector is needed ONLY to resolve the two proxy-globals tokens; if a caller supplies the
|
|
4294
|
+
// globals directly (createStoreContext — the worker-side seam with no DI), skip inject entirely
|
|
4295
|
+
const sharedGlobals = rest[STORE_SHARED_GLOBALS];
|
|
4296
|
+
const hasSharedGlobals = !!(sharedGlobals?.cache && sharedGlobals?.registry);
|
|
4297
|
+
if (!injector && !hasSharedGlobals)
|
|
4294
4298
|
injector = inject(Injector);
|
|
4295
4299
|
const writableSource = isWritableSignal(source)
|
|
4296
4300
|
? source
|
|
@@ -4308,13 +4312,18 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
|
|
|
4308
4312
|
return 'primitive';
|
|
4309
4313
|
}, ...(ngDevMode ? [{ debugName: "kind" }] : []));
|
|
4310
4314
|
const STORE_OPTIONS = {
|
|
4311
|
-
|
|
4315
|
+
// may be undefined in worker/DI-less mode; unused downstream once globals are resolved
|
|
4316
|
+
// (children thread the resolved globals via STORE_SHARED_OPTIONS, derived needs no injector)
|
|
4317
|
+
injector: injector,
|
|
4312
4318
|
vivify,
|
|
4313
4319
|
noUnionLeaves,
|
|
4314
4320
|
[STORE_SHARED_GLOBALS]: {
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4321
|
+
// the `injector!` reads run only when a global is absent, which (per hasSharedGlobals) means
|
|
4322
|
+
// an injector was resolved above
|
|
4323
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4324
|
+
cache: sharedGlobals?.cache ?? injector.get(PROXY_CACHE_TOKEN),
|
|
4325
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4326
|
+
registry: sharedGlobals?.registry ?? injector.get(PROXY_CLEANUP_TOKEN),
|
|
4318
4327
|
},
|
|
4319
4328
|
};
|
|
4320
4329
|
// built lazily so non-array nodes never allocate it
|
|
@@ -4386,7 +4395,14 @@ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...r
|
|
|
4386
4395
|
return () => {
|
|
4387
4396
|
if (!isWritableSource)
|
|
4388
4397
|
return s;
|
|
4389
|
-
return untracked(() => toStore(source.asReadonly(), {
|
|
4398
|
+
return untracked(() => toStore(source.asReadonly(), {
|
|
4399
|
+
injector,
|
|
4400
|
+
vivify,
|
|
4401
|
+
noUnionLeaves,
|
|
4402
|
+
// forward the resolved globals — re-resolving from the injector both re-injects
|
|
4403
|
+
// needlessly and breaks in DI-less (worker) mode where injector is undefined
|
|
4404
|
+
[STORE_SHARED_GLOBALS]: STORE_OPTIONS[STORE_SHARED_GLOBALS],
|
|
4405
|
+
}));
|
|
4390
4406
|
};
|
|
4391
4407
|
const k = untracked(kind);
|
|
4392
4408
|
if (prop === 'extend' && k !== 'array')
|
|
@@ -4551,6 +4567,40 @@ function mutableStore(value, opt) {
|
|
|
4551
4567
|
...opt,
|
|
4552
4568
|
});
|
|
4553
4569
|
}
|
|
4570
|
+
/**
|
|
4571
|
+
* Builds a DI-less store context — the shared proxy-cache and cleanup registry that {@link toStore}
|
|
4572
|
+
* normally resolves from the injector — so a `store`/`toStore`/`opLog` graph can run with NO Angular
|
|
4573
|
+
* injection context. Spread the result into the options:
|
|
4574
|
+
*
|
|
4575
|
+
* ```ts
|
|
4576
|
+
* import { microtaskOpLogDriver } from '@mmstack/worker/host';
|
|
4577
|
+
* const ctx = createStoreContext();
|
|
4578
|
+
* const s = store({ todos: [] }, ctx);
|
|
4579
|
+
* const log = opLog(s, { driver: microtaskOpLogDriver(), origin: 'worker' }); // no injector anywhere
|
|
4580
|
+
* ```
|
|
4581
|
+
*
|
|
4582
|
+
* **This is a worker-only fallback — do NOT use it on the main thread.** DI is the default and
|
|
4583
|
+
* correct path in an app: the injector scopes the proxy-cache/cleanup singletons per app instance,
|
|
4584
|
+
* which on the SERVER keeps one request's store identity from bleeding into another's (the exact
|
|
4585
|
+
* hazard a module-scope singleton would reintroduce). A Web Worker is safe because it is a single
|
|
4586
|
+
* store graph per thread and never runs during SSR (spawn is a `PLATFORM_ID === 'server'` no-op),
|
|
4587
|
+
* so there is no cross-request scope to contaminate. Never hoist a `createStoreContext()` to module
|
|
4588
|
+
* scope on a shared/main thread.
|
|
4589
|
+
*
|
|
4590
|
+
* **Share ONE context across every store in a worker** — the same way `providedIn: 'root'` shares
|
|
4591
|
+
* one cache across all of an app's stores. `@mmstack/worker/host` memoizes this per worker
|
|
4592
|
+
* (`workerStoreContext()`); reach for `createStoreContext()` directly only in a bare
|
|
4593
|
+
* (non-worker-host) DI-less setup, and hold the single instance yourself.
|
|
4594
|
+
*/
|
|
4595
|
+
function createStoreContext() {
|
|
4596
|
+
const cache = new WeakMap();
|
|
4597
|
+
const registry = new FinalizationRegistry(({ target, prop }) => {
|
|
4598
|
+
const entry = cache.get(target);
|
|
4599
|
+
if (entry)
|
|
4600
|
+
entry.delete(prop);
|
|
4601
|
+
});
|
|
4602
|
+
return { [STORE_SHARED_GLOBALS]: { cache, registry } };
|
|
4603
|
+
}
|
|
4554
4604
|
|
|
4555
4605
|
function isPlainRecord(value) {
|
|
4556
4606
|
if (value === null || typeof value !== 'object')
|
|
@@ -4633,7 +4683,7 @@ function generateOrigin() {
|
|
|
4633
4683
|
return globalThis.crypto.randomUUID();
|
|
4634
4684
|
return Math.random().toString(36).substring(2);
|
|
4635
4685
|
}
|
|
4636
|
-
const isPlainArray = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4686
|
+
const isPlainArray$1 = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4637
4687
|
/**
|
|
4638
4688
|
* Reference-identity-pruned structural diff — the same short-circuit discipline as `merge3`:
|
|
4639
4689
|
* an untouched subtree kept its reference (the store's copy-on-write contract), so the walk
|
|
@@ -4658,7 +4708,7 @@ function diffNode(prev, next, path, ops) {
|
|
|
4658
4708
|
}
|
|
4659
4709
|
return;
|
|
4660
4710
|
}
|
|
4661
|
-
if (isPlainArray(prev) && isPlainArray(next)) {
|
|
4711
|
+
if (isPlainArray$1(prev) && isPlainArray$1(next)) {
|
|
4662
4712
|
// same length → per-index descent (matches `arr[i].x.set(...)` writes); a length
|
|
4663
4713
|
// change is a whole unit — index attribution lies under insert/remove/reorder
|
|
4664
4714
|
if (prev.length === next.length) {
|
|
@@ -4675,7 +4725,7 @@ function diffNode(prev, next, path, ops) {
|
|
|
4675
4725
|
/** Immutably applies one op along its path, vivifying missing containers `'auto'`-style. */
|
|
4676
4726
|
function applyAt(container, path, idx, op) {
|
|
4677
4727
|
const seg = path[idx];
|
|
4678
|
-
const base = isPlainArray(container)
|
|
4728
|
+
const base = isPlainArray$1(container)
|
|
4679
4729
|
? container.slice()
|
|
4680
4730
|
: isRecord(container)
|
|
4681
4731
|
? { ...container }
|
|
@@ -4695,6 +4745,37 @@ function applyAt(container, path, idx, op) {
|
|
|
4695
4745
|
base[seg] = applyAt(base[seg], path, idx + 1, op);
|
|
4696
4746
|
return base;
|
|
4697
4747
|
}
|
|
4748
|
+
/**
|
|
4749
|
+
* Pure, store-free application of ops onto a plain root value, returning the next immutable root
|
|
4750
|
+
* (structural-sharing along op paths, missing containers vivified `'auto'`-style). This is the
|
|
4751
|
+
* same transform {@link OpLog.apply} runs, extracted so a replica can fold a received batch into
|
|
4752
|
+
* a value WITHOUT owning a diffing {@link opLog} — e.g. the worker-graph read-replica seam.
|
|
4753
|
+
* Accepts a batch or a bare op list.
|
|
4754
|
+
*/
|
|
4755
|
+
function applyOps(root, ops) {
|
|
4756
|
+
const list = Array.isArray(ops) ? ops : ops.ops;
|
|
4757
|
+
let next = root;
|
|
4758
|
+
for (const op of list) {
|
|
4759
|
+
if (op.path.length === 0) {
|
|
4760
|
+
if (op.kind === 'set')
|
|
4761
|
+
next = op.next;
|
|
4762
|
+
continue; // a root delete is meaningless — ignore (mirrors OpLog.apply)
|
|
4763
|
+
}
|
|
4764
|
+
next = applyAt(next, op.path, 0, op);
|
|
4765
|
+
}
|
|
4766
|
+
return next;
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Pure reference-pruned structural diff of two roots into minimal ops (the emission core of
|
|
4770
|
+
* {@link opLog}, exported so code outside a log can produce a batch — e.g. diffing a scratch
|
|
4771
|
+
* draft against a replica's current value to route a write to its owner). Trusts the
|
|
4772
|
+
* copy-on-write contract: an untouched subtree that kept its reference is skipped.
|
|
4773
|
+
*/
|
|
4774
|
+
function diffOps(prev, next) {
|
|
4775
|
+
const ops = [];
|
|
4776
|
+
diffNode(prev, next, [], ops);
|
|
4777
|
+
return ops;
|
|
4778
|
+
}
|
|
4698
4779
|
/**
|
|
4699
4780
|
* Inverts a batch for undo: reversed order, `set`↔its own inverse (an add — a `set` with no
|
|
4700
4781
|
* `prev` — inverts to a `delete`; a `delete` inverts to a `set` restoring `prev`). Feed the
|
|
@@ -4707,14 +4788,24 @@ function invertBatch(batch) {
|
|
|
4707
4788
|
for (let i = ops.length - 1; i >= 0; i--) {
|
|
4708
4789
|
const op = ops[i];
|
|
4709
4790
|
if (op.kind === 'delete') {
|
|
4710
|
-
inverted.push({
|
|
4791
|
+
inverted.push({
|
|
4792
|
+
kind: 'set',
|
|
4793
|
+
path: op.path,
|
|
4794
|
+
next: op.prev,
|
|
4795
|
+
prev: undefined,
|
|
4796
|
+
});
|
|
4711
4797
|
continue;
|
|
4712
4798
|
}
|
|
4713
4799
|
if (!Object.hasOwn(op, 'prev')) {
|
|
4714
4800
|
inverted.push({ kind: 'delete', path: op.path, prev: op.next });
|
|
4715
4801
|
}
|
|
4716
4802
|
else {
|
|
4717
|
-
inverted.push({
|
|
4803
|
+
inverted.push({
|
|
4804
|
+
kind: 'set',
|
|
4805
|
+
path: op.path,
|
|
4806
|
+
next: op.prev,
|
|
4807
|
+
prev: op.next,
|
|
4808
|
+
});
|
|
4718
4809
|
}
|
|
4719
4810
|
}
|
|
4720
4811
|
return inverted;
|
|
@@ -4743,7 +4834,6 @@ function invertBatch(batch) {
|
|
|
4743
4834
|
* ```
|
|
4744
4835
|
*/
|
|
4745
4836
|
function opLog(source, opt) {
|
|
4746
|
-
const injector = opt?.injector ?? inject(Injector);
|
|
4747
4837
|
const origin = opt?.origin ?? generateOrigin();
|
|
4748
4838
|
// a store proxy's `has` trap answers for the VALUE's keys, so `isMutable`'s `'mutate' in`
|
|
4749
4839
|
// probe can't see the brand — ask the store's own kind symbol first
|
|
@@ -4774,16 +4864,24 @@ function opLog(source, opt) {
|
|
|
4774
4864
|
for (const cb of [...subscribers])
|
|
4775
4865
|
cb(batch);
|
|
4776
4866
|
};
|
|
4777
|
-
const
|
|
4867
|
+
const run = () => {
|
|
4778
4868
|
source(); // track every commit…
|
|
4779
4869
|
untracked(flush); // …and emit the delta since the last flush
|
|
4780
|
-
}
|
|
4870
|
+
};
|
|
4871
|
+
// default driver is an Angular effect (needs an injector); a supplied driver runs injector-free
|
|
4872
|
+
// (the worker-side seam, e.g. microtaskOpLogDriver from @mmstack/worker/host)
|
|
4873
|
+
const ref = opt?.driver
|
|
4874
|
+
? opt.driver(run)
|
|
4875
|
+
: effect(run, { injector: opt?.injector ?? inject(Injector) });
|
|
4781
4876
|
return {
|
|
4782
4877
|
latest: latest.asReadonly(),
|
|
4783
4878
|
subscribe: (cb) => {
|
|
4784
4879
|
subscribers.add(cb);
|
|
4785
4880
|
return () => subscribers.delete(cb);
|
|
4786
4881
|
},
|
|
4882
|
+
// the emission core, callable on demand — reads the source untracked, so it never disturbs the
|
|
4883
|
+
// driver's subscription; a subsequent scheduled run just finds the baseline already advanced
|
|
4884
|
+
flush: () => flush(),
|
|
4787
4885
|
apply: (batchOrOps) => {
|
|
4788
4886
|
const ops = Array.isArray(batchOrOps)
|
|
4789
4887
|
? batchOrOps
|
|
@@ -4792,15 +4890,7 @@ function opLog(source, opt) {
|
|
|
4792
4890
|
return;
|
|
4793
4891
|
// pending local writes must emit BEFORE the baseline advances past them
|
|
4794
4892
|
flush();
|
|
4795
|
-
|
|
4796
|
-
for (const op of ops) {
|
|
4797
|
-
if (op.path.length === 0) {
|
|
4798
|
-
if (op.kind === 'set')
|
|
4799
|
-
root = op.next;
|
|
4800
|
-
continue; // a root delete is meaningless — ignore
|
|
4801
|
-
}
|
|
4802
|
-
root = applyAt(root, op.path, 0, op);
|
|
4803
|
-
}
|
|
4893
|
+
const root = applyOps(untracked(source), ops); // one atomic root, structural-shared
|
|
4804
4894
|
source.set(root);
|
|
4805
4895
|
prevRoot = root; // baseline advance: an applied batch never echoes
|
|
4806
4896
|
},
|
|
@@ -4812,6 +4902,108 @@ function opLog(source, opt) {
|
|
|
4812
4902
|
};
|
|
4813
4903
|
}
|
|
4814
4904
|
|
|
4905
|
+
const isPlainArray = (v) => Array.isArray(v) && !isOpaque(v);
|
|
4906
|
+
function keyOf(item, key) {
|
|
4907
|
+
if (typeof key === 'function')
|
|
4908
|
+
return key(item);
|
|
4909
|
+
return isRecord(item) ? item[key] : item;
|
|
4910
|
+
}
|
|
4911
|
+
/**
|
|
4912
|
+
* Produces a value equal to `next` but sharing as much of `prev`'s reference structure as possible:
|
|
4913
|
+
* an object subtree that did not change keeps its `prev` reference, and array items are matched by
|
|
4914
|
+
* `key` so a surviving item keeps its identity across a reorder/insert/remove (only added items are
|
|
4915
|
+
* new, only removed items are dropped). This is what lets a derived store recompute without tearing
|
|
4916
|
+
* down every downstream `computed` that reads an unchanged part of it.
|
|
4917
|
+
*/
|
|
4918
|
+
function reconcile(prev, next, key = 'id') {
|
|
4919
|
+
return reconcileValue(prev, next, key);
|
|
4920
|
+
}
|
|
4921
|
+
function reconcileValue(prev, next, key) {
|
|
4922
|
+
if (Object.is(prev, next))
|
|
4923
|
+
return prev;
|
|
4924
|
+
if (isPlainArray(prev) && isPlainArray(next)) {
|
|
4925
|
+
const byKey = new Map();
|
|
4926
|
+
for (const item of prev)
|
|
4927
|
+
byKey.set(keyOf(item, key), item);
|
|
4928
|
+
let changed = prev.length !== next.length;
|
|
4929
|
+
const out = next.map((item, i) => {
|
|
4930
|
+
const match = byKey.get(keyOf(item, key));
|
|
4931
|
+
const rv = match !== undefined ? reconcileValue(match, item, key) : item;
|
|
4932
|
+
if (rv !== prev[i])
|
|
4933
|
+
changed = true;
|
|
4934
|
+
return rv;
|
|
4935
|
+
});
|
|
4936
|
+
return changed ? out : prev;
|
|
4937
|
+
}
|
|
4938
|
+
if (isRecord(prev) && isRecord(next)) {
|
|
4939
|
+
const nextKeys = Object.keys(next);
|
|
4940
|
+
let changed = Object.keys(prev).length !== nextKeys.length;
|
|
4941
|
+
const out = {};
|
|
4942
|
+
for (const k of nextKeys) {
|
|
4943
|
+
const rv = Object.hasOwn(prev, k)
|
|
4944
|
+
? reconcileValue(prev[k], next[k], key)
|
|
4945
|
+
: next[k];
|
|
4946
|
+
out[k] = rv;
|
|
4947
|
+
if (rv !== prev[k])
|
|
4948
|
+
changed = true;
|
|
4949
|
+
}
|
|
4950
|
+
return changed ? out : prev;
|
|
4951
|
+
}
|
|
4952
|
+
return next;
|
|
4953
|
+
}
|
|
4954
|
+
/**
|
|
4955
|
+
* A derived STORE, the store-shaped counterpart to `computed`. `fn` receives a mutable draft seeded
|
|
4956
|
+
* with the current value and either mutates it in place or returns a new value; whichever it does,
|
|
4957
|
+
* the result is reconciled against the previous value (see {@link reconcile}) so unchanged subtrees
|
|
4958
|
+
* keep reference identity and keyed array items keep their proxy identity. Reading through the
|
|
4959
|
+
* returned store is fine-grained: a `computed` over one field only recomputes when that field
|
|
4960
|
+
* actually changes, even though the whole projection re-ran.
|
|
4961
|
+
*
|
|
4962
|
+
* Recompute is pull-based, exactly like `computed`: the projection is memoized and re-runs on the
|
|
4963
|
+
* first read after a signal `fn` depends on changes, so reads are always coherent (no waiting on an
|
|
4964
|
+
* effect flush) and nothing recomputes while nobody reads. `fn` must be pure, it runs inside the
|
|
4965
|
+
* reactive computation. Prefer `computed` for a plain value; reach for `projection` when you want
|
|
4966
|
+
* the per-property tracking of a store on top of a derivation.
|
|
4967
|
+
*
|
|
4968
|
+
* ```ts
|
|
4969
|
+
* const active = projection<User[]>(() => users().filter((u) => u.active), [], { key: 'id' });
|
|
4970
|
+
* // active[0].name(); — surviving users keep identity across recomputes
|
|
4971
|
+
* ```
|
|
4972
|
+
*
|
|
4973
|
+
* Needs an injection context (or an explicit `injector`) for the store layer's cleanup on the main
|
|
4974
|
+
* thread; with an explicit store context (`createStoreContext()`) it is injector-free, so it also
|
|
4975
|
+
* runs on a worker host.
|
|
4976
|
+
*
|
|
4977
|
+
* @param fn receives the current draft; mutate it, or return new data.
|
|
4978
|
+
* @param seed the initial value, held before the first run.
|
|
4979
|
+
*/
|
|
4980
|
+
function projection(fn, seed, opt) {
|
|
4981
|
+
const { key = 'id', ...storeOpt } = opt ?? {};
|
|
4982
|
+
// linkedSignal rather than an effect-driven signal: the computation runs in the tracked
|
|
4983
|
+
// context (fn's reads are dependencies) and `previous` hands back the last emitted value for
|
|
4984
|
+
// the reconcile, so the projection is glitch-free, lazy, and needs no effect scheduler.
|
|
4985
|
+
const root = linkedSignal(...(ngDevMode ? [{ debugName: "root", source: () => undefined,
|
|
4986
|
+
computation: (_, previous) => {
|
|
4987
|
+
const base = previous ? previous.value : seed;
|
|
4988
|
+
// a plain mutable scratch seeded with the current value; fn mutates it or returns new data
|
|
4989
|
+
const draft = structuredClone(base);
|
|
4990
|
+
const returned = fn(draft);
|
|
4991
|
+
const next = (returned === undefined ? draft : returned);
|
|
4992
|
+
return reconcile(base, next, key);
|
|
4993
|
+
} }] : [{
|
|
4994
|
+
source: () => undefined,
|
|
4995
|
+
computation: (_, previous) => {
|
|
4996
|
+
const base = previous ? previous.value : seed;
|
|
4997
|
+
// a plain mutable scratch seeded with the current value; fn mutates it or returns new data
|
|
4998
|
+
const draft = structuredClone(base);
|
|
4999
|
+
const returned = fn(draft);
|
|
5000
|
+
const next = (returned === undefined ? draft : returned);
|
|
5001
|
+
return reconcile(base, next, key);
|
|
5002
|
+
},
|
|
5003
|
+
}]));
|
|
5004
|
+
return toStore(root, storeOpt).asReadonlyStore();
|
|
5005
|
+
}
|
|
5006
|
+
|
|
4815
5007
|
/**
|
|
4816
5008
|
* @internal The plain-`effect` sibling of the public {@link pausableEffect} (which is built on
|
|
4817
5009
|
* `nestedEffect`). For infra utilities that own a single top-level effect/subscription and don't
|
|
@@ -5346,5 +5538,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
5346
5538
|
* Generated bundle index. Do not edit.
|
|
5347
5539
|
*/
|
|
5348
5540
|
|
|
5349
|
-
export { MmActivity, MmTransition, MmViewTransitionName, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, invertBatch, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, latest, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opLog, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
|
|
5541
|
+
export { MmActivity, MmTransition, MmViewTransitionName, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, invertBatch, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, latest, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opLog, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, projection, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, reconcile, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, use, windowSize, withHistory };
|
|
5350
5542
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|