@mmstack/primitives 20.15.3 → 20.16.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 +22 -1
- package/fesm2022/mmstack-primitives.mjs +122 -10
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +90 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install @mmstack/primitives
|
|
|
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`, `storeHistory`, `stored`, `persistedStore`, `tabSync`, `opLog`
|
|
24
|
-
- [Sync & convergence](#sync--convergence) — `opSync`, `tabSync(store)`, merge policies (`lww`, `mergeThree`, `keyedArray`, `preserve`), `Conflicted`, keyed containers (`
|
|
24
|
+
- [Sync & convergence](#sync--convergence) — `opSync`, `tabSync(store)`, merge policies (`lww`, `mergeThree`, `keyedArray`, `preserve`), `Conflicted`, keyed containers (`keyedContainer`, `wrappedContainer`, `orderedEntries`, `posBetween`), `rebaseOps`, `policyStrategy`, `syncedFork`
|
|
25
25
|
- [Observability](#observability) — `provideConcurrencyInstrumentation`, `perfCustomTracks`
|
|
26
26
|
- [Performance helpers](#performance-helpers) — `chunked`, `pooled` / `pooledArray` / `pooledMap` / `pooledSet`
|
|
27
27
|
- [Sensors](#sensors) — `sensor()` facade + browser-state signals
|
|
@@ -369,8 +369,11 @@ The foundation of stale-while-revalidate. Wraps a signal so it **holds its last
|
|
|
369
369
|
import { keepPrevious } from '@mmstack/primitives';
|
|
370
370
|
|
|
371
371
|
const held = keepPrevious(resource.value); // drops to undefined mid-reload → keeps last value
|
|
372
|
+
const rows = keepPrevious(resource.value, { fallback: [] }); // [] only until the first value lands
|
|
372
373
|
```
|
|
373
374
|
|
|
375
|
+
`fallback` is yielded only while nothing has ever been defined; after the first defined value the previous value covers every gap, never the fallback. Like any linked signal the hold is lazy: it carries a value it has computed with, so place it over the value your readers read — reading is what feeds it. `@mmstack/resource` does exactly that for its `keepPrevious` option.
|
|
376
|
+
|
|
374
377
|
If the source is writable, `set` / `update` / `asReadonly` (and `mutate` / `inline` / `from` for mutable / derived sources) are forwarded through, so it stays a drop-in replacement. `@mmstack/resource` uses it under the hood for its `keepPrevious` option.
|
|
375
378
|
|
|
376
379
|
### Keep-alive — `MmActivity` / `injectPaused` / `providePaused`
|
|
@@ -792,6 +795,24 @@ const board = tabSync(store({ title: 'Board', todos: [] }), {
|
|
|
792
795
|
|
|
793
796
|
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.
|
|
794
797
|
|
|
798
|
+
### Keyed containers
|
|
799
|
+
|
|
800
|
+
**A list several peers reorder is a record keyed by element id, never an array.** Each element carries a fractional position at `~pos`, so an insert is one write at `[list, id]` and a move is one write at `[list, id, '~pos']` — two peers inserting into the same list at once keep both elements, where one whole-array write would have folded over the other.
|
|
801
|
+
|
|
802
|
+
```typescript
|
|
803
|
+
import { keyedContainer } from '@mmstack/primitives';
|
|
804
|
+
|
|
805
|
+
const board = store<{ todos: Record<string, Todo> }>({ todos: {} });
|
|
806
|
+
const todos = keyedContainer({ key: (t: Todo) => t.id }); // or pass the key to insert
|
|
807
|
+
|
|
808
|
+
todos.insert(board.todos, { id: 't1', title: 'Ship it' }, 0);
|
|
809
|
+
todos.move(board.todos, 't1', 3); // writes the position and nothing else
|
|
810
|
+
todos.entries(board.todos()); // reading order: by ~pos, key breaking ties
|
|
811
|
+
todos.rebalance(sync, board.todos); // authority sweep when positions grow long
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Reading order is a pure function of the materialized value, so every replica agrees without consulting the op log. `wrappedContainer` stores elements as `{ '~pos', value }` instead, keeping the payload a closed record a schema can validate; the choice is fixed when the container is created and never inferred from data, so peers of a synced container must agree on it. `posBetween(before, after)` is the fractional index underneath.
|
|
815
|
+
|
|
795
816
|
## Observability
|
|
796
817
|
|
|
797
818
|
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.
|
|
@@ -1917,18 +1917,27 @@ function createSetter(source) {
|
|
|
1917
1917
|
|
|
1918
1918
|
function keepPrevious(src, opt) {
|
|
1919
1919
|
const mutableSrc = isWritableSignal(src) && isMutable(src);
|
|
1920
|
+
const { fallback, ...signalOpt } = opt ?? {};
|
|
1920
1921
|
let cnt = 0;
|
|
1921
1922
|
const baseEqual = opt?.equal;
|
|
1922
1923
|
const equal = mutableSrc
|
|
1923
1924
|
? (a, b) => cnt > 0 ? false : baseEqual ? baseEqual(a, b) : Object.is(a, b)
|
|
1924
1925
|
: baseEqual;
|
|
1925
|
-
const persisted = linkedSignal(...(ngDevMode ? [{ debugName: "persisted", ...
|
|
1926
|
+
const persisted = linkedSignal(...(ngDevMode ? [{ debugName: "persisted", ...signalOpt,
|
|
1926
1927
|
source: () => src(),
|
|
1927
|
-
computation: (next, prev) =>
|
|
1928
|
+
computation: (next, prev) => {
|
|
1929
|
+
if (next !== undefined)
|
|
1930
|
+
return next;
|
|
1931
|
+
return prev !== undefined ? prev.value : fallback;
|
|
1932
|
+
},
|
|
1928
1933
|
equal }] : [{
|
|
1929
|
-
...
|
|
1934
|
+
...signalOpt,
|
|
1930
1935
|
source: () => src(),
|
|
1931
|
-
computation: (next, prev) =>
|
|
1936
|
+
computation: (next, prev) => {
|
|
1937
|
+
if (next !== undefined)
|
|
1938
|
+
return next;
|
|
1939
|
+
return prev !== undefined ? prev.value : fallback;
|
|
1940
|
+
},
|
|
1932
1941
|
equal,
|
|
1933
1942
|
}]));
|
|
1934
1943
|
if (isWritableSignal(src)) {
|
|
@@ -5121,11 +5130,14 @@ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
|
5121
5130
|
* deterministic on every replica.
|
|
5122
5131
|
*/
|
|
5123
5132
|
function orderedEntries(container) {
|
|
5133
|
+
return entriesOf(container, (element) => element);
|
|
5134
|
+
}
|
|
5135
|
+
function entriesOf(container, payloadOf) {
|
|
5124
5136
|
const entries = [];
|
|
5125
5137
|
for (const key of Object.keys(container)) {
|
|
5126
|
-
const
|
|
5127
|
-
const raw = isRecord(
|
|
5128
|
-
entries.push({ key, pos: typeof raw === 'string' ? raw : '', value });
|
|
5138
|
+
const element = container[key];
|
|
5139
|
+
const raw = isRecord(element) ? element[POS_SEGMENT] : undefined;
|
|
5140
|
+
entries.push({ key, pos: typeof raw === 'string' ? raw : '', value: payloadOf(element) });
|
|
5129
5141
|
}
|
|
5130
5142
|
entries.sort((a, b) => a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
5131
5143
|
return entries;
|
|
@@ -5145,12 +5157,18 @@ const neighborPositions = (entries, index) => {
|
|
|
5145
5157
|
* layer diffs on its own. Returns the assigned position. Re-inserting an existing key overwrites it.
|
|
5146
5158
|
*/
|
|
5147
5159
|
function insertElement(container, key, value, index) {
|
|
5160
|
+
return insertInto(container, key, value, index, inlineElement);
|
|
5161
|
+
}
|
|
5162
|
+
const inlineElement = (value, pos) => {
|
|
5148
5163
|
if (POS_SEGMENT in value)
|
|
5149
|
-
devError(`
|
|
5164
|
+
devError(`insert: '${POS_SEGMENT}' is managed, drop it from the value`);
|
|
5165
|
+
return { ...value, [POS_SEGMENT]: pos };
|
|
5166
|
+
};
|
|
5167
|
+
function insertInto(container, key, value, index, elementOf) {
|
|
5150
5168
|
const entries = orderedEntries(container()).filter((e) => e.key !== key);
|
|
5151
5169
|
const [before, after] = neighborPositions(entries, index ?? entries.length);
|
|
5152
5170
|
const pos = posBetween(before, after);
|
|
5153
|
-
container.update((c) => ({ ...c, [key]:
|
|
5171
|
+
container.update((c) => ({ ...c, [key]: elementOf(value, pos) }));
|
|
5154
5172
|
return pos;
|
|
5155
5173
|
}
|
|
5156
5174
|
/**
|
|
@@ -5217,6 +5235,23 @@ function evenPositions(n) {
|
|
|
5217
5235
|
}
|
|
5218
5236
|
return out;
|
|
5219
5237
|
}
|
|
5238
|
+
function keyedContainer(config = {}) {
|
|
5239
|
+
return helpersFor(config.key, inlineElement, (element) => element);
|
|
5240
|
+
}
|
|
5241
|
+
function wrappedContainer(config = {}) {
|
|
5242
|
+
return helpersFor(config.key, (value, pos) => ({ [POS_SEGMENT]: pos, value }), (element) => element.value);
|
|
5243
|
+
}
|
|
5244
|
+
function helpersFor(extract, elementOf, payloadOf) {
|
|
5245
|
+
return {
|
|
5246
|
+
entries: (container) => entriesOf(container, payloadOf),
|
|
5247
|
+
insert: (container, a, b, c) => extract
|
|
5248
|
+
? insertInto(container, extract(a), a, b, elementOf)
|
|
5249
|
+
: insertInto(container, a, b, c, elementOf),
|
|
5250
|
+
move: (container, key, index) => moveElement(container, key, index),
|
|
5251
|
+
remove: (container, key) => removeElement(container, key),
|
|
5252
|
+
rebalance: (sync, container) => rebalanceContainer(sync, container),
|
|
5253
|
+
};
|
|
5254
|
+
}
|
|
5220
5255
|
|
|
5221
5256
|
/**
|
|
5222
5257
|
* Wire protocol version. Version 2 ops carry `cites` + `epoch` (the dot-citation register).
|
|
@@ -5306,6 +5341,81 @@ function validateEnvelope(env) {
|
|
|
5306
5341
|
}
|
|
5307
5342
|
return null;
|
|
5308
5343
|
}
|
|
5344
|
+
function wireValueViolation(value) {
|
|
5345
|
+
return violationAt(value, false, []);
|
|
5346
|
+
}
|
|
5347
|
+
function violationAt(value, nested, ancestors) {
|
|
5348
|
+
if (value === undefined)
|
|
5349
|
+
return nested ? 'undefined-in-container' : null;
|
|
5350
|
+
if (value === null)
|
|
5351
|
+
return null;
|
|
5352
|
+
const t = typeof value;
|
|
5353
|
+
if (t === 'boolean' || t === 'string')
|
|
5354
|
+
return null;
|
|
5355
|
+
if (t === 'number') {
|
|
5356
|
+
if (!Number.isFinite(value))
|
|
5357
|
+
return 'non-finite-number';
|
|
5358
|
+
return Object.is(value, -0) ? 'negative-zero' : null;
|
|
5359
|
+
}
|
|
5360
|
+
if (t === 'bigint')
|
|
5361
|
+
return 'bigint';
|
|
5362
|
+
if (t === 'function')
|
|
5363
|
+
return 'function';
|
|
5364
|
+
if (t === 'symbol')
|
|
5365
|
+
return 'symbol';
|
|
5366
|
+
if (ancestors.includes(value))
|
|
5367
|
+
return 'cycle';
|
|
5368
|
+
ancestors.push(value);
|
|
5369
|
+
try {
|
|
5370
|
+
if (Array.isArray(value)) {
|
|
5371
|
+
for (let i = 0; i < value.length; i++) {
|
|
5372
|
+
if (!Object.hasOwn(value, i))
|
|
5373
|
+
return 'sparse-array';
|
|
5374
|
+
const reason = violationAt(value[i], true, ancestors);
|
|
5375
|
+
if (reason)
|
|
5376
|
+
return reason;
|
|
5377
|
+
}
|
|
5378
|
+
const keys = Object.keys(value);
|
|
5379
|
+
if (keys.length !== value.length)
|
|
5380
|
+
return 'array-named-property';
|
|
5381
|
+
for (const key of keys) {
|
|
5382
|
+
const idx = Number(key);
|
|
5383
|
+
if (String(idx) !== key ||
|
|
5384
|
+
!Number.isInteger(idx) ||
|
|
5385
|
+
idx < 0 ||
|
|
5386
|
+
idx >= value.length) {
|
|
5387
|
+
return 'array-named-property';
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return null;
|
|
5391
|
+
}
|
|
5392
|
+
const proto = Object.getPrototypeOf(value);
|
|
5393
|
+
if (proto !== Object.prototype && proto !== null)
|
|
5394
|
+
return 'non-plain-object';
|
|
5395
|
+
for (const key of Object.keys(value)) {
|
|
5396
|
+
const reason = violationAt(value[key], true, ancestors);
|
|
5397
|
+
if (reason)
|
|
5398
|
+
return reason;
|
|
5399
|
+
}
|
|
5400
|
+
return null;
|
|
5401
|
+
}
|
|
5402
|
+
finally {
|
|
5403
|
+
ancestors.pop();
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
// dev-only: one warning per offending batch names the first violating op, then stops
|
|
5407
|
+
const lintWireValues = (ops) => {
|
|
5408
|
+
for (const op of ops) {
|
|
5409
|
+
const reason = (op.kind === 'set' ? wireValueViolation(op.next) : null) ??
|
|
5410
|
+
(op.kind !== 'clear' && Object.hasOwn(op, 'prev')
|
|
5411
|
+
? wireValueViolation(op.prev)
|
|
5412
|
+
: null);
|
|
5413
|
+
if (!reason)
|
|
5414
|
+
continue;
|
|
5415
|
+
console.warn(`[@mmstack/primitives] op value at "${op.path.join('.')}" will not survive a JSON transport (${reason}): peers materialize a different value than this emitter, and the room can diverge. Op values must round-trip JSON unchanged; encode rich leaves as strings.`);
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5418
|
+
};
|
|
5309
5419
|
const lww = (_ancestor, mine) => mine;
|
|
5310
5420
|
const mergeThree = (ancestor, mine, theirs) => merge3(ancestor, mine, theirs);
|
|
5311
5421
|
const preserve = (ancestor, mine, theirs) => ({
|
|
@@ -6059,6 +6169,8 @@ function opSync(source, opt) {
|
|
|
6059
6169
|
const emitLocal = (ops) => {
|
|
6060
6170
|
const frontier = scopeFrontier;
|
|
6061
6171
|
const stamped = conv.stamp(ops, { bump: bumping, frontier });
|
|
6172
|
+
if (isDevMode())
|
|
6173
|
+
lintWireValues(stamped);
|
|
6062
6174
|
const nextVersion = (versions.get(origin) ?? 0) + 1;
|
|
6063
6175
|
const env = {
|
|
6064
6176
|
proto: OP_PROTO_VERSION,
|
|
@@ -7447,5 +7559,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
7447
7559
|
* Generated bundle index. Do not edit.
|
|
7448
7560
|
*/
|
|
7449
7561
|
|
|
7450
|
-
export { CONCURRENCY_INSTRUMENTATION, MmActivity, MmTransition, MmViewTransitionName, OP_PROTO_VERSION, PAUSABLE_OPTIONS, PERSISTED_STORE_OPTIONS, POS_SEGMENT, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, compareHlc, compareSiblings, compareTotal, createAttributedPending, createConvergingApply, createForwardingScope, createHlcClock, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, defaultFold, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, insertElement, invertBatch, isConflicted, isDerivation, isLeaf, isMutable, isOpaque, isStore, isStored, keepPrevious, keyArray, keyedArray, latest, lww, map, mapArray, mapObject, mediaQuery, merge3, mergeThree, mousePosition, moveElement, mutable, mutableStore, nestedEffect, networkStatus, opLog, opSync, opaque, orderedEntries, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, perfCustomTracks, persist, persistedStore, pipeable, piped, pointerDrag, policyStrategy, pooled, pooledArray, pooledKeys, pooledMap, pooledSet, posBetween, prefersDarkMode, prefersReducedMotion, preserve, projection, provideConcurrencyInstrumentation, provideForwardingTransitionScope, providePausableOptions, providePaused, providePersistedStoreOptions, provideTransitionScope, rebalanceContainer, rebaseOps, reconcile, registerResource, removeElement, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, storeHistory, stored, syncedFork, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, traced, until, use, validateEnvelope, windowSize, withHistory };
|
|
7562
|
+
export { CONCURRENCY_INSTRUMENTATION, MmActivity, MmTransition, MmViewTransitionName, OP_PROTO_VERSION, PAUSABLE_OPTIONS, PERSISTED_STORE_OPTIONS, POS_SEGMENT, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, applyOps, batteryStatus, bridgeScopeToPendingTasks, chunked, clipboard, combineWith, compareHlc, compareSiblings, compareTotal, createAttributedPending, createConvergingApply, createForwardingScope, createHlcClock, createStoreContext, createTransaction, createTransitionScope, debounce, debounced, defaultFold, deferredValue, derived, diffOps, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, insertElement, invertBatch, isConflicted, isDerivation, isLeaf, isMutable, isOpaque, isStore, isStored, keepPrevious, keyArray, keyedArray, keyedContainer, latest, lww, map, mapArray, mapObject, mediaQuery, merge3, mergeThree, mousePosition, moveElement, mutable, mutableStore, nestedEffect, networkStatus, opLog, opSync, opaque, orderedEntries, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, perfCustomTracks, persist, persistedStore, pipeable, piped, pointerDrag, policyStrategy, pooled, pooledArray, pooledKeys, pooledMap, pooledSet, posBetween, prefersDarkMode, prefersReducedMotion, preserve, projection, provideConcurrencyInstrumentation, provideForwardingTransitionScope, providePausableOptions, providePaused, providePersistedStoreOptions, provideTransitionScope, rebalanceContainer, rebaseOps, reconcile, registerResource, removeElement, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, storeHistory, stored, syncedFork, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, traced, until, use, validateEnvelope, windowSize, withHistory, wrappedContainer };
|
|
7451
7563
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|