@mmstack/primitives 20.10.1 → 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 CHANGED
@@ -20,7 +20,9 @@ npm install @mmstack/primitives
20
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`)
@@ -717,6 +719,86 @@ Batching is per tick (two writes to one leaf in a tick emit one composed op), `p
717
719
 
718
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.
719
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
+
720
802
  ## Performance helpers
721
803
 
722
804
  ### `chunked`