@mmstack/primitives 20.5.3 → 20.5.5
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 +71 -12
- package/fesm2022/mmstack-primitives.mjs +93 -2
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +124 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ This library provides the following primitives:
|
|
|
29
29
|
- `toWritable` - Converts a read-only signal to writable using custom write logic.
|
|
30
30
|
- `derived` - Creates a signal with two-way binding to a source signal.
|
|
31
31
|
- `chunked` - Creates a signal that time-slices an array into chunked values & emits thats array based on the provided options.
|
|
32
|
+
- `pooled` / `pooledArray` / `pooledMap` / `pooledSet` - Double-buffered object pools for `computed` signals; recycle the output container to remove allocation pressure in high-frequency recomputation.
|
|
32
33
|
- `tabSync` - Low level primitive to "share" the value of a WritableSignal accross tabs via the BroadcastChannel api.
|
|
33
34
|
- `sensor` - A facade function to create various reactive sensor signals (e.g., mouse position, network status, page visibility, dark mode preference)." (This was the suggestion from before; it just reads a little smoother and more accurately reflects what the facade creates directly).
|
|
34
35
|
- `mediaQuery` - A generic primitive that tracks a CSS media query (forms the basis for `prefersDarkMode` and `prefersReducedMotion`).
|
|
@@ -94,7 +95,6 @@ import { JsonPipe } from '@angular/common';
|
|
|
94
95
|
|
|
95
96
|
@Component({
|
|
96
97
|
selector: 'app-throttle-demo',
|
|
97
|
-
standalone: true,
|
|
98
98
|
imports: [JsonPipe],
|
|
99
99
|
template: `
|
|
100
100
|
<div (mousemove)="onMouseMove($event)" style="width: 300px; height: 200px; border: 1px solid black; padding: 10px; user-select: none;">Move mouse here to see updates...</div>
|
|
@@ -196,7 +196,6 @@ import { stored } from '@mmstack/primitives';
|
|
|
196
196
|
|
|
197
197
|
@Component({
|
|
198
198
|
selector: 'app-theme-selector',
|
|
199
|
-
standalone: true,
|
|
200
199
|
// imports: [FormsModule], // Import if using ngModel
|
|
201
200
|
template: `
|
|
202
201
|
Theme:
|
|
@@ -274,7 +273,6 @@ import { JsonPipe } from '@angular/common';
|
|
|
274
273
|
|
|
275
274
|
@Component({
|
|
276
275
|
selector: 'app-store-demo',
|
|
277
|
-
standalone: true,
|
|
278
276
|
imports: [FormsModule, JsonPipe],
|
|
279
277
|
template: `
|
|
280
278
|
<h3>User Profile</h3>
|
|
@@ -658,6 +656,75 @@ export class HeavyListComponent {
|
|
|
658
656
|
}
|
|
659
657
|
```
|
|
660
658
|
|
|
659
|
+
### pooled / pooledArray / pooledMap / pooledSet
|
|
660
|
+
|
|
661
|
+
A double-buffered object pool for `computed` signal outputs. After a brief warm-up the pool reaches steady state with **zero allocations per recomputation** — two buffers are swapped on every read, with `reset` invoked before each `computation`. Each read returns a different identity from the previous read, so default `Object.is` equality still flags changes correctly. Most users will reach for the preset helpers (`pooledArray`, `pooledMap`, `pooledSet`); drop down to `pooled` only when you need a custom buffer type.
|
|
662
|
+
|
|
663
|
+
> **Retention contract:** the value returned from a pooled signal is only valid until the next read of that signal. The container is reused on the second-next read and will be `reset` first, mutating any reference you still hold. Do not store the result in component state, async closures, or anywhere outside the same reactive tick. Treat it as scratch output consumed synchronously.
|
|
664
|
+
|
|
665
|
+
Use these when a computed is recomputed at high frequency and produces a large allocation (filter/map outputs over big arrays, lookup indices, RAF-driven computeds). For typical UI computeds over small data, just use `computed` — the docs cost and footgun aren't worth saving an allocation that doesn't show up in a profile.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import { Component, signal } from '@angular/core';
|
|
669
|
+
import { pooledArray, pooledMap, pooledSet } from '@mmstack/primitives';
|
|
670
|
+
|
|
671
|
+
@Component({
|
|
672
|
+
selector: 'app-pooled-demo',
|
|
673
|
+
template: `<p>Active: {{ activeIds().length }} / {{ items().length }}</p>`,
|
|
674
|
+
})
|
|
675
|
+
export class PooledDemoComponent {
|
|
676
|
+
readonly items = signal(Array.from({ length: 10_000 }, (_, i) => ({ id: i, active: i % 2 === 0 })));
|
|
677
|
+
|
|
678
|
+
// Recycles a single number[] across recomputations.
|
|
679
|
+
readonly activeIds = pooledArray<number[]>((buf) => {
|
|
680
|
+
for (const item of this.items()) {
|
|
681
|
+
if (item.active) buf.push(item.id);
|
|
682
|
+
}
|
|
683
|
+
return buf;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Recycles a Map for fast id → item lookups.
|
|
687
|
+
readonly byId = pooledMap<Map<number, { id: number; active: boolean }>>((buf) => {
|
|
688
|
+
for (const item of this.items()) buf.set(item.id, item);
|
|
689
|
+
return buf;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Recycles a Set of distinct values.
|
|
693
|
+
readonly distinctFlags = pooledSet<Set<boolean>>((buf) => {
|
|
694
|
+
for (const item of this.items()) buf.add(item.active);
|
|
695
|
+
return buf;
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Need a custom buffer type (typed array, your own struct)? Use `pooled` directly:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import { signal } from '@angular/core';
|
|
704
|
+
import { pooled } from '@mmstack/primitives';
|
|
705
|
+
|
|
706
|
+
const source = signal<{ active: boolean }[]>([]);
|
|
707
|
+
|
|
708
|
+
// Pre-allocate both slots at construction (eager) — useful when create() is expensive.
|
|
709
|
+
const counters = pooled<{ total: number; active: number }>({
|
|
710
|
+
create: () => ({ total: 0, active: 0 }),
|
|
711
|
+
reset: (c) => {
|
|
712
|
+
c.total = 0;
|
|
713
|
+
c.active = 0;
|
|
714
|
+
},
|
|
715
|
+
computation: (c) => {
|
|
716
|
+
for (const item of source()) {
|
|
717
|
+
c.total++;
|
|
718
|
+
if (item.active) c.active++;
|
|
719
|
+
}
|
|
720
|
+
return c;
|
|
721
|
+
},
|
|
722
|
+
eager: true,
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Complementary to `linkedSignal` (which carries previous *state* forward, not the *container*) and `chunked` (which time-slices large outputs across frames).
|
|
727
|
+
|
|
661
728
|
### tabSync
|
|
662
729
|
|
|
663
730
|
A low-level primitive that synchronizes a WritableSignal across multiple browser tabs or windows of the same application using the BroadcastChannel API. Used by the cache in @mmstack/resource & the stored signal.
|
|
@@ -681,7 +748,7 @@ import { tabSync } from '@mmstack/primitives';
|
|
|
681
748
|
template: `
|
|
682
749
|
<p>Open this page in two tabs!</p>
|
|
683
750
|
|
|
684
|
-
<button (click)="counter.update(n => n + 1)">Count: {{ counter() }}</button>
|
|
751
|
+
<button (click)="counter.update((n) => n + 1)">Count: {{ counter() }}</button>
|
|
685
752
|
|
|
686
753
|
<select [ngModel]="theme()" (ngModelChange)="theme.set($event)">
|
|
687
754
|
<option value="light">Light</option>
|
|
@@ -714,7 +781,6 @@ import { Component, signal, effect } from '@angular/core';
|
|
|
714
781
|
|
|
715
782
|
@Component({
|
|
716
783
|
selector: 'app-history-demo',
|
|
717
|
-
standalone: true,
|
|
718
784
|
imports: [FormsModule, JsonPipe],
|
|
719
785
|
template: `
|
|
720
786
|
<h4>Simple Text Editor</h4>
|
|
@@ -781,7 +847,6 @@ import { JsonPipe } from '@angular/common';
|
|
|
781
847
|
|
|
782
848
|
@Component({
|
|
783
849
|
selector: 'app-mouse-tracker',
|
|
784
|
-
standalone: true,
|
|
785
850
|
imports: [JsonPipe],
|
|
786
851
|
template: `
|
|
787
852
|
<div (mousemove)="onMouseMove($event)" style="width: 300px; height: 200px; border: 1px solid black; padding: 10px; user-select: none;">Move mouse here...</div>
|
|
@@ -819,7 +884,6 @@ import { DatePipe } from '@angular/common';
|
|
|
819
884
|
|
|
820
885
|
@Component({
|
|
821
886
|
selector: 'app-network-info',
|
|
822
|
-
standalone: true,
|
|
823
887
|
imports: [DatePipe],
|
|
824
888
|
template: `
|
|
825
889
|
@if (netStatus()) {
|
|
@@ -850,7 +914,6 @@ import { sensor } from '@mmstack/primitives'; // Or import { pageVisibility }
|
|
|
850
914
|
|
|
851
915
|
@Component({
|
|
852
916
|
selector: 'app-visibility-logger',
|
|
853
|
-
standalone: true,
|
|
854
917
|
template: `<p>Page is currently: {{ visibility() }}</p>`,
|
|
855
918
|
})
|
|
856
919
|
export class VisibilityLoggerComponent {
|
|
@@ -877,7 +940,6 @@ import { sensor } from '@mmstack/primitives'; // Or import { windowSize }
|
|
|
877
940
|
|
|
878
941
|
@Component({
|
|
879
942
|
selector: 'app-responsive-display',
|
|
880
|
-
standalone: true,
|
|
881
943
|
template: `
|
|
882
944
|
<p>Current Window Size: {{ winSize().width }}px x {{ winSize().height }}px</p>
|
|
883
945
|
<p>Unthrottled: W: {{ winSize.unthrottled().width }} H: {{ winSize.unthrottled().height }}</p>
|
|
@@ -911,7 +973,6 @@ import { JsonPipe } from '@angular/common';
|
|
|
911
973
|
|
|
912
974
|
@Component({
|
|
913
975
|
selector: 'app-scroll-indicator',
|
|
914
|
-
standalone: true,
|
|
915
976
|
imports: [JsonPipe],
|
|
916
977
|
template: `
|
|
917
978
|
<div style="height: 100px; border-bottom: 2px solid red; position: fixed; top: 0; left: 0; width: 100%; background: white; z-index: 10;">
|
|
@@ -945,7 +1006,6 @@ import { mediaQuery, prefersDarkMode, prefersReducedMotion } from '@mmstack/prim
|
|
|
945
1006
|
|
|
946
1007
|
@Component({
|
|
947
1008
|
selector: 'app-layout-checker',
|
|
948
|
-
standalone: true,
|
|
949
1009
|
template: `
|
|
950
1010
|
@if (isLargeScreen()) {
|
|
951
1011
|
<p>Using large screen layout.</p>
|
|
@@ -1007,7 +1067,6 @@ import { elementVisibility } from '@mmstack/primitives';
|
|
|
1007
1067
|
|
|
1008
1068
|
@Component({
|
|
1009
1069
|
selector: 'app-lazy-load-item',
|
|
1010
|
-
standalone: true,
|
|
1011
1070
|
template: `
|
|
1012
1071
|
<div #itemToObserve style="height: 300px; margin-top: 100vh; border: 2px solid green;">
|
|
1013
1072
|
@if (intersectionEntry.visible()) {
|
|
@@ -844,6 +844,98 @@ function piped(initial, opt) {
|
|
|
844
844
|
return pipeable(signal(initial, opt));
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
/**
|
|
848
|
+
* A `Signal<U>` backed by a two-slot object pool: `create` is called at most
|
|
849
|
+
* twice over the pool's lifetime, and the two `T` instances are swapped on
|
|
850
|
+
* every recomputation with `reset` invoked on the dirty one before
|
|
851
|
+
* `computation` writes into it. Consecutive reads return different identities,
|
|
852
|
+
* so the default `Object.is` equality still flags changes.
|
|
853
|
+
*
|
|
854
|
+
* **Retention contract:** the returned value is only valid until the next
|
|
855
|
+
* recomputation of this signal. The container is recycled and `reset`,
|
|
856
|
+
* mutating any reference you still hold — do not store the result, pass it to
|
|
857
|
+
* async code, or hand it to consumers that outlive the current reactive tick.
|
|
858
|
+
*
|
|
859
|
+
* For collection buffers prefer the presets: {@link pooledArray},
|
|
860
|
+
* {@link pooledMap}, {@link pooledSet}.
|
|
861
|
+
*
|
|
862
|
+
* @see [Angular `linkedSignal`](https://angular.dev/api/core/linkedSignal) — carries previous *state* forward; complementary, not a substitute.
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* ```ts
|
|
866
|
+
* const source = signal<{ active: boolean }[]>([]);
|
|
867
|
+
*
|
|
868
|
+
* const counters = pooled<{ total: number; active: number }>({
|
|
869
|
+
* create: () => ({ total: 0, active: 0 }),
|
|
870
|
+
* reset: (c) => { c.total = 0; c.active = 0; },
|
|
871
|
+
* computation: (c) => {
|
|
872
|
+
* for (const item of source()) { c.total++; if (item.active) c.active++; }
|
|
873
|
+
* return c;
|
|
874
|
+
* },
|
|
875
|
+
* });
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
function pooled({ create, reset, computation, ...opt }) {
|
|
879
|
+
let other = opt.eager ? create() : undefined;
|
|
880
|
+
let current = opt.eager ? create() : undefined;
|
|
881
|
+
// only relevant for eager mode: the pre-allocated `current` is fresh until its first demote
|
|
882
|
+
let currentFresh = opt.eager;
|
|
883
|
+
return computed(() => {
|
|
884
|
+
const next = other ?? untracked(() => create());
|
|
885
|
+
if (current !== undefined) {
|
|
886
|
+
if (currentFresh) {
|
|
887
|
+
// never-mutated buffer leaving the active slot; nothing to clean
|
|
888
|
+
other = current;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
// reset on release: clean the dirty buffer as it goes back into the pool
|
|
892
|
+
// (also threads the swap-return correctly into the pool's spare slot)
|
|
893
|
+
other = untracked(() => reset(current)) ?? current;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
current = next;
|
|
897
|
+
currentFresh = false;
|
|
898
|
+
return computation(next);
|
|
899
|
+
}, opt);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function toPooledOptions(optOrComputation, create, reset, signalOpt) {
|
|
903
|
+
const opt = typeof optOrComputation === 'object' ? optOrComputation : signalOpt;
|
|
904
|
+
const computation = typeof optOrComputation === 'function'
|
|
905
|
+
? optOrComputation
|
|
906
|
+
: optOrComputation.computation;
|
|
907
|
+
return {
|
|
908
|
+
create,
|
|
909
|
+
reset,
|
|
910
|
+
computation,
|
|
911
|
+
...opt,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function createEmptyArray() {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
function resetArray(arr) {
|
|
918
|
+
arr.length = 0;
|
|
919
|
+
}
|
|
920
|
+
function pooledArray(optOrComputation, signalOpt) {
|
|
921
|
+
return pooled(toPooledOptions(optOrComputation, createEmptyArray, resetArray, signalOpt));
|
|
922
|
+
}
|
|
923
|
+
function createEmptySet() {
|
|
924
|
+
return new Set();
|
|
925
|
+
}
|
|
926
|
+
function resetClearable(clearable) {
|
|
927
|
+
clearable.clear();
|
|
928
|
+
}
|
|
929
|
+
function pooledSet(optOrComputation, signalOpt) {
|
|
930
|
+
return pooled(toPooledOptions(optOrComputation, createEmptySet, resetClearable, signalOpt));
|
|
931
|
+
}
|
|
932
|
+
function createEmptyMap() {
|
|
933
|
+
return new Map();
|
|
934
|
+
}
|
|
935
|
+
function pooledMap(optOrComputation, signalOpt) {
|
|
936
|
+
return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
|
|
937
|
+
}
|
|
938
|
+
|
|
847
939
|
function observerSupported$1() {
|
|
848
940
|
return typeof ResizeObserver !== 'undefined';
|
|
849
941
|
}
|
|
@@ -1943,7 +2035,6 @@ const noopStore = {
|
|
|
1943
2035
|
*
|
|
1944
2036
|
* @Component({
|
|
1945
2037
|
* selector: 'app-settings',
|
|
1946
|
-
* standalone: true,
|
|
1947
2038
|
* template: `
|
|
1948
2039
|
* Theme:
|
|
1949
2040
|
* <select [ngModel]="theme()" (ngModelChange)="theme.set($event)">
|
|
@@ -2381,5 +2472,5 @@ function withHistory(source, opt) {
|
|
|
2381
2472
|
* Generated bundle index. Do not edit.
|
|
2382
2473
|
*/
|
|
2383
2474
|
|
|
2384
|
-
export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
2475
|
+
export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
2385
2476
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|