@mmstack/primitives 20.10.0 → 20.11.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/README.md +113 -2
- package/fesm2022/mmstack-primitives.mjs +1213 -230
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +552 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,10 +17,12 @@ 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
|
-
- [History & persistence](#history--persistence) — `withHistory`, `stored`, `tabSync`, `opLog`
|
|
23
|
+
- [History & persistence](#history--persistence) — `withHistory`, `storeHistory`, `stored`, `persistedStore`, `tabSync`, `opLog`
|
|
24
|
+
- [Sync & convergence](#sync--convergence) — `opSync`, `tabSync(store)`, merge policies (`lww`, `mergeThree`, `keyedArray`, `preserve`), `Conflicted`, `rebaseOps`, `policyStrategy`
|
|
25
|
+
- [Observability](#observability) — `provideConcurrencyInstrumentation`, `perfCustomTracks`
|
|
24
26
|
- [Performance helpers](#performance-helpers) — `chunked`, `pooled` / `pooledArray` / `pooledMap` / `pooledSet`
|
|
25
27
|
- [Sensors](#sensors) — `sensor()` facade + browser-state signals
|
|
26
28
|
- [Pipelines](#pipelines) — `piped` / `pipeable`, operators (`select`, `map`, `filter`, `filterWith`, `distinct`, `combineWith`, `tap`, `startWith`, `pairwise`, `scan`)
|
|
@@ -303,6 +305,32 @@ const controls = mapObject(
|
|
|
303
305
|
);
|
|
304
306
|
```
|
|
305
307
|
|
|
308
|
+
### `projection`
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { projection } from '@mmstack/primitives';
|
|
314
|
+
|
|
315
|
+
const users = signal<User[]>([]);
|
|
316
|
+
|
|
317
|
+
// return form: derive a filtered collection, reconciled by id
|
|
318
|
+
const active = projection<User[]>(() => users().filter((u) => u.active), [], {
|
|
319
|
+
key: 'id',
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// mutate form: update fields on the draft
|
|
323
|
+
const summary = projection<{ total: number; active: number }>(
|
|
324
|
+
(draft) => {
|
|
325
|
+
draft.total = users().length;
|
|
326
|
+
draft.active = users().filter((u) => u.active).length;
|
|
327
|
+
},
|
|
328
|
+
{ total: 0, active: 0 },
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
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.
|
|
333
|
+
|
|
306
334
|
## Effects
|
|
307
335
|
|
|
308
336
|
### `nestedEffect`
|
|
@@ -681,6 +709,7 @@ log.latest(); // Signal<OpBatch | null> — lossy sampling (devtools-style)
|
|
|
681
709
|
state.user.name.set('Bea');
|
|
682
710
|
// → { origin, version, ops: [{ kind: 'set', path: ['user','name'], next: 'Bea', prev: 'Ann' }] }
|
|
683
711
|
|
|
712
|
+
log.flush(); // synchronously emit any pending change now, instead of waiting for the tick. idempotent, no-op when clean.
|
|
684
713
|
log.apply(remoteBatch); // applies ops in ONE commit AND advances the diff baseline —
|
|
685
714
|
// so applying a remote batch emits no echo batch (sync loops terminate by construction)
|
|
686
715
|
invertBatch(batch); // prev-based inverse — undo is a data transform
|
|
@@ -688,6 +717,88 @@ invertBatch(batch); // prev-based inverse — undo is a data transform
|
|
|
688
717
|
|
|
689
718
|
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
719
|
|
|
720
|
+
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.
|
|
721
|
+
|
|
722
|
+
### `storeHistory`
|
|
723
|
+
|
|
724
|
+
Undo and redo for a store, over the op-log rather than value snapshots, so each entry costs only the diff. `undo()` applies one inverse batch; a new edit after an undo forks the timeline.
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
import { store, storeHistory } from '@mmstack/primitives';
|
|
728
|
+
|
|
729
|
+
const doc = store({ title: 'Draft', body: '' });
|
|
730
|
+
const history = storeHistory(doc);
|
|
731
|
+
|
|
732
|
+
doc.title.set('Final');
|
|
733
|
+
history.undo(); // title back to 'Draft'
|
|
734
|
+
history.canRedo(); // Signal<boolean>
|
|
735
|
+
|
|
736
|
+
storeHistory(doc, { track: syncClient }); // collaborative: only your own writes are undoable
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### `persistedStore`
|
|
740
|
+
|
|
741
|
+
Persists a whole store to an async backend (IndexedDB) and restores it on boot. It ships no IndexedDB code: you pass an `AsyncStore` adapter, which `idb-keyval` satisfies directly and a Dexie table satisfies with a few lines. Local durability, not sync (compose `tabSync` / `@mmstack/mesh` for that). Reads stay synchronous; because the backend is async, the store shows its initial value until the snapshot loads (`hydrated` is a signal you can gate on).
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
import * as idbKeyval from 'idb-keyval';
|
|
745
|
+
import { persistedStore, providePersistedStoreOptions } from '@mmstack/primitives';
|
|
746
|
+
|
|
747
|
+
providePersistedStoreOptions({ store: idbKeyval }); // wire the backend once
|
|
748
|
+
|
|
749
|
+
const draft = persistedStore({ title: '', body: '' }, { key: 'draft' });
|
|
750
|
+
draft.store.title.set('Hi'); // persisted (debounced), restored on next load
|
|
751
|
+
draft.hydrated(); // Signal<boolean>
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
When the persisted shape changes between releases, pass `version` and a `migrate` hook. An older snapshot is brought forward on boot before it is adopted, then re-persisted in the new shape (a newer snapshot than the running build is left untouched). Boot is already async, so `migrate` can be async, so the migration ladder can be lazy-loaded.
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
const profile = persistedStore({ first: '', last: '' }, {
|
|
758
|
+
key: 'profile',
|
|
759
|
+
version: 2,
|
|
760
|
+
migrate: async (data, from) => (await import('./migrations')).run(data, from),
|
|
761
|
+
});
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
`persistedStore` is `store()` + `persist()`. Reach for `persist(store, opt)` directly to add durability to a store you already have — one you also `meshSync`, or a worker-owned store's replica. Persistence is a reader over the op-log, so it composes with the other readers on the same store.
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
import { store, persist, meshSync } from '@mmstack/primitives';
|
|
768
|
+
|
|
769
|
+
const doc = store({ title: '', body: '' });
|
|
770
|
+
persist(doc, { key: 'draft', store: idbKeyval }); // durable to IndexedDB
|
|
771
|
+
meshSync(doc, { room: 'doc-42', writer, transport }); // and synced to peers
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
## Sync & convergence
|
|
775
|
+
|
|
776
|
+
The op-log is the substrate; these keep two copies of a store in agreement across a boundary (tabs, a worker, a network). `opSync` wires a store to a transport: local writes emit stamped envelopes, received envelopes fold in through a per-path last-writer-wins register map, ordered by a hybrid logical clock so any arrival order converges to the same state. `tabSync(store, { id })` is `opSync` over `BroadcastChannel` with a join handshake.
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
import { store, tabSync, keyedArray, preserve, isConflicted } from '@mmstack/primitives';
|
|
780
|
+
|
|
781
|
+
const board = tabSync(store({ title: 'Board', todos: [] }), {
|
|
782
|
+
id: 'board',
|
|
783
|
+
policies: [
|
|
784
|
+
{ path: 'todos', merge: keyedArray((t) => t.id) }, // reconcile a list by item identity
|
|
785
|
+
{ path: 'title', merge: preserve }, // keep both sides of a clash as data
|
|
786
|
+
],
|
|
787
|
+
});
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
A **merge policy** decides the result when two peers change one path at once: `lww` (default), `mergeThree` (three-way against the common ancestor), `keyedArray(idFn)` (list reconcile by identity), or `preserve` (both sides survive as a `Conflicted` value; `isConflicted(v)` narrows it, resolution is a later write). `rebaseOps(root, pending, remote, policies)` is the pure invert-apply-reapply routine behind optimistic updates and offline queues, and `policyStrategy(policies)` gives a `forkStore` the same per-path resolution. This is what [`@mmstack/mesh`](https://www.npmjs.com/package/@mmstack/mesh) wraps for multiplayer.
|
|
791
|
+
|
|
792
|
+
## Observability
|
|
793
|
+
|
|
794
|
+
An optional listener seam on the concurrency layer. `provideConcurrencyInstrumentation(listener)` receives events as transition scopes coordinate pending, suspense, and transaction windows; with no listener the taps are no-ops. `perfCustomTracks()` is a ready listener that writes each window to a Chrome DevTools Performance track, and the window hooks are span-shaped, so forwarding to [`@mmstack/telemetry-core`](https://www.npmjs.com/package/@mmstack/telemetry-core) is a direct mapping.
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
import { provideConcurrencyInstrumentation, perfCustomTracks } from '@mmstack/primitives';
|
|
798
|
+
|
|
799
|
+
providers: [provideConcurrencyInstrumentation(perfCustomTracks())];
|
|
800
|
+
```
|
|
801
|
+
|
|
691
802
|
## Performance helpers
|
|
692
803
|
|
|
693
804
|
### `chunked`
|