@mmstack/primitives 22.2.1 → 22.3.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 +59 -11
- package/fesm2022/mmstack-primitives.mjs +666 -381
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +339 -135
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install @mmstack/primitives
|
|
|
19
19
|
- [Timing & propagation](#timing--propagation) — `debounced`, `throttled`, `until`
|
|
20
20
|
- [Reactive collections](#reactive-collections) — `indexArray`, `keyArray`, `mapObject`
|
|
21
21
|
- [Effects](#effects) — `nestedEffect`
|
|
22
|
-
- [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable
|
|
22
|
+
- [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*` / `providePausableOptions`, Suspense (`mm-suspense`), `startTransition` / `startTransaction`, `holdUntilReady`
|
|
23
23
|
- [History & persistence](#history--persistence) — `withHistory`, `stored`, `tabSync`
|
|
24
24
|
- [Performance helpers](#performance-helpers) — `chunked`, `pooled` / `pooledArray` / `pooledMap` / `pooledSet`
|
|
25
25
|
- [Sensors](#sensors) — `sensor()` facade + browser-state signals
|
|
@@ -117,14 +117,18 @@ Each level's shape is resolved from what's known: a value that is currently an o
|
|
|
117
117
|
|
|
118
118
|
Top-level array support isn't exposed yet — use `indexArray` / `keyArray` for those.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
**Union leaves (perf opt-in).** `noUnionLeaves: true` promises no node ever flips between a leaf and a sub-store, so each node's leaf-ness is resolved once on first access and cached instead of staying reactive. Off by default — leave it off if a value can switch between a primitive and an object/array.
|
|
121
121
|
|
|
122
|
-
`
|
|
122
|
+
### `extendStore` (scoped overlay)
|
|
123
|
+
|
|
124
|
+
`extendStore(store, seed)` (on any store kind) creates a **scoped overlay** — a child store that **shares** the parent's signals for inherited keys (the same `WritableSignal`: writes go through to the parent and parent changes flow down) while keeping the seed and any new keys in a **local layer** that never propagates upward. No diffing, no syncing — local keys simply aren't wired to the parent.
|
|
123
125
|
|
|
124
126
|
```typescript
|
|
127
|
+
import { extendStore, store } from '@mmstack/primitives';
|
|
128
|
+
|
|
125
129
|
const app = store({ user: { name: 'Alice' }, theme: 'dark' });
|
|
126
130
|
|
|
127
|
-
const scope = app
|
|
131
|
+
const scope = extendStore(app, { draft: '' }); // inherits user + theme, adds a local draft
|
|
128
132
|
|
|
129
133
|
scope.user === app.user; // true — the same signal (shared, two-way)
|
|
130
134
|
scope.user.name.set('Bob'); // writes through to the parent
|
|
@@ -132,19 +136,19 @@ scope.draft.set('hello'); // local only — `app` never gains `draft`
|
|
|
132
136
|
scope(); // { user: { name: 'Bob' }, theme: 'dark', draft: 'hello' }
|
|
133
137
|
```
|
|
134
138
|
|
|
135
|
-
Resolution per key is **local → parent → local**: a seed key (or one set on the scope before it exists on the parent) is local and _shadows_ the parent — and keeps shadowing even if the parent later grows that key; a key that exists only on the parent writes through to it; a brand-new key lands locally. `scope()` is the merged view (local shadowing), and `Object.keys(scope)` / `key in scope` are the union of both layers.
|
|
139
|
+
Resolution per key is **local → parent → local**: a seed key (or one set on the scope before it exists on the parent) is local and _shadows_ the parent — and keeps shadowing even if the parent later grows that key; a key that exists only on the parent writes through to it; a brand-new key lands locally. `scope()` is the merged view (local shadowing), and `Object.keys(scope)` / `key in scope` are the union of both layers. It composes — `extendStore(extendStore(app, x), y)` chains parents.
|
|
136
140
|
|
|
137
141
|
The seed may also be a **signal** of the matching kind, so an existing (externally-owned, reactive) signal becomes the local layer:
|
|
138
142
|
|
|
139
143
|
```typescript
|
|
140
144
|
const draft = signal({ title: '' });
|
|
141
|
-
const scope = app
|
|
145
|
+
const scope = extendStore(app, draft); // writes to scope.title flow out to `draft`, and back in
|
|
142
146
|
```
|
|
143
147
|
|
|
144
148
|
A few release notes:
|
|
145
149
|
|
|
146
|
-
- The
|
|
147
|
-
- Reserved names — `
|
|
150
|
+
- The scope inherits the parent's config (`vivify` / `noUnionLeaves`) and its injector-scoped proxy cache, so **both** inherited and local paths vivify when the parent was created with `vivify`. `extendStore` doesn't accept `vivify` / `noUnionLeaves` — they always come from the parent.
|
|
151
|
+
- Reserved names — `asReadonlyStore` and the signal methods (`set` / `update` / `mutate` / `inline` / `asReadonly`) — shadow same-named data keys, as on any store.
|
|
148
152
|
- `scope.asReadonlyStore()` returns a read-only **snapshot view** of the merge (reactive reads, no writes); it does not share sub-store identity.
|
|
149
153
|
|
|
150
154
|
### `forkStore`
|
|
@@ -165,13 +169,13 @@ base.user.name(); // 'Bob'
|
|
|
165
169
|
// draft.discard(); // …or throw the edits away
|
|
166
170
|
```
|
|
167
171
|
|
|
168
|
-
The fork is a full store (`draft.store.user.name(...)`, `
|
|
172
|
+
The fork is a full store (`draft.store.user.name(...)`, `extendStore`, deep reads/writes — everything `store` gives you). It's built on `linkedSignal`: it holds local writes until the **base changes underneath it**, then runs a `strategy` to reconcile:
|
|
169
173
|
|
|
170
174
|
- **`'fine'`** (default for immutable stores) — per-path 3-way merge: keep the paths the fork edited, take the base's live values for the paths it didn't. Survives concurrent base changes. Relies on copy-on-write reference identity, so it's **unsupported on a mutable base** (in-place mutation defeats it — `fork` warns and falls back to `'coarse'`).
|
|
171
175
|
- **`'coarse'`** — any base change resets the whole fork. Cheapest; correct when the base is held for the fork's lifetime (e.g. a transition). The default for a mutable base.
|
|
172
176
|
- **a `ReconcileFn<T>`** — `(ancestor, mine, theirs) => merged`, for bring-your-own merge (array-by-id, Immer patches, CRDT-ish).
|
|
173
177
|
|
|
174
|
-
>
|
|
178
|
+
> The fork inherits the base's `vivify` / `noUnionLeaves` and its injector-scoped proxy cache automatically, so its write semantics match the base. Pass them explicitly only to override (advanced).
|
|
175
179
|
|
|
176
180
|
### `toWritable`
|
|
177
181
|
|
|
@@ -365,6 +369,19 @@ pausableEffect(() => poll(url())); // body skipped while paused; deps collapse s
|
|
|
365
369
|
|
|
366
370
|
While paused each one **collapses its dependency set to just the pause predicate**, so an upstream change can't trigger work; on resume it re-tracks and re-runs / recomputes with the latest values. SSR never pauses.
|
|
367
371
|
|
|
372
|
+
#### `providePausableOptions`
|
|
373
|
+
|
|
374
|
+
Sets an app-wide default pause source for every pausable-aware primitive — the `pausable*` family above plus the opt-in integrations (`stored`, `chunked`). A call-site `pause` always wins; this only fills in when the call didn't specify one. Use it to make everything honour the ambient `*mmActivity` boundary from one place:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { providePausableOptions } from '@mmstack/primitives';
|
|
378
|
+
|
|
379
|
+
// e.g. in app.config.ts
|
|
380
|
+
providers: [providePausableOptions({ pause: true })];
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
With this provided, `stored(...)` / `chunked(...)` (off by default) start reading the ambient paused context; pass `pause: false` at an individual call site to opt that one back out.
|
|
384
|
+
|
|
368
385
|
### Suspense — `<mm-suspense>` and the transition scope
|
|
369
386
|
|
|
370
387
|
A **transition scope** is a per-boundary registry of resources whose async state a boundary coordinates. `<mm-suspense>` provides its own scope, so resources created in its subtree register into it automatically (via `@mmstack/resource`'s `register` option, or `registerResource(ref)` for a hand-rolled `ResourceRef`):
|
|
@@ -631,6 +648,7 @@ const mouse = sensor('mousePosition', {
|
|
|
631
648
|
| `windowSize` | `windowSize()` | `Signal<{ width, height }>` + `.unthrottled` | Throttled to 100ms by default. |
|
|
632
649
|
| `scrollPosition` | `scrollPosition()` | `Signal<{ x, y }>` + `.unthrottled` | Window or element scroll, throttled 100ms. |
|
|
633
650
|
| `mousePosition` | `mousePosition()` | `Signal<{ x, y }>` + `.unthrottled` | Throttled 100ms. `coordinateSpace: 'client' \| 'page'`, optional `touch`. |
|
|
651
|
+
| `pointerDrag` | `pointerDrag()` | `Signal<PointerDragState>` + `.unthrottled` + `.cancel()` | Pointer gesture (down→move→up) with `activationThreshold`, `delta`, modifiers, pointer capture, Escape-cancel. |
|
|
634
652
|
| `elementVisibility` | `elementVisibility(target?)` | `Signal<IntersectionObserverEntry?>` + `.visible` | IntersectionObserver-based, `.visible` is a boolean shorthand. |
|
|
635
653
|
| `elementSize` | `elementSize(target?)` | `Signal<{ width, height }?>` | ResizeObserver-based. Defaults to `border-box`. |
|
|
636
654
|
| `geolocation` | `geolocation(opt?)` | `Signal<GeolocationPosition?>` + `.error` + `.loading` | One-shot by default; pass `watch: true` for `watchPosition`. |
|
|
@@ -640,7 +658,37 @@ const mouse = sensor('mousePosition', {
|
|
|
640
658
|
| `idle` | `idle({ ms })` | `Signal<boolean>` + `.since` | Flips to `true` after `ms` of inactivity. Configurable activity events. |
|
|
641
659
|
| `focusWithin` | `focusWithin(target?)` | `Signal<boolean>` | Mirrors the `:focus-within` CSS pseudo-class. |
|
|
642
660
|
|
|
643
|
-
Element-targeting sensors (`elementSize`, `elementVisibility`, `focusWithin`) default `target` to `inject(ElementRef)` so they're drop-in inside a component.
|
|
661
|
+
Element-targeting sensors (`elementSize`, `elementVisibility`, `focusWithin`, `pointerDrag`) default `target` to `inject(ElementRef)` so they're drop-in inside a component.
|
|
662
|
+
|
|
663
|
+
### `pointerDrag`
|
|
664
|
+
|
|
665
|
+
Tracks a pointer **gesture** (pointerdown → capture → move → up) as a signal — the
|
|
666
|
+
foundation for pointer-based move/resize/marquee on a canvas. Unlike native HTML5
|
|
667
|
+
drag, pointer events fire continuously and coordinates stay reliable; `delta` is
|
|
668
|
+
computed on the same update as `current` (never torn). `active` only flips true
|
|
669
|
+
once the pointer travels past `activationThreshold`, so the same element stays
|
|
670
|
+
clickable. Uses `setPointerCapture`, supports a delegated `handleSelector`, and
|
|
671
|
+
cancels on Escape or via `.cancel()`.
|
|
672
|
+
|
|
673
|
+
A delegated `handleSelector` reports which child actually started the drag via
|
|
674
|
+
`drag().origin` (so one listener on a container can serve many handles), and
|
|
675
|
+
`stopPropagation: true` lets an inner sensor claim the `pointerdown` over an
|
|
676
|
+
outer one on the same tree (e.g. a nested sortable). Reads are throttled
|
|
677
|
+
(`throttle`, default 16ms); `drag.unthrottled()` exposes the un-throttled view
|
|
678
|
+
for logic that needs the exact release position.
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { sensor } from '@mmstack/primitives';
|
|
682
|
+
|
|
683
|
+
const drag = sensor('pointerDrag', { activationThreshold: 4 });
|
|
684
|
+
|
|
685
|
+
// derive position from the gesture — no effects
|
|
686
|
+
const position = computed(() => {
|
|
687
|
+
const d = drag();
|
|
688
|
+
return d.active ? { x: base.x + d.delta.x, y: base.y + d.delta.y } : base;
|
|
689
|
+
});
|
|
690
|
+
// drag().modifiers.shift → e.g. constrain axis · drag().origin → the handle · drag.cancel() → revert
|
|
691
|
+
```
|
|
644
692
|
|
|
645
693
|
### `signalFromEvent`
|
|
646
694
|
|