ustor 0.2.0 → 0.3.1
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 -52
- package/dist/index.cjs +5 -5
- package/dist/index.es.js +5 -5
- package/dist/index.min.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.es.js +1 -1
- package/package.json +5 -1
- package/types/index.d.ts +3 -3
- package/types/react.d.ts +1 -16
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ A tiny, dependency-free reactive store for JavaScript objects and arrays. UStor
|
|
|
18
18
|
- [Installation](#installation)
|
|
19
19
|
- [Quickstart](#quickstart)
|
|
20
20
|
- [Common use cases](#common-use-cases)
|
|
21
|
+
- [Examples](#examples)
|
|
21
22
|
- [API reference](#api-reference)
|
|
22
23
|
- [Arrays](#arrays)
|
|
23
24
|
- [Supported adapters](#supported-adapters)
|
|
@@ -53,10 +54,10 @@ import { signal, computed, effect } from "@preact/signals-core";
|
|
|
53
54
|
|
|
54
55
|
Object.assign(api, {
|
|
55
56
|
signal,
|
|
56
|
-
|
|
57
|
+
computed,
|
|
57
58
|
effect,
|
|
58
59
|
get: (value) => value.value,
|
|
59
|
-
|
|
60
|
+
getComputed: (value) => value.value,
|
|
60
61
|
set: (target, value) => { target.value = value; },
|
|
61
62
|
is: (value) => typeof value?.peek === "function",
|
|
62
63
|
});
|
|
@@ -147,6 +148,10 @@ ui.theme = "dark";
|
|
|
147
148
|
|
|
148
149
|
Create request- or component-specific stores when the state must not be shared. See [SSR and Next.js](#ssr-and-nextjs).
|
|
149
150
|
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
The [source example collection](examples/README.md) contains runnable Node programs for counters, carts, validation, async race handling, undo history, Vue, and RxJS; React and Next.js components; a browser import-map demo; and TypeScript examples. Run `npm run examples` to build, execute the isolated Node examples, and type-check the component/TypeScript sources.
|
|
154
|
+
|
|
150
155
|
## API reference
|
|
151
156
|
|
|
152
157
|
### `store(values, proto?)`
|
|
@@ -190,7 +195,7 @@ is(null); // false
|
|
|
190
195
|
|
|
191
196
|
### `state.$property`
|
|
192
197
|
|
|
193
|
-
Exposes the underlying signal for a data property, or the underlying
|
|
198
|
+
Exposes the underlying signal for a data property, or the underlying computed value for a computed getter.
|
|
194
199
|
|
|
195
200
|
```javascript
|
|
196
201
|
const state = store({ count: 0 });
|
|
@@ -199,7 +204,7 @@ state.$count.value = 2; // Preact signal example
|
|
|
199
204
|
console.log(state.count); // 2
|
|
200
205
|
```
|
|
201
206
|
|
|
202
|
-
Aliases are non-enumerable. Getters without `api.
|
|
207
|
+
Aliases are non-enumerable. Getters without `api.computed` do not have a `$` alias. If an input explicitly defines a conflicting key such as `$count`, the user-defined property takes precedence.
|
|
203
208
|
|
|
204
209
|
### Adapter API
|
|
205
210
|
|
|
@@ -216,11 +221,11 @@ Optional operations:
|
|
|
216
221
|
|
|
217
222
|
| Operation | Purpose |
|
|
218
223
|
| --- | --- |
|
|
219
|
-
| `api.
|
|
220
|
-
| `api.
|
|
224
|
+
| `api.computed(fn)` | Create a lazy computed value. |
|
|
225
|
+
| `api.getComputed(computed)` | Read a computed value; defaults to `api.get`. |
|
|
221
226
|
| `api.effect(fn)` | Required by `ustor/react`; must auto-track synchronously and return a cleanup function. |
|
|
222
227
|
|
|
223
|
-
Without `api.
|
|
228
|
+
Without `api.computed`, getters execute on each access and still participate in compatible effects. With `api.computed`, getters are lazy and cached while their dependencies remain unchanged.
|
|
224
229
|
|
|
225
230
|
## Arrays
|
|
226
231
|
|
|
@@ -253,35 +258,35 @@ The configurations in `adapters.spec.js` are executable documentation and includ
|
|
|
253
258
|
|
|
254
259
|
| Package | Getter support |
|
|
255
260
|
| --- | --- |
|
|
256
|
-
| `@preact/signals-core` | Automatic
|
|
257
|
-
| `solid-js` | Automatic
|
|
258
|
-
| `oby` | Automatic
|
|
259
|
-
| `sinuous/observable` | Automatic
|
|
260
|
-
| `ulive` | Automatic
|
|
261
|
-
| `usignal` | Automatic
|
|
262
|
-
| `@vue/reactivity` | Automatic
|
|
263
|
-
| `alien-signals` | Automatic
|
|
264
|
-
| `s-js` | Automatic
|
|
265
|
-
| `mobx` | Automatic
|
|
266
|
-
| `@tanstack/store` | Automatic
|
|
267
|
-
| `@webreflection/signal` | Automatic
|
|
268
|
-
| `@webreflection/signals` | Automatic
|
|
269
|
-
| `@webreflection/alien-signals` | Automatic
|
|
261
|
+
| `@preact/signals-core` | Automatic computed values |
|
|
262
|
+
| `solid-js` | Automatic computed values |
|
|
263
|
+
| `oby` | Automatic computed values |
|
|
264
|
+
| `sinuous/observable` | Automatic computed values |
|
|
265
|
+
| `ulive` | Automatic computed values |
|
|
266
|
+
| `usignal` | Automatic computed values |
|
|
267
|
+
| `@vue/reactivity` | Automatic computed values using `shallowRef` |
|
|
268
|
+
| `alien-signals` | Automatic computed values |
|
|
269
|
+
| `s-js` | Automatic computed values using the provided branded factory |
|
|
270
|
+
| `mobx` | Automatic computed values using shallow observable boxes |
|
|
271
|
+
| `@tanstack/store` | Automatic computed values using the provided value wrapper |
|
|
272
|
+
| `@webreflection/signal` | Automatic computed values |
|
|
273
|
+
| `@webreflection/signals` | Automatic computed values |
|
|
274
|
+
| `@webreflection/alien-signals` | Automatic computed values |
|
|
270
275
|
| `rxjs` | Basic: `BehaviorSubject` values and direct getters |
|
|
271
276
|
| `nanostores` | Basic: atoms and direct getters |
|
|
272
277
|
|
|
273
|
-
**Basic adapters** support synchronous values, nested objects, arrays, and getters evaluated on access. RxJS and Nanostores do not automatically discover arbitrary getter dependencies, so automatic
|
|
278
|
+
**Basic adapters** support synchronous values, nested objects, arrays, and getters evaluated on access. RxJS and Nanostores do not automatically discover arbitrary getter dependencies, so automatic computed values are not advertised for them.
|
|
274
279
|
|
|
275
280
|
Adapter-specific notes:
|
|
276
281
|
|
|
277
282
|
- Vue and MobX use shallow containers to avoid wrapping UStor's proxies twice.
|
|
278
283
|
- TanStack Store treats bare functions as computed values; its tested adapter wraps writable values.
|
|
279
|
-
- S.js has no built-in signal brand; use the tested factory and create
|
|
280
|
-
- Solid.js, Vue, S.js, and similar libraries may require their own owner/scope lifecycle for short-lived
|
|
284
|
+
- S.js has no built-in signal brand; use the tested factory and create stores with computed values inside an S.js root.
|
|
285
|
+
- Solid.js, Vue, S.js, and similar libraries may require their own owner/scope lifecycle for short-lived computed values.
|
|
281
286
|
|
|
282
287
|
## Porting any signal library
|
|
283
288
|
|
|
284
|
-
You normally **do not need to modify UStor's core**. Port a library by translating its synchronous read/write API into four required functions, then add optional
|
|
289
|
+
You normally **do not need to modify UStor's core**. Port a library by translating its synchronous read/write API into four required functions, then add optional computed/effect functions when supported.
|
|
285
290
|
|
|
286
291
|
### 1. Map the four required operations
|
|
287
292
|
|
|
@@ -370,20 +375,20 @@ api.is = (value) => signals.has(value);
|
|
|
370
375
|
|
|
371
376
|
Create external signals through `api.signal` when using this approach. The S.js configuration in `adapters.spec.js` demonstrates it.
|
|
372
377
|
|
|
373
|
-
### 4. Add
|
|
378
|
+
### 4. Add computed values only when dependencies are automatic
|
|
374
379
|
|
|
375
380
|
If the library can compute a value by automatically tracking signals read inside `fn`, add:
|
|
376
381
|
|
|
377
382
|
```javascript
|
|
378
383
|
Object.assign(api, {
|
|
379
|
-
|
|
380
|
-
|
|
384
|
+
computed: (fn) => library.computed(fn),
|
|
385
|
+
getComputed: (computed) => library.read(computed),
|
|
381
386
|
});
|
|
382
387
|
```
|
|
383
388
|
|
|
384
|
-
The
|
|
389
|
+
The computed value may be lazy or eager, but it must invalidate when any dependency changes, including conditional dependencies.
|
|
385
390
|
|
|
386
|
-
If the library requires an explicit dependency list—as with RxJS or Nanostores—**omit `
|
|
391
|
+
If the library requires an explicit dependency list—as with RxJS or Nanostores—**omit `computed` and `getComputed`**. UStor then uses direct getters evaluated on access. Do not emulate automatic dependency tracking incorrectly.
|
|
387
392
|
|
|
388
393
|
### 5. Add effects for React integration only when compatible
|
|
389
394
|
|
|
@@ -406,11 +411,11 @@ The core store does not require `api.effect`. Basic adapters without automatic d
|
|
|
406
411
|
|
|
407
412
|
### 6. Configure once and manage lifecycle
|
|
408
413
|
|
|
409
|
-
Configure `api` before creating stores and keep the same implementation for their lifetime. If your library requires an owner, scope, root, or disposer for
|
|
414
|
+
Configure `api` before creating stores and keep the same implementation for their lifetime. If your library requires an owner, scope, root, or disposer for computed values/effects, create short-lived stores in that lifecycle and dispose them with their owner.
|
|
410
415
|
|
|
411
416
|
### 7. Validate the adapter
|
|
412
417
|
|
|
413
|
-
This Node script checks the required behavior, function preservation, nested updates, arrays, and optional
|
|
418
|
+
This Node script checks the required behavior, function preservation, nested updates, arrays, and optional computed values:
|
|
414
419
|
|
|
415
420
|
```javascript
|
|
416
421
|
import assert from "node:assert/strict";
|
|
@@ -444,15 +449,15 @@ assert.equal(state.total, 11);
|
|
|
444
449
|
state.list[0] = 5;
|
|
445
450
|
assert.equal(state.total, 15);
|
|
446
451
|
|
|
447
|
-
if (api.
|
|
448
|
-
assert.equal((api.
|
|
452
|
+
if (api.computed) {
|
|
453
|
+
assert.equal((api.getComputed || api.get)(state.$total), 15);
|
|
449
454
|
}
|
|
450
455
|
```
|
|
451
456
|
|
|
452
457
|
A production-quality adapter should also verify:
|
|
453
458
|
|
|
454
459
|
- Whole-array replacement, deletion, and length changes.
|
|
455
|
-
- Conditional
|
|
460
|
+
- Conditional computed dependencies and no-op writes.
|
|
456
461
|
- Effect cleanup and behavior under nested/batched updates.
|
|
457
462
|
- React StrictMode and unmount behavior when used with `ustor/react`.
|
|
458
463
|
- Owner/scope disposal and long-running memory behavior.
|
|
@@ -468,22 +473,23 @@ npm run profile
|
|
|
468
473
|
|
|
469
474
|
## React
|
|
470
475
|
|
|
471
|
-
Install React and import the optional
|
|
476
|
+
Install React and import the optional hook:
|
|
472
477
|
|
|
473
478
|
```bash
|
|
474
479
|
npm install react
|
|
475
480
|
```
|
|
476
481
|
|
|
477
482
|
```jsx
|
|
478
|
-
import {
|
|
483
|
+
import { useSelector } from "ustor/react";
|
|
479
484
|
|
|
480
485
|
// Uses the Preact configuration from Quickstart, including api.effect.
|
|
481
486
|
const state = store({ count: 1, items: [1, 2] });
|
|
487
|
+
const selectCount = () => state.count;
|
|
482
488
|
const selectTotal = () => state.items.reduce((sum, value) => sum + value, 0);
|
|
483
489
|
|
|
484
490
|
export function Counter() {
|
|
485
|
-
const count =
|
|
486
|
-
const total =
|
|
491
|
+
const count = useSelector(selectCount);
|
|
492
|
+
const total = useSelector(selectTotal);
|
|
487
493
|
|
|
488
494
|
return (
|
|
489
495
|
<button onClick={() => { state.count++; state.items.push(state.count); }}>
|
|
@@ -493,18 +499,18 @@ export function Counter() {
|
|
|
493
499
|
}
|
|
494
500
|
```
|
|
495
501
|
|
|
496
|
-
- `
|
|
497
|
-
-
|
|
502
|
+
- `useSelector(selector)` is the single React hook for both simple properties and derived values.
|
|
503
|
+
- It tracks all compatible signal reads in the selector, including array versions.
|
|
498
504
|
- Selectors must return a primitive or stable reference, not a fresh object on every read.
|
|
499
505
|
- Keep selector functions stable with `useCallback` when they are created inside a component.
|
|
500
506
|
- React is an optional peer dependency and is excluded from the core bundle.
|
|
501
507
|
- StrictMode and unmount cleanup are tested.
|
|
502
508
|
|
|
503
|
-
Plain property reads do not schedule React renders by themselves; use the
|
|
509
|
+
Plain property reads do not schedule React renders by themselves; use the hook or another React binding.
|
|
504
510
|
|
|
505
511
|
## SSR and Next.js
|
|
506
512
|
|
|
507
|
-
Yes. `
|
|
513
|
+
Yes. `useSelector` provides a server snapshot through `useSyncExternalStore`. Server rendering creates no effects; subscriptions start on the client.
|
|
508
514
|
|
|
509
515
|
The test suite verifies:
|
|
510
516
|
|
|
@@ -515,14 +521,14 @@ The test suite verifies:
|
|
|
515
521
|
|
|
516
522
|
### Next.js App Router example
|
|
517
523
|
|
|
518
|
-
Use the
|
|
524
|
+
Use the hook in a **Client Component**. Create request- or component-owned state instead of exporting a request-specific singleton.
|
|
519
525
|
|
|
520
526
|
```jsx
|
|
521
527
|
"use client";
|
|
522
528
|
|
|
523
529
|
import { useCallback, useState } from "react";
|
|
524
530
|
import { store } from "ustor";
|
|
525
|
-
import {
|
|
531
|
+
import { useSelector } from "ustor/react";
|
|
526
532
|
// Import your shared adapter configuration once:
|
|
527
533
|
// import "@/lib/ustor-setup";
|
|
528
534
|
|
|
@@ -531,12 +537,13 @@ export default function Counter({ initialCount }) {
|
|
|
531
537
|
count: initialCount,
|
|
532
538
|
items: [1, 2],
|
|
533
539
|
}));
|
|
540
|
+
const selectCount = useCallback(() => state.count, [state]);
|
|
534
541
|
const selectTotal = useCallback(
|
|
535
542
|
() => state.items.reduce((sum, value) => sum + value, 0),
|
|
536
543
|
[state],
|
|
537
544
|
);
|
|
538
|
-
const count =
|
|
539
|
-
const total =
|
|
545
|
+
const count = useSelector(selectCount);
|
|
546
|
+
const total = useSelector(selectTotal);
|
|
540
547
|
|
|
541
548
|
return (
|
|
542
549
|
<button onClick={() => { state.count++; state.items.push(1); }}>
|
|
@@ -572,7 +579,7 @@ Declarations are included for both entry points; no separate `@types/ustor` pack
|
|
|
572
579
|
|
|
573
580
|
```typescript
|
|
574
581
|
import { store } from "ustor";
|
|
575
|
-
import {
|
|
582
|
+
import { useSelector } from "ustor/react";
|
|
576
583
|
|
|
577
584
|
const state = store({
|
|
578
585
|
count: 1,
|
|
@@ -586,14 +593,13 @@ state.count = 2; // valid
|
|
|
586
593
|
|
|
587
594
|
const doubled: number = state.double;
|
|
588
595
|
const text: string = state.nested.text;
|
|
589
|
-
const total =
|
|
596
|
+
const total = useSelector(() => state.list.reduce((sum, value) => sum + value, 0));
|
|
590
597
|
```
|
|
591
598
|
|
|
592
599
|
Exported type utilities:
|
|
593
600
|
|
|
594
|
-
- `SignalApi<Signal,
|
|
601
|
+
- `SignalApi<Signal, Computed>` for custom adapters.
|
|
595
602
|
- `Store<T>` for reactive store inference.
|
|
596
|
-
- `SignalValue<Signal>` for React signal-value inference.
|
|
597
603
|
|
|
598
604
|
Run the repository's strict compile-time tests with:
|
|
599
605
|
|
|
@@ -605,7 +611,7 @@ npm run typecheck
|
|
|
605
611
|
|
|
606
612
|
The profiler uses Preact Signals and measures:
|
|
607
613
|
|
|
608
|
-
- High-frequency
|
|
614
|
+
- High-frequency computed property updates.
|
|
609
615
|
- Batched property updates.
|
|
610
616
|
- Bounded array mutation workloads.
|
|
611
617
|
- Repeated store/effect creation, updates, and disposal with heap/RSS samples.
|
|
@@ -672,6 +678,7 @@ npm test # build, type tests, unit/adapter/React/SSR tests, pack
|
|
|
672
678
|
npm run typecheck # strict TypeScript declaration tests
|
|
673
679
|
npm run test:coverage # enforced 100% runtime coverage
|
|
674
680
|
npm run test:watch
|
|
681
|
+
npm run examples # isolated Node examples and JSX/TypeScript checks
|
|
675
682
|
npm run profile:smoke
|
|
676
683
|
npm run build
|
|
677
684
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -76,14 +76,14 @@ const objectStore = (values, proto) => {
|
|
|
76
76
|
const desc = Object.getOwnPropertyDescriptor(values, key),
|
|
77
77
|
accessor = desc.get || desc.set,
|
|
78
78
|
getter = desc.get?.bind(state),
|
|
79
|
-
|
|
79
|
+
createComputed = getter && api.computed,
|
|
80
80
|
signal = accessor ? undefined : api.is(desc.value) ? desc.value : api.signal(resolve(desc.value));
|
|
81
|
-
let
|
|
82
|
-
const getSignal = () => getter ? (
|
|
81
|
+
let computed;
|
|
82
|
+
const getSignal = () => getter ? (computed ||= createComputed(getter)) : signal;
|
|
83
83
|
Object.defineProperty(state, key, {
|
|
84
84
|
get: accessor
|
|
85
85
|
? getter
|
|
86
|
-
? () =>
|
|
86
|
+
? () => createComputed ? (api.getComputed || api.get)(getSignal()) : getter()
|
|
87
87
|
: undefined
|
|
88
88
|
: () => api.get(signal),
|
|
89
89
|
set: accessor
|
|
@@ -94,7 +94,7 @@ const objectStore = (values, proto) => {
|
|
|
94
94
|
enumerable: desc.enumerable,
|
|
95
95
|
});
|
|
96
96
|
if (
|
|
97
|
-
(!accessor ||
|
|
97
|
+
(!accessor || createComputed) &&
|
|
98
98
|
typeof key === "string" &&
|
|
99
99
|
!key.startsWith("$") &&
|
|
100
100
|
!Object.hasOwn(values, "$" + key)
|
package/dist/index.es.js
CHANGED
|
@@ -74,14 +74,14 @@ const objectStore = (values, proto) => {
|
|
|
74
74
|
const desc = Object.getOwnPropertyDescriptor(values, key),
|
|
75
75
|
accessor = desc.get || desc.set,
|
|
76
76
|
getter = desc.get?.bind(state),
|
|
77
|
-
|
|
77
|
+
createComputed = getter && api.computed,
|
|
78
78
|
signal = accessor ? undefined : api.is(desc.value) ? desc.value : api.signal(resolve(desc.value));
|
|
79
|
-
let
|
|
80
|
-
const getSignal = () => getter ? (
|
|
79
|
+
let computed;
|
|
80
|
+
const getSignal = () => getter ? (computed ||= createComputed(getter)) : signal;
|
|
81
81
|
Object.defineProperty(state, key, {
|
|
82
82
|
get: accessor
|
|
83
83
|
? getter
|
|
84
|
-
? () =>
|
|
84
|
+
? () => createComputed ? (api.getComputed || api.get)(getSignal()) : getter()
|
|
85
85
|
: undefined
|
|
86
86
|
: () => api.get(signal),
|
|
87
87
|
set: accessor
|
|
@@ -92,7 +92,7 @@ const objectStore = (values, proto) => {
|
|
|
92
92
|
enumerable: desc.enumerable,
|
|
93
93
|
});
|
|
94
94
|
if (
|
|
95
|
-
(!accessor ||
|
|
95
|
+
(!accessor || createComputed) &&
|
|
96
96
|
typeof key === "string" &&
|
|
97
97
|
!key.startsWith("$") &&
|
|
98
98
|
!Object.hasOwn(values, "$" + key)
|
package/dist/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var ustor=function(e){const t={},r=Symbol(),n=new WeakMap,s=/^(copyWithin|fill|pop|push|reverse|shift|sort|splice|unshift)$/,o=e=>!!e?.[r],i=e=>{const t=e&&Object.getPrototypeOf(e);return null!=e&&(null===t||t?.constructor===Object)},c=e=>Array.isArray(e)||i(e)?a(e):e,a=(e,a)=>{if(o(e))return e;if(!Array.isArray(e)&&!i(e))return e;const l=n.get(e);if(l)return l;if(!(t.signal&&t.get&&t.set&&t.is))throw new Error("ustor: configure api.signal, api.get, api.set and api.is");return Array.isArray(e)?(e=>{const o=t.signal(0);let i=0,a=!1;const l=()=>t.get(o),u=()=>{a=!1,t.set(o,t.get(o)+1)},f=new Proxy(e,{get:(e,t,n)=>{if(t===r)return!0;l();const o=Reflect.get(e,t,n);return"string"==typeof t&&s.test(t)&&"function"==typeof o?(...e)=>{i++;try{return o.apply(n,e)}finally{! --i&&a&&u()}}:o},set:(e,t,r)=>{const n=c(r);return!!Object.is(e[t],n)||!!Reflect.set(e,t,n)&&(i?a=!0:u(),!0)},deleteProperty:(e,t)=>!Object.hasOwn(e,t)||!!Reflect.deleteProperty(e,t)&&(i?a=!0:u(),!0),ownKeys:e=>(l(),Reflect.ownKeys(e)),has:(e,t)=>(l(),Reflect.has(e,t))});return n.set(e,f),e.forEach((
|
|
1
|
+
var ustor=function(e){const t={},r=Symbol(),n=new WeakMap,s=/^(copyWithin|fill|pop|push|reverse|shift|sort|splice|unshift)$/,o=e=>!!e?.[r],i=e=>{const t=e&&Object.getPrototypeOf(e);return null!=e&&(null===t||t?.constructor===Object)},c=e=>Array.isArray(e)||i(e)?a(e):e,a=(e,a)=>{if(o(e))return e;if(!Array.isArray(e)&&!i(e))return e;const l=n.get(e);if(l)return l;if(!(t.signal&&t.get&&t.set&&t.is))throw new Error("ustor: configure api.signal, api.get, api.set and api.is");return Array.isArray(e)?(e=>{const o=t.signal(0);let i=0,a=!1;const l=()=>t.get(o),u=()=>{a=!1,t.set(o,t.get(o)+1)},f=new Proxy(e,{get:(e,t,n)=>{if(t===r)return!0;l();const o=Reflect.get(e,t,n);return"string"==typeof t&&s.test(t)&&"function"==typeof o?(...e)=>{i++;try{return o.apply(n,e)}finally{! --i&&a&&u()}}:o},set:(e,t,r)=>{const n=c(r);return!!Object.is(e[t],n)||!!Reflect.set(e,t,n)&&(i?a=!0:u(),!0)},deleteProperty:(e,t)=>!Object.hasOwn(e,t)||!!Reflect.deleteProperty(e,t)&&(i?a=!0:u(),!0),ownKeys:e=>(l(),Reflect.ownKeys(e)),has:(e,t)=>(l(),Reflect.has(e,t))});return n.set(e,f),e.forEach((t,r)=>{e[r]=c(t)}),f})(e):((e,s)=>{const o=Object.create(void 0===s?Object.getPrototypeOf(e):s);return n.set(e,o),Reflect.ownKeys(e).forEach(r=>{const n=Object.getOwnPropertyDescriptor(e,r),s=n.get||n.set,i=n.get?.bind(o),a=i&&t.computed,l=s?void 0:t.is(n.value)?n.value:t.signal(c(n.value));let u;const f=()=>i?u||=a(i):l;Object.defineProperty(o,r,{get:s?i?()=>a?(t.getComputed||t.get)(f()):i():void 0:()=>t.get(l),set:s?n.set?.bind(o):!1===n.writable?void 0:e=>t.set(l,c(e)),enumerable:n.enumerable}),s&&!a||"string"!=typeof r||r.startsWith("$")||Object.hasOwn(e,"$"+r)||Object.defineProperty(o,"$"+r,{get:f,enumerable:!1})}),Object.defineProperty(o,r,{value:!0}),Object.seal(o)})(e,a)};return e.api=t,e.is=o,e.store=a,e}({});
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ustor={})}(this,
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ustor={})}(this,function(e){const t={},r=Symbol(),n=new WeakMap,s=/^(copyWithin|fill|pop|push|reverse|shift|sort|splice|unshift)$/,o=e=>!!e?.[r],i=e=>{const t=e&&Object.getPrototypeOf(e);return null!=e&&(null===t||t?.constructor===Object)},c=e=>Array.isArray(e)||i(e)?a(e):e,a=(e,a)=>{if(o(e))return e;if(!Array.isArray(e)&&!i(e))return e;const f=n.get(e);if(f)return f;if(!(t.signal&&t.get&&t.set&&t.is))throw new Error("ustor: configure api.signal, api.get, api.set and api.is");return Array.isArray(e)?(e=>{const o=t.signal(0);let i=0,a=!1;const f=()=>t.get(o),l=()=>{a=!1,t.set(o,t.get(o)+1)},u=new Proxy(e,{get:(e,t,n)=>{if(t===r)return!0;f();const o=Reflect.get(e,t,n);return"string"==typeof t&&s.test(t)&&"function"==typeof o?(...e)=>{i++;try{return o.apply(n,e)}finally{! --i&&a&&l()}}:o},set:(e,t,r)=>{const n=c(r);return!!Object.is(e[t],n)||!!Reflect.set(e,t,n)&&(i?a=!0:l(),!0)},deleteProperty:(e,t)=>!Object.hasOwn(e,t)||!!Reflect.deleteProperty(e,t)&&(i?a=!0:l(),!0),ownKeys:e=>(f(),Reflect.ownKeys(e)),has:(e,t)=>(f(),Reflect.has(e,t))});return n.set(e,u),e.forEach((t,r)=>{e[r]=c(t)}),u})(e):((e,s)=>{const o=Object.create(void 0===s?Object.getPrototypeOf(e):s);return n.set(e,o),Reflect.ownKeys(e).forEach(r=>{const n=Object.getOwnPropertyDescriptor(e,r),s=n.get||n.set,i=n.get?.bind(o),a=i&&t.computed,f=s?void 0:t.is(n.value)?n.value:t.signal(c(n.value));let l;const u=()=>i?l||=a(i):f;Object.defineProperty(o,r,{get:s?i?()=>a?(t.getComputed||t.get)(u()):i():void 0:()=>t.get(f),set:s?n.set?.bind(o):!1===n.writable?void 0:e=>t.set(f,c(e)),enumerable:n.enumerable}),s&&!a||"string"!=typeof r||r.startsWith("$")||Object.hasOwn(e,"$"+r)||Object.defineProperty(o,"$"+r,{get:u,enumerable:!1})}),Object.defineProperty(o,r,{value:!0}),Object.seal(o)})(e,a)};e.api=t,e.is=o,e.store=a});
|
package/dist/react.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("react");const r={}
|
|
1
|
+
"use strict";var e=require("react");const r={};exports.useSelector=t=>{const c=e.useCallback(e=>{if(!r.effect)throw new Error("ustor/react: configure api.effect with cleanup support");let c=!0;return r.effect(()=>{t(),c?c=!1:e()})},[t]);return e.useSyncExternalStore(c,t,t)};
|
package/dist/react.es.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useCallback as
|
|
1
|
+
import{useCallback as r,useSyncExternalStore as t}from"react";const e={},o=o=>{const c=r(r=>{if(!e.effect)throw new Error("ustor/react: configure api.effect with cleanup support");let t=!0;return e.effect(()=>{o(),t?t=!1:r()})},[o]);return t(c,o,o)};export{o as useSelector};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ustor",
|
|
3
3
|
"description": "A tiny, dependency-free reactive store with pluggable signal adapters and optional React hooks.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"source": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.nodenext.json",
|
|
41
41
|
"test:watch": "vitest",
|
|
42
42
|
"test:coverage": "vitest --run --coverage",
|
|
43
|
+
"examples": "npm run examples:node && npm run examples:typecheck",
|
|
44
|
+
"examples:node": "npm run build && node scripts/run-examples.mjs",
|
|
45
|
+
"examples:typecheck": "tsc -p tsconfig.examples.json",
|
|
43
46
|
"profile": "node --expose-gc scripts/profile.mjs",
|
|
44
47
|
"profile:smoke": "node --expose-gc scripts/profile.mjs --updates=5000 --array-updates=500 --seconds=0.5 --cycles=500 --sample-ms=100",
|
|
45
48
|
"prepack": "npm run build"
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"@preact/signals-core": "^1.14.4",
|
|
49
52
|
"@rollup/plugin-terser": "^1.0.2",
|
|
50
53
|
"@tanstack/store": "^0.11.2",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
51
55
|
"@vitest/coverage-v8": "^5.0.5",
|
|
52
56
|
"@vue/reactivity": "^3.5.44",
|
|
53
57
|
"@webreflection/alien-signals": "^0.4.2",
|
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export interface SignalApi<Signal = any,
|
|
1
|
+
export interface SignalApi<Signal = any, Computed = any> {
|
|
2
2
|
signal(value: any): Signal;
|
|
3
3
|
get(signal: Signal): any;
|
|
4
4
|
set(signal: Signal, value: any): void;
|
|
5
5
|
is(value: unknown): boolean;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
computed?(fn: () => any): Computed;
|
|
7
|
+
getComputed?(computed: Computed): any;
|
|
8
8
|
effect?(fn: () => void): () => void;
|
|
9
9
|
[key: string]: unknown;
|
|
10
10
|
}
|
package/types/react.d.ts
CHANGED
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
export type SignalValue<Signal> = Signal extends { state: readonly (infer Value)[] }
|
|
2
|
-
? Value
|
|
3
|
-
: Signal extends { value: infer Value }
|
|
4
|
-
? Value
|
|
5
|
-
: Signal extends { get(): infer Value }
|
|
6
|
-
? Value
|
|
7
|
-
: Signal extends () => infer Value
|
|
8
|
-
? Value
|
|
9
|
-
: Signal extends readonly [() => infer Value, ...any[]]
|
|
10
|
-
? Value
|
|
11
|
-
: unknown;
|
|
12
|
-
|
|
13
1
|
/** Subscribe to a selector that returns a primitive or stable reference. */
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
/** Subscribe to one underlying signal exposed as state.$property. */
|
|
17
|
-
export declare function useSignal<Signal>(signal: Signal): SignalValue<Signal>;
|
|
2
|
+
export declare function useSelector<Value>(selector: () => Value): Value;
|