ustor 0.1.0 → 0.2.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
@@ -1,257 +1,687 @@
1
1
  # UStor
2
2
 
3
- [![tests](https://github.com/kethan/ustor/actions/workflows/node.js.yml/badge.svg)](https://github.com/ustor/actions/workflows/node.js.yml) [![Version](https://img.shields.io/npm/v/ustor.svg?color=success&style=flat-square)](https://www.npmjs.com/package/ustor) [![Badge size](https://deno.bundlejs.com/badge?q=ustor&treeshake=[*]&config={"compression":"brotli"})](https://unpkg.com/ustor) [![Badge size](https://deno.bundlejs.com/badge?q=ustor&treeshake=[*]&config={"compression":"gzip"})](https://unpkg.com/ustor)
3
+ [![tests](https://github.com/kethan/ustor/actions/workflows/node.js.yml/badge.svg)](https://github.com/kethan/ustor/actions/workflows/node.js.yml)
4
+ [![Version](https://img.shields.io/npm/v/ustor.svg?color=success&style=flat-square)](https://www.npmjs.com/package/ustor)
5
+ [![Bundle size](https://deno.bundlejs.com/badge?q=ustor&treeshake=[*]&config={%22compression%22:%22brotli%22})](https://bundlejs.com/?q=ustor)
4
6
 
5
- # Reactive Store Library
7
+ A tiny, dependency-free reactive store for JavaScript objects and arrays. UStor uses your signal implementation instead of bundling a reactivity engine.
6
8
 
7
- This library provides a powerful reactivity system for creating stateful objects using signals, integrated seamlessly into JavaScript objects and arrays. This library does not depend on any specific reactive library and can be used by any UI framework to manage and react to changes in state effectively.
9
+ - **Small:** about 1.8 KiB minified and less than 1 KiB gzipped.
10
+ - **Deeply reactive:** nested objects, arrays, getters, and existing signals.
11
+ - **Framework-friendly:** 16 tested signal/store adapters plus optional React hooks.
12
+ - **SSR-ready:** server rendering and hydration are tested.
13
+ - **Typed:** TypeScript declarations for the core and React entry points.
14
+ - **Interoperable:** ESM, CommonJS, IIFE, and UMD builds.
8
15
 
9
- ## Table of Contents
16
+ ## Contents
10
17
 
11
18
  - [Installation](#installation)
12
- - [Setup](#setup)
13
- - [API](#api)
14
- - [store(values, proto)](#storevalues-proto)
15
- - [is(value)](#isvalue)
16
- - [api.is](#apiis)
17
- - [api.signal](#apisignal)
18
- - [api.get](#apiget)
19
- - [api.set](#apiset)
20
- - [Usage](#usage)
21
- - [Basic Usage](#basic-usage)
22
- - [Advanced Usage](#advanced-usage)
23
- - [Deeply Nested Structures](#deeply-nested-structures)
24
- - [Arrays and Objects](#arrays-and-objects)
25
- - [Reactivity and Effects](#reactivity-and-effects)
26
- - [`state.$prop`](#stateprop)
27
-
28
- - [Integration with Solid.js, Preact Signals or any](#integration-with-solidjs-preact-signals-or-any)
19
+ - [Quickstart](#quickstart)
20
+ - [Common use cases](#common-use-cases)
21
+ - [API reference](#api-reference)
22
+ - [Arrays](#arrays)
23
+ - [Supported adapters](#supported-adapters)
24
+ - [Porting any signal library](#porting-any-signal-library)
25
+ - [React](#react)
26
+ - [SSR and Next.js](#ssr-and-nextjs)
27
+ - [TypeScript](#typescript)
28
+ - [Profiling and memory analysis](#profiling-and-memory-analysis)
29
+ - [Production notes](#production-notes)
30
+ - [Development](#development)
29
31
 
30
32
  ## Installation
31
33
 
32
- To use this library, you can install it using npm or yarn:
33
-
34
34
  ```bash
35
35
  npm install ustor
36
- yarn add ustor
37
36
  ```
38
37
 
39
- This library can be integrated with `solid-js`, `@preact/signals-core`, `ulive`, `usignal`, `oby`, `sinuous` any other libarary you can think of for managing reactive signals.
38
+ Install the signal library you want to use. The examples below use `@preact/signals-core`:
39
+
40
+ ```bash
41
+ npm install @preact/signals-core
42
+ ```
43
+
44
+ UStor targets modern JavaScript environments with `Proxy`, `Reflect`, `WeakMap`, and `Object.hasOwn`. The runtime is not transpiled or polyfilled for legacy browsers.
45
+
46
+ ## Quickstart
47
+
48
+ Configure the adapter once, before creating stores:
49
+
50
+ ```javascript
51
+ import { store, api } from "ustor";
52
+ import { signal, computed, effect } from "@preact/signals-core";
53
+
54
+ Object.assign(api, {
55
+ signal,
56
+ memo: computed,
57
+ effect,
58
+ get: (value) => value.value,
59
+ getMemo: (value) => value.value,
60
+ set: (target, value) => { target.value = value; },
61
+ is: (value) => typeof value?.peek === "function",
62
+ });
63
+ ```
64
+
65
+ Create a store and use ordinary property reads and writes:
66
+
67
+ ```javascript
68
+ const state = store({
69
+ count: 1,
70
+ items: [1, 2],
71
+ get double() {
72
+ return this.count * 2;
73
+ },
74
+ });
75
+
76
+ const dispose = effect(() => {
77
+ console.log(state.double, state.items.length);
78
+ });
79
+
80
+ state.count = 3; // effects using count or double update
81
+ state.items.push(3); // array mutations are reactive too
82
+
83
+ console.log(state.count); // 3
84
+ console.log(state.double); // 6
85
+ console.log(state.$count.value); // 3: the underlying Preact signal
86
+
87
+ dispose();
88
+ ```
89
+
90
+ **Important:** `api` is shared by stores from the same UStor module instance. Configure one adapter and do not switch implementations while existing stores are still in use.
91
+
92
+ ## Common use cases
93
+
94
+ ### Shopping cart with reactive items and totals
95
+
96
+ ```javascript
97
+ const cart = store({
98
+ items: [],
99
+ get total() {
100
+ return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
101
+ },
102
+ });
103
+
104
+ cart.items.push({ id: "book", price: 12, quantity: 1 });
105
+ console.log(cart.total); // 12
106
+
107
+ cart.items[0].quantity = 2;
108
+ console.log(cart.total); // 24
109
+
110
+ cart.items.push({ id: "pen", price: 2, quantity: 3 });
111
+ console.log(cart.total); // 30
112
+ ```
113
+
114
+ Objects inserted into arrays become reactive stores. Their existing fields can be updated, while their object schema remains sealed.
115
+
116
+ ### Form state with derived validation
117
+
118
+ ```javascript
119
+ const form = store({
120
+ email: "",
121
+ password: "",
122
+ get valid() {
123
+ return this.email.includes("@") && this.password.length >= 8;
124
+ },
125
+ });
126
+
127
+ form.email = "person@example.com";
128
+ form.password = "very-secret";
129
+ console.log(form.valid); // true
130
+ ```
40
131
 
41
- ## Setup
132
+ Use an effect or a UI binding to react to `form.valid`; reading it manually always returns the current value.
42
133
 
43
- First, import the `store` function and the `api` object. You need to initialize `api` with your preferred signal implementation.
134
+ ### Shared UI state
44
135
 
45
136
  ```javascript
46
- import { store, api } from "./src";
47
- import { createSignal } from "solid-js";
48
- // import { signal } from '@preact/signals-core'; // 'usignal' 'ulive'
137
+ const ui = store({
138
+ theme: "light",
139
+ sidebar: { open: false },
140
+ notifications: [],
141
+ });
142
+
143
+ ui.sidebar.open = true;
144
+ ui.notifications.push({ id: 1, message: "Saved" });
145
+ ui.theme = "dark";
146
+ ```
49
147
 
50
- // Solid.js setup
51
- api.signal = createSignal;
52
- api.get = (v) => v[0]();
53
- api.set = (signal, v) => signal[1](v);
54
- api.is = (v) =>
55
- (Array.isArray(v) &&
56
- typeof v[0] === "function" &&
57
- typeof v[1] === "function") ||
58
- v[0]?.name?.includes("readSignal");
148
+ Create request- or component-specific stores when the state must not be shared. See [SSR and Next.js](#ssr-and-nextjs).
59
149
 
60
- // @preact/signals-core, usignal or ulive setup
61
- api.signal = signal;
62
- api.get = (v) => v.value;
63
- api.set = (signal, v) => (signal.value = v);
64
- api.is = (v) => v?.peek;
150
+ ## API reference
151
+
152
+ ### `store(values, proto?)`
153
+
154
+ Creates and returns a reactive object or array store.
155
+
156
+ ```javascript
157
+ const state = store({ nested: { value: 1 } });
158
+ state.nested.value = 2;
65
159
  ```
66
160
 
67
- ## API
161
+ Behavior:
68
162
 
69
- ### `store(values, proto)`
163
+ - Existing object properties become signal-backed accessors.
164
+ - Nested objects and arrays are recursively converted.
165
+ - Existing stores are returned unchanged.
166
+ - Repeated and cyclic references preserve identity.
167
+ - `proto` optionally replaces the new object store's prototype.
168
+ - Primitives, including `null` and `undefined`, are returned unchanged.
169
+ - Class instances, dates, maps, sets, and similar objects are treated as atomic values.
170
+ - Calling `store` for an object or array before configuring the required adapter throws an error.
70
171
 
71
- The `store` function creates a stateful object that automatically converts properties into reactive signals.
172
+ Object stores have a **sealed schema**. Adding or deleting object properties is unsupported; replace the containing object when its shape must change. Arrays remain structurally mutable.
72
173
 
73
- - **values**: The initial values for the store, which can be objects, arrays, or primitives.
74
- - **proto**: Optional prototype to set for the new store object.
174
+ ```javascript
175
+ const state = store({ profile: { name: "Ada" } });
75
176
 
76
- The function returns a reactive store object that maintains the structure of the original `values` while adding reactivity.
177
+ // Instead of adding a new field to profile:
178
+ state.profile = { name: "Ada", active: true };
179
+ ```
77
180
 
78
181
  ### `is(value)`
79
182
 
80
- The `is` function checks if a given value is a reactive store instance.
183
+ Returns `true` for a reactive store and `false` for every other value.
184
+
185
+ ```javascript
186
+ is(state); // true
187
+ is({}); // false
188
+ is(null); // false
189
+ ```
190
+
191
+ ### `state.$property`
192
+
193
+ Exposes the underlying signal for a data property, or the underlying memo for a memoized getter.
194
+
195
+ ```javascript
196
+ const state = store({ count: 0 });
197
+
198
+ state.$count.value = 2; // Preact signal example
199
+ console.log(state.count); // 2
200
+ ```
201
+
202
+ Aliases are non-enumerable. Getters without `api.memo` do not have a `$` alias. If an input explicitly defines a conflicting key such as `$count`, the user-defined property takes precedence.
203
+
204
+ ### Adapter API
205
+
206
+ Required operations:
81
207
 
82
- - **value**: The value to check.
208
+ | Operation | Purpose |
209
+ | --- | --- |
210
+ | `api.signal(value)` | Create a signal. |
211
+ | `api.get(signal)` | Read its current value. |
212
+ | `api.set(signal, value)` | Update its value. |
213
+ | `api.is(value)` | Detect an existing signal without misclassifying ordinary functions or objects. |
83
214
 
84
- Returns `true` if the value is a reactive store, otherwise `false`.
215
+ Optional operations:
85
216
 
86
- ### `api.is`
217
+ | Operation | Purpose |
218
+ | --- | --- |
219
+ | `api.memo(fn)` | Create a lazy computed value. |
220
+ | `api.getMemo(memo)` | Read a memo; defaults to `api.get`. |
221
+ | `api.effect(fn)` | Required by `ustor/react`; must auto-track synchronously and return a cleanup function. |
87
222
 
88
- The api.is function checks if a given value is a signal.
223
+ Without `api.memo`, getters execute on each access and still participate in compatible effects. With `api.memo`, getters are lazy and cached while their dependencies remain unchanged.
89
224
 
90
- - **value**: The value to check.
225
+ ## Arrays
91
226
 
92
- Returns true if the value is a signal, otherwise false.
227
+ Arrays are proxies over their mutable input target. Mutate the returned store, not the original input, to send notifications.
228
+
229
+ ```javascript
230
+ const state = store({ list: [1, 2, 3] });
93
231
 
94
- ### `api.signal`
232
+ state.list[1] = 5;
233
+ state.list.push(4);
234
+ state.list.splice(0, 1);
235
+ state.list.length = 2;
236
+ delete state.list[0];
237
+ ```
95
238
 
96
- The `api.signal` function is used to create a reactive signal.
239
+ Tracked operations include:
97
240
 
98
- - **value**: The initial value for the signal.
241
+ - Index assignment and whole-array replacement.
242
+ - `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, and `copyWithin`.
243
+ - Length changes and `delete`.
244
+ - Iteration, key enumeration, and the `in` operator.
245
+ - Objects inserted by assignment or mutating methods.
246
+ - Sparse arrays and nullish entries.
99
247
 
100
- Returns a signal that can be used to create reactive state within the store.
248
+ Each standard mutating method emits at most one notification after it finishes. Deterministic model tests compare randomized mutation sequences against native arrays.
101
249
 
102
- ### `api.get`
250
+ ## Supported adapters
103
251
 
104
- The `api.get` function retrieves the current value of a signal.
252
+ The configurations in `adapters.spec.js` are executable documentation and include lifecycle scopes where appropriate.
105
253
 
106
- - **signal**: The signal whose value you want to retrieve.
254
+ | Package | Getter support |
255
+ | --- | --- |
256
+ | `@preact/signals-core` | Automatic memoization |
257
+ | `solid-js` | Automatic memoization |
258
+ | `oby` | Automatic memoization |
259
+ | `sinuous/observable` | Automatic memoization |
260
+ | `ulive` | Automatic memoization |
261
+ | `usignal` | Automatic memoization |
262
+ | `@vue/reactivity` | Automatic memoization using `shallowRef` |
263
+ | `alien-signals` | Automatic memoization |
264
+ | `s-js` | Automatic memoization using the provided branded factory |
265
+ | `mobx` | Automatic memoization using shallow observable boxes |
266
+ | `@tanstack/store` | Automatic memoization using the provided value wrapper |
267
+ | `@webreflection/signal` | Automatic memoization |
268
+ | `@webreflection/signals` | Automatic memoization |
269
+ | `@webreflection/alien-signals` | Automatic memoization |
270
+ | `rxjs` | Basic: `BehaviorSubject` values and direct getters |
271
+ | `nanostores` | Basic: atoms and direct getters |
107
272
 
108
- Returns the current value of the signal.
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 memoization is not advertised for them.
109
274
 
110
- ### `api.set`
275
+ Adapter-specific notes:
111
276
 
112
- The `api.set` function updates the value of a signal.
277
+ - Vue and MobX use shallow containers to avoid wrapping UStor's proxies twice.
278
+ - 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 memoized stores inside an S.js root.
280
+ - Solid.js, Vue, S.js, and similar libraries may require their own owner/scope lifecycle for short-lived memos.
113
281
 
114
- - **signal**: The signal whose value you want to update.
115
- - **value**: The new value to set.
282
+ ## Porting any signal library
116
283
 
117
- Updates the signal with the provided value.
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 memo/effect functions when supported.
118
285
 
119
- ## Usage
286
+ ### 1. Map the four required operations
120
287
 
121
- ### Basic Usage
288
+ Use this template and replace the example method names with your library's API:
122
289
 
123
290
  ```javascript
124
- const s = store({ a: 1, b: 2 });
125
- console.log(s.a); // 1
126
- s.a = 5;
127
- console.log(s.a); // 5
291
+ import { store, api } from "ustor";
292
+ import * as library from "your-signal-library";
293
+
294
+ Object.assign(api, {
295
+ // Create a container that can hold any value.
296
+ signal: (value) => library.create(value),
297
+
298
+ // Return the current value synchronously.
299
+ get: (signal) => library.read(signal),
300
+
301
+ // Store this exact value and notify synchronously.
302
+ set: (signal, value) => library.write(signal, value),
303
+
304
+ // Return true only for signals from this library.
305
+ is: (value) => library.isSignal(value),
306
+ });
128
307
  ```
129
308
 
130
- You can use the `store` function to create a reactive state object. Assigning a new value to `s.a` will automatically trigger updates wherever `s.a` is used.
309
+ For a `.value`-based library, the mapping usually looks like this:
310
+
311
+ ```javascript
312
+ Object.assign(api, {
313
+ signal: (value) => library.create(value),
314
+ get: (signal) => signal.value,
315
+ set: (signal, value) => { signal.value = value; },
316
+ is: (value) => value instanceof library.Signal,
317
+ });
318
+ ```
131
319
 
132
- ### Advanced Usage
320
+ For a callable-signal library:
133
321
 
134
322
  ```javascript
135
- const s = store({
136
- a: 2,
137
- b: 3,
138
- get sum() {
139
- return this.a + this.b;
140
- },
323
+ Object.assign(api, {
324
+ signal: (value) => library.create(value),
325
+ get: (signal) => signal(),
326
+ set: (signal, value) => signal(value),
327
+ is: (value) => typeof value === "function" && library.isSignal(value),
141
328
  });
329
+ ```
330
+
331
+ If the setter expects an updater rather than a direct value, adapt it explicitly:
142
332
 
143
- console.log(s.sum); // 5
144
- s.a = 5;
145
- console.log(s.sum); // 8
333
+ ```javascript
334
+ api.set = (signal, value) => signal.setState(() => value);
146
335
  ```
147
336
 
148
- Here, you can define getters that automatically compute derived state values, and these getters will update whenever the underlying signals change.
337
+ ### 2. Preserve values without double-wrapping
338
+
339
+ `api.signal` must be able to store `null`, `undefined`, functions, arrays, objects, and UStor proxies.
149
340
 
150
- ### Deeply Nested Structures
341
+ - Use a **shallow** signal/ref/box when the library offers one.
342
+ - Do not make the contained object deeply observable a second time.
343
+ - Do not call a function merely because it was stored as a value.
344
+ - If a library interprets a function argument as a computed getter, place the value inside a shallow container or use the tested wrapper pattern from `adapters.spec.js`.
151
345
 
152
- The `store` function can also handle deeply nested objects, converting nested properties into reactive signals:
346
+ For example, the Vue adapter uses `shallowRef`, and the MobX adapter uses `observable.box(value, { deep: false })`.
347
+
348
+ ### 3. Make `api.is` a precise brand check
349
+
350
+ A broad check can turn ordinary values into signals accidentally. Prefer `instanceof`, a library-provided brand check, or a documented marker.
351
+
352
+ ```javascript
353
+ // Risky for callable-signal libraries: ordinary functions are values too.
354
+ api.is = (value) => typeof value === "function"; // Do not use this by itself.
355
+ ```
356
+
357
+ If the library has no brand, create signals through a factory and track them in a `WeakSet`:
153
358
 
154
359
  ```javascript
155
- const s = store({ nested: { deep: { value: 10 } } });
156
- const deepValue = s.nested.deep.value * 2;
360
+ const signals = new WeakSet();
361
+
362
+ api.signal = (value) => {
363
+ const signal = library.create(value);
364
+ signals.add(signal);
365
+ return signal;
366
+ };
157
367
 
158
- console.log(deepValue); // 20
159
- s.nested.deep.value = 15;
160
- console.log(s.nested.deep.value) * 2; // 30
368
+ api.is = (value) => signals.has(value);
161
369
  ```
162
370
 
163
- ### Arrays and Objects
371
+ Create external signals through `api.signal` when using this approach. The S.js configuration in `adapters.spec.js` demonstrates it.
372
+
373
+ ### 4. Add memoization only when dependencies are automatic
164
374
 
165
- This library can handle arrays and objects seamlessly, automatically wrapping array elements with reactivity:
375
+ If the library can compute a value by automatically tracking signals read inside `fn`, add:
166
376
 
167
377
  ```javascript
168
- const s = store({ list: [1, 2, 3] });
169
- const sum = s.list.reduce((acc, item) => acc + item, 0);
378
+ Object.assign(api, {
379
+ memo: (fn) => library.computed(fn),
380
+ getMemo: (memo) => library.read(memo),
381
+ });
382
+ ```
170
383
 
171
- console.log(sum); // 6
172
- s.list = [1, 5, 3];
173
- console.log(s.list.reduce((acc, item) => acc + item, 0)); // 9
384
+ The memo may be lazy or eager, but it must invalidate when any dependency changes, including conditional dependencies.
385
+
386
+ If the library requires an explicit dependency list—as with RxJS or Nanostores—**omit `memo` and `getMemo`**. UStor then uses direct getters evaluated on access. Do not emulate automatic memoization incorrectly.
387
+
388
+ ### 5. Add effects for React integration only when compatible
389
+
390
+ `ustor/react` requires this additional contract:
391
+
392
+ ```javascript
393
+ api.effect = (fn) => {
394
+ const dispose = library.effect(fn);
395
+ return () => dispose(); // Always return a cleanup function.
396
+ };
174
397
  ```
175
398
 
176
- ### Reactivity and Effects
399
+ A compatible effect must:
400
+
401
+ 1. Run `fn` and automatically track its synchronous signal reads.
402
+ 2. Rerun synchronously after a dependency changes.
403
+ 3. Return a cleanup function, or be wrapped in an owner/root that provides one.
404
+
405
+ The core store does not require `api.effect`. Basic adapters without automatic dependency discovery can still provide ordinary state reads and writes.
406
+
407
+ ### 6. Configure once and manage lifecycle
408
+
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 memos/effects, create short-lived stores in that lifecycle and dispose them with their owner.
410
+
411
+ ### 7. Validate the adapter
177
412
 
178
- The signal value trigger with effects to track dependencies and automatically re-run whenever dependencies change:
413
+ This Node script checks the required behavior, function preservation, nested updates, arrays, and optional memoization:
179
414
 
180
415
  ```javascript
181
- const s = store({ a: 1 });
182
- let effectValue = 0;
416
+ import assert from "node:assert/strict";
417
+ import { store, api } from "ustor";
418
+ // Configure your adapter before these assertions.
183
419
 
184
- effect(() => {
185
- effectValue = s.a * 2;
420
+ const external = api.signal(1);
421
+ const state = store({
422
+ value: external,
423
+ nested: { count: 2 },
424
+ list: [1, 2],
425
+ fn: () => 7,
426
+ get total() {
427
+ return this.nested.count + this.list.reduce((sum, value) => sum + value, 0);
428
+ },
186
429
  });
187
430
 
188
- console.log(effectValue); // 2
189
- s.a = 4;
190
- console.log(effectValue); // 8
431
+ assert.equal(state.value, 1);
432
+ assert.equal(state.$value, external);
433
+ assert.equal(state.fn(), 7); // The function remains a value.
434
+
435
+ state.value = 3;
436
+ assert.equal(api.get(external), 3);
437
+
438
+ state.nested.count = 4;
439
+ assert.equal(state.total, 7);
440
+
441
+ state.list.push(4);
442
+ assert.equal(state.total, 11);
443
+
444
+ state.list[0] = 5;
445
+ assert.equal(state.total, 15);
446
+
447
+ if (api.memo) {
448
+ assert.equal((api.getMemo || api.get)(state.$total), 15);
449
+ }
191
450
  ```
192
451
 
193
- ### `state.$prop`
452
+ A production-quality adapter should also verify:
194
453
 
195
- The `$` properties are automatically generated for each property in the store. These `$`-prefixed properties contain the underlying signal, providing direct access to the signal itself, separate from the reactive value it holds. The `$` properties are non-enumerable and are useful when you need to access or manipulate the signal directly, rather than the reactive value.
454
+ - Whole-array replacement, deletion, and length changes.
455
+ - Conditional memo dependencies and no-op writes.
456
+ - Effect cleanup and behavior under nested/batched updates.
457
+ - React StrictMode and unmount behavior when used with `ustor/react`.
458
+ - Owner/scope disposal and long-running memory behavior.
459
+ - The library's ESM and CommonJS entry points in your target bundler.
196
460
 
197
- For example, if you have a store with a count property:
461
+ For working examples, copy the closest configuration from `adapters.spec.js`, then run:
198
462
 
199
- ```js
200
- const s = store({ count: 0 });
201
- console.log(s.count); // 0
202
- console.log(s.$count); // The underlying signal object
463
+ ```bash
464
+ npm test
465
+ npm run test:coverage
466
+ npm run profile
203
467
  ```
204
- ## Integration with Solid.js, Preact Signals or any
205
468
 
206
- This library can be used with Solid.js, Preact Signals, or any other UI framework to provide reactive signals.
469
+ ## React
207
470
 
208
- To use Solid.js:
471
+ Install React and import the optional hooks:
209
472
 
210
- ```javascript
211
- import { createSignal } from "solid-js";
473
+ ```bash
474
+ npm install react
475
+ ```
476
+
477
+ ```jsx
478
+ import { useSignal, useStore } from "ustor/react";
479
+
480
+ // Uses the Preact configuration from Quickstart, including api.effect.
481
+ const state = store({ count: 1, items: [1, 2] });
482
+ const selectTotal = () => state.items.reduce((sum, value) => sum + value, 0);
212
483
 
213
- api.signal = createSignal;
214
- api.get = (v) => v?.[0]();
215
- api.set = (signal, v) => signal?.[1](v);
216
- api.is = (v) =>
217
- (Array.isArray(v) &&
218
- typeof v[0] === "function" &&
219
- typeof v[1] === "function") ||
220
- v?.[0]?.name?.includes("readSignal");
484
+ export function Counter() {
485
+ const count = useSignal(state.$count);
486
+ const total = useStore(selectTotal);
487
+
488
+ return (
489
+ <button onClick={() => { state.count++; state.items.push(state.count); }}>
490
+ {count} / {total}
491
+ </button>
492
+ );
493
+ }
221
494
  ```
222
495
 
223
- To use Preact Signals, usignal, ulive, etc:
496
+ - `useSignal(signal)` subscribes to one `$property` signal.
497
+ - `useStore(selector)` tracks all compatible signal reads in the selector, including array versions.
498
+ - Selectors must return a primitive or stable reference, not a fresh object on every read.
499
+ - Keep selector functions stable with `useCallback` when they are created inside a component.
500
+ - React is an optional peer dependency and is excluded from the core bundle.
501
+ - StrictMode and unmount cleanup are tested.
502
+
503
+ Plain property reads do not schedule React renders by themselves; use the hooks or another React binding.
504
+
505
+ ## SSR and Next.js
506
+
507
+ Yes. `useStore` and `useSignal` provide a server snapshot through `useSyncExternalStore`. Server rendering creates no effects; subscriptions start on the client.
508
+
509
+ The test suite verifies:
510
+
511
+ - Server rendering without `api.effect`.
512
+ - Independent per-request stores.
513
+ - Hydration without markup mismatches.
514
+ - Reactive updates after hydration.
515
+
516
+ ### Next.js App Router example
517
+
518
+ Use the hooks in a **Client Component**. Create request- or component-owned state instead of exporting a request-specific singleton.
519
+
520
+ ```jsx
521
+ "use client";
522
+
523
+ import { useCallback, useState } from "react";
524
+ import { store } from "ustor";
525
+ import { useSignal, useStore } from "ustor/react";
526
+ // Import your shared adapter configuration once:
527
+ // import "@/lib/ustor-setup";
528
+
529
+ export default function Counter({ initialCount }) {
530
+ const [state] = useState(() => store({
531
+ count: initialCount,
532
+ items: [1, 2],
533
+ }));
534
+ const selectTotal = useCallback(
535
+ () => state.items.reduce((sum, value) => sum + value, 0),
536
+ [state],
537
+ );
538
+ const count = useSignal(state.$count);
539
+ const total = useStore(selectTotal);
540
+
541
+ return (
542
+ <button onClick={() => { state.count++; state.items.push(1); }}>
543
+ {count} / {total}
544
+ </button>
545
+ );
546
+ }
547
+ ```
224
548
 
225
- ```javascript
226
- import { signal } from "@preact/signals-core";
549
+ ```jsx
550
+ // app/page.jsx
551
+ import Counter from "./Counter";
227
552
 
228
- api.signal = signal;
229
- api.get = (v) => v?.value;
230
- api.set = (signal, v) => (signal.value = v);
231
- api.is = (v) => v?.peek;
553
+ export default function Page() {
554
+ return <Counter initialCount={1} />;
555
+ }
232
556
  ```
233
557
 
234
- To use any signal library
558
+ SSR requirements:
235
559
 
236
- ```javascript
237
- import ... from "...";
560
+ 1. Use `"use client"` for components that call the hooks.
561
+ 2. Pass the same serializable initial props during server rendering and hydration.
562
+ 3. Do not mutate the rendered state between its server snapshot and initial hydration.
563
+ 4. Do not store request-specific data in a module-level singleton shared by server requests.
564
+ 5. UStor does not provide a dehydration format. Serialize and restore initial data with your framework when needed.
565
+ 6. Configure `api.effect` for client hydration even though it is not used during server rendering.
566
+
567
+ The repository tests use `react-dom/server` and `hydrateRoot`, the same underlying React SSR primitives used by Next.js Client Components.
568
+
569
+ ## TypeScript
570
+
571
+ Declarations are included for both entry points; no separate `@types/ustor` package is needed.
238
572
 
239
- api.signal = ...;
240
- api.get = ...;
241
- api.set = (signal, value) => ...;
242
- api.is = (v) => ...;
573
+ ```typescript
574
+ import { store } from "ustor";
575
+ import { useStore } from "ustor/react";
243
576
 
244
577
  const state = store({
245
- count: 0
578
+ count: 1,
579
+ nested: { text: "hello" },
580
+ list: [1, 2],
581
+ get double() { return this.count * 2; },
246
582
  });
583
+
584
+ state.count = 2; // valid
585
+ // state.count = "invalid"; // TypeScript error
586
+
587
+ const doubled: number = state.double;
588
+ const text: string = state.nested.text;
589
+ const total = useStore(() => state.list.reduce((sum, value) => sum + value, 0));
590
+ ```
591
+
592
+ Exported type utilities:
593
+
594
+ - `SignalApi<Signal, Memo>` for custom adapters.
595
+ - `Store<T>` for reactive store inference.
596
+ - `SignalValue<Signal>` for React signal-value inference.
597
+
598
+ Run the repository's strict compile-time tests with:
599
+
600
+ ```bash
601
+ npm run typecheck
602
+ ```
603
+
604
+ ## Profiling and memory analysis
605
+
606
+ The profiler uses Preact Signals and measures:
607
+
608
+ - High-frequency memoized property updates.
609
+ - Batched property updates.
610
+ - Bounded array mutation workloads.
611
+ - Repeated store/effect creation, updates, and disposal with heap/RSS samples.
612
+
613
+ Run a short smoke profile:
614
+
615
+ ```bash
616
+ npm run profile:smoke
617
+ ```
618
+
619
+ Run the default 30-second memory profile:
620
+
621
+ ```bash
622
+ npm run profile
623
+ ```
624
+
625
+ Run a longer custom profile:
626
+
627
+ ```bash
628
+ npm run profile -- --updates=1000000 --array-updates=100000 --seconds=60 --sample-ms=1000
629
+ ```
630
+
631
+ Write machine-readable output:
632
+
633
+ ```bash
634
+ npm run --silent profile -- --json --seconds=30 > profile.json
635
+ ```
636
+
637
+ Profiling guidelines:
638
+
639
+ - Run with the same Node version and workload characteristics as production.
640
+ - Compare several samples after warm-up and forced GC, not one heap reading.
641
+ - A flat retained-heap trend is more useful than a single delta.
642
+ - RSS can grow early and plateau because of allocator behavior; RSS alone does not prove a leak.
643
+ - The CI smoke profile checks that the profiler remains runnable, not that every workload is leak-free.
644
+ - Repeat equivalent profiling with the adapter and application workload you actually deploy.
645
+
646
+ ## Production notes
647
+
648
+ - Use one adapter per UStor module instance and keep it for the lifetime of existing stores.
649
+ - Dispose adapter owners/scopes and UI subscriptions for short-lived stores.
650
+ - Test your actual browser, bundler, SSR, and hydration path.
651
+ - Stress-test application-specific update patterns and long-running memory usage.
652
+ - Use basic RxJS/Nanostores adapters only where their direct-getter semantics are sufficient.
653
+ - Keep runtime and development artifacts separate; `dist` and declarations are the only published implementation files.
654
+
655
+ ### Release checklist
656
+
657
+ The repository already checks unit and adapter behavior, TypeScript declarations, React, SSR/hydration, package formats, bundle size, coverage, and dependency audit status.
658
+
659
+ Before publishing a production release:
660
+
661
+ 1. **Choose a new version and write release notes.** The changes since the original `0.1.0` include observable behavior changes. Do not publish them as the same version; use an appropriate new version and document getter, sealed-object, array-proxy, and packaging behavior.
662
+ 2. **Test the packed artifact in a clean consumer application.** Run your real bundler and framework against the tarball rather than only against repository source.
663
+ 3. **Exercise your production SSR path.** The generic React SSR primitives are tested, but a full Next.js or other framework application should verify its own rendering, dehydration, and hydration flow.
664
+ 4. **Run target-browser and production-adapter checks.** Repeat performance and long-running memory profiling with the adapter and workload you deploy.
665
+ 5. **Review the release artifact.** Confirm `npm pack --dry-run`, `npm audit`, the package entry points, declaration files, license, and version before tagging or publishing.
666
+
667
+ ## Development
668
+
669
+ ```bash
670
+ npm ci
671
+ npm test # build, type tests, unit/adapter/React/SSR tests, package smoke tests
672
+ npm run typecheck # strict TypeScript declaration tests
673
+ npm run test:coverage # enforced 100% runtime coverage
674
+ npm run test:watch
675
+ npm run profile:smoke
676
+ npm run build
247
677
  ```
248
678
 
249
- The library provides a unified API to work with different reactive systems, allowing you to switch between Solid.js, Preact Signals, or any other UI framework easily.
679
+ The core size budget is enforced at 2 KiB minified and 1 KiB gzipped. `dist` is generated during build/prepack; declarations are published from `types/`.
250
680
 
251
- ### License
681
+ ## License
252
682
 
253
- This library is provided "as-is" under the MIT license. Feel free to use, modify, and distribute it in your projects.
683
+ [MIT](./LICENSE)
254
684
 
255
- ### Thanks and Inspiration
685
+ ## Thanks and inspiration
256
686
 
257
- - **[dy](https://github.com/dy)**
687
+ - [dy](https://github.com/dy)