@neurosell/reactivets 0.9.5 → 0.9.7
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 +635 -38
- package/browser/reactivets.global.js +1 -1
- package/browser/reactivets.global.js.map +1 -1
- package/dist/bench.cjs +136 -93
- package/dist/bench.cjs.map +1 -1
- package/dist/bench.js +2 -2
- package/dist/bench.js.map +1 -1
- package/dist/{chunk-DIVJ3EXH.js → chunk-7L26OYCK.js} +347 -95
- package/dist/chunk-7L26OYCK.js.map +1 -0
- package/dist/index.cjs +355 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +284 -23
- package/dist/index.d.ts +284 -23
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-DIVJ3EXH.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# ReactiveTS
|
|
2
|
+

|
|
3
|
+
|
|
2
4
|
A **reactive state engine for TypeScript** with fields, events, objects, arrays, computed values, effects, batching, path subscriptions, async support, cancellation, and built-in undo/redo history.
|
|
3
5
|
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
[Get Started]() | [Other Libraries](https://github.com/neurosell) | [
|
|
8
|
+
[Get Started](#installation) | [Other Libraries](https://github.com/neurosell) | [Telegram](https://t.me/devsdaddy) | [Contacts](https://neurosell.top/)
|
|
7
9
|
|
|
8
10
|
---
|
|
9
11
|
|
|
@@ -24,84 +26,679 @@ A **reactive state engine for TypeScript** with fields, events, objects, arrays,
|
|
|
24
26
|
* **Async listeners** with cancellation;
|
|
25
27
|
* **Adapters:** ``toPromise``, ``fromEvent``, ``fromObservable``;
|
|
26
28
|
* **WeakMap proxy cache** for stable nested references;
|
|
29
|
+
* **Transaction middleware and profiler**;
|
|
30
|
+
* **Snapshot API** for capture/restore;
|
|
31
|
+
* **Sync helpers** for one-way and two-way field synchronization;
|
|
32
|
+
* **Lens and Atom** primitives;
|
|
33
|
+
* **Inspect Dependencies** API for computed values;
|
|
34
|
+
* **Worker bridge** and **DevTools event bus**;
|
|
27
35
|
|
|
28
36
|
## Table of Contents
|
|
29
37
|
* [Installation](#installation)
|
|
30
|
-
* [Core Concepts]()
|
|
31
|
-
* [ReactiveField]()
|
|
32
|
-
* [ReactiveEvent]()
|
|
33
|
-
* [ReactiveObject]()
|
|
34
|
-
* [ReactiveArray]()
|
|
35
|
-
* [Computed]()
|
|
36
|
-
* [Selectors]()
|
|
37
|
-
* [Effect]()
|
|
38
|
-
* [Batching]()
|
|
39
|
-
* [History & Transactions]()
|
|
40
|
-
* [Path Subscriptions]()
|
|
41
|
-
* [Async & Cancellation]()
|
|
42
|
-
* [Views (``useFiltered``, ``useMapped``, ``useSorted``)]()
|
|
43
|
-
* [Adapters (``toPromise``, ``fromEvent``, ``fromObservable``)]()
|
|
44
|
-
* [
|
|
45
|
-
* [
|
|
46
|
-
* [
|
|
47
|
-
* [
|
|
38
|
+
* [Core Concepts](#core-concepts)
|
|
39
|
+
* [ReactiveField](#reactive-fields)
|
|
40
|
+
* [ReactiveEvent](#reactive-events)
|
|
41
|
+
* [ReactiveObject](#reactive-objects)
|
|
42
|
+
* [ReactiveArray](#reactive-arrays)
|
|
43
|
+
* [Computed](#computed)
|
|
44
|
+
* [Selectors](#selectors)
|
|
45
|
+
* [Effect](#effects)
|
|
46
|
+
* [Batching](#batching)
|
|
47
|
+
* [History & Transactions](#history-and-transactions)
|
|
48
|
+
* [Path Subscriptions](#path-subscriptions)
|
|
49
|
+
* [Async & Cancellation](#async-and-cancellation)
|
|
50
|
+
* [Views (``useFiltered``, ``useMapped``, ``useSorted``)](#views-filtering-mapping-sorting)
|
|
51
|
+
* [Adapters (``toPromise``, ``fromEvent``, ``fromObservable``)](#adapters-and-converters)
|
|
52
|
+
* [Transaction Middleware & Profiler](#transaction-middleware-and-profiler)
|
|
53
|
+
* [Snapshot API](#snapshot-api)
|
|
54
|
+
* [Sync API](#sync-api)
|
|
55
|
+
* [Lens and Atom](#lens-and-atom)
|
|
56
|
+
* [Inspect Dependencies](#inspect-dependencies)
|
|
57
|
+
* [Worker Bridge](#worker-bridge)
|
|
58
|
+
* [DevTools](#devtools)
|
|
59
|
+
* [Reactive Watcher (auto-unsubscribe)](#reactive-watcher)
|
|
60
|
+
* [Performance Notes](#performance-notes-and-benchmark)
|
|
61
|
+
* [Comparison Philosophy](#comparison-philosophy)
|
|
62
|
+
* [License](#license)
|
|
48
63
|
|
|
49
64
|
### Installation
|
|
50
|
-
|
|
65
|
+
To install the library, you can use NPM:
|
|
51
66
|
```bash
|
|
52
67
|
npm install @neurosell/reactivets
|
|
53
68
|
```
|
|
54
69
|
|
|
70
|
+
**Or from CDN:**
|
|
71
|
+
```html
|
|
72
|
+
<script src="https://cdn.jsdelivr.net/npm/@neurosell/reactivets@0.9.5/browser/reactivets.global.js"></script>
|
|
73
|
+
<script type="text/javascript">
|
|
74
|
+
// Will be connected as Global
|
|
75
|
+
const { ReactiveField } = window.ReactiveTS;
|
|
76
|
+
</script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Manual GitHub Installation for developers:**
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/Neurosell/ReactiveTS.git
|
|
82
|
+
cd ./ReactiveTS/
|
|
83
|
+
npm install
|
|
84
|
+
npm run build
|
|
85
|
+
```
|
|
86
|
+
|
|
55
87
|
### Core Concepts
|
|
56
|
-
|
|
88
|
+
**ReactiveTS** Library is built around:
|
|
89
|
+
* **State-first** reactivity;
|
|
90
|
+
* **Automatic dependency** tracking;
|
|
91
|
+
* **Microtask** batching;
|
|
92
|
+
* **Deterministic** undo/redo with **transactions** support;
|
|
93
|
+
* **Minimal** boilerplate;
|
|
94
|
+
|
|
95
|
+
**You work with state naturally:**
|
|
96
|
+
```javascript
|
|
97
|
+
state.value.user.name = "Elijah";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
And everything reacts. Simple.
|
|
57
101
|
|
|
58
102
|
### Reactive Fields
|
|
59
|
-
|
|
103
|
+
**ReactiveField** is a reactive primitive (similar to a signal).
|
|
104
|
+
> By default, **ReactiveTS** coalesces (merges) reactions into a single microtask.
|
|
105
|
+
> The restart of effects and reactive fields is scheduled once and will be executed at the end of the tick, so it only sees the last value. This is due to the batching system for optimization, so you should take this into account in your work.
|
|
106
|
+
|
|
107
|
+
**Basic Usage:**
|
|
108
|
+
```javascript
|
|
109
|
+
// Import
|
|
110
|
+
import { ReactiveField } from "@neurosell/reactivets";
|
|
111
|
+
|
|
112
|
+
// Create Reactive Field
|
|
113
|
+
const count = new ReactiveField(0);
|
|
114
|
+
count.addListener((v) => {
|
|
115
|
+
console.log("count:", v);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
count.value = 1;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Batching and Unsubscribe:**
|
|
122
|
+
```javascript
|
|
123
|
+
// Let's Create our Reactive Field
|
|
124
|
+
const count = new ReactiveField(0);
|
|
125
|
+
|
|
126
|
+
// Listener Returns Unsubscribe Method
|
|
127
|
+
const unsub = count.addListener((v) => {
|
|
128
|
+
console.log("count:", v);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
count.value = 1;
|
|
132
|
+
await new Promise(resolve => {}) // If you don't wait before unsubscribe in single tick - batching does't run reactive listener
|
|
133
|
+
unsub();
|
|
134
|
+
```
|
|
60
135
|
|
|
61
136
|
### Reactive Events
|
|
62
|
-
|
|
137
|
+
**Reactive events** are generally similar in concept to reactive fields, but typically do not contain a current value (such as fields or objects), except when you use history.
|
|
138
|
+
|
|
139
|
+
**Let's look at basic usage:**
|
|
140
|
+
```javascript
|
|
141
|
+
// Import Events Class
|
|
142
|
+
import { ReactiveEvent } from "@neurosell/reactivets";
|
|
143
|
+
|
|
144
|
+
// Create Event
|
|
145
|
+
const event = new ReactiveEvent<string>();
|
|
146
|
+
|
|
147
|
+
// Add Listener
|
|
148
|
+
event.addListener((msg, ctx) => {
|
|
149
|
+
console.log(msg);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Invoke Event
|
|
153
|
+
event.invoke("hello");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**You can also use async event listeners:**
|
|
157
|
+
```javascript
|
|
158
|
+
event.addListener(async (msg, ctx) => {
|
|
159
|
+
await new Promise(r => setTimeout(r, 100));
|
|
160
|
+
if (ctx.signal.aborted) return;
|
|
161
|
+
console.log(msg);
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Reactive Events Supports:**
|
|
166
|
+
* **batched** listeners;
|
|
167
|
+
* **AbortSignal** cancellation;
|
|
168
|
+
* ``invokeAsync()`` for async events;
|
|
63
169
|
|
|
64
170
|
### Reactive Objects
|
|
65
|
-
|
|
171
|
+
**Reactive objects** are similar to fields, but they can contain any objects. This is useful when you need to track changes, for example, in user data. Reactive objects work through **Proxy**, can also use **Patch Tracking**, and support **change history** (stream).
|
|
172
|
+
|
|
173
|
+
**Let's look at basic usage:**
|
|
174
|
+
```javascript
|
|
175
|
+
import { ReactiveObject } from "@neurosell/reactivets";
|
|
176
|
+
|
|
177
|
+
// Create our Object
|
|
178
|
+
const state = new ReactiveObject({
|
|
179
|
+
user: { name: "Ada" },
|
|
180
|
+
count: 0
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Add Listener
|
|
184
|
+
state.addListener((patch) => {
|
|
185
|
+
console.log("patch:", patch);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Let's change object
|
|
189
|
+
state.value.count++;
|
|
190
|
+
state.value.user.name = "Grace";
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Listener contains patch for our changes. For example:**
|
|
194
|
+
```javascript
|
|
195
|
+
{
|
|
196
|
+
patch: {
|
|
197
|
+
op: "set", // Operation
|
|
198
|
+
path: ['count'], // Object Path
|
|
199
|
+
prev: 0, // Preview Value
|
|
200
|
+
next: 1 // Next Value
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
66
204
|
|
|
67
205
|
### Reactive Arrays
|
|
68
|
-
|
|
206
|
+
Reactive arrays work in a similar way to objects, but additional filters and other functions can be applied to them (which we will discuss later).
|
|
207
|
+
|
|
208
|
+
**Basic Usage with Patch Tracking:**
|
|
209
|
+
```javascript
|
|
210
|
+
import { ReactiveArray } from "@neurosell/reactivets";
|
|
211
|
+
|
|
212
|
+
// Our Array
|
|
213
|
+
const list = new ReactiveArray<number>([1, 2]);
|
|
214
|
+
|
|
215
|
+
// Similar Add Listener
|
|
216
|
+
list.addListener((patch) => {
|
|
217
|
+
console.log("array patch:", patch);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// And Try to Change Array
|
|
221
|
+
list.value.push(3);
|
|
222
|
+
list.value.splice(0, 1);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Patch Example:**
|
|
226
|
+
```javascript
|
|
227
|
+
{
|
|
228
|
+
op: "splice", path: [], index: 0, deleteCount: 1, items: [3], removed: [1]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
69
231
|
|
|
70
232
|
### Computed
|
|
71
|
-
|
|
233
|
+
> Computed functions are needed to automatically track dependencies for calculations and recalculate the final value if one of the dependencies changes. An example of the logic behind such calculations can be found in linked cells in Excel — when you change one of the two, the sum changes.
|
|
234
|
+
|
|
235
|
+
**Computed are:**
|
|
236
|
+
* **tracks dependencies** automatically;
|
|
237
|
+
* **recomputes final value** when dependencies change;
|
|
238
|
+
* **batched**;
|
|
239
|
+
* **supports lazy mode**;
|
|
240
|
+
* supports **custom equality**;
|
|
241
|
+
|
|
242
|
+
**Let's look at simple example:**
|
|
243
|
+
```javascript
|
|
244
|
+
import { ReactiveField, useComputed } from "@neurosell/reactivets";
|
|
245
|
+
|
|
246
|
+
// Let's create two Reactive Fields
|
|
247
|
+
const a = new ReactiveField(2);
|
|
248
|
+
const b = new ReactiveField(3);
|
|
249
|
+
|
|
250
|
+
// Create Computed Function
|
|
251
|
+
const sum = useComputed(() => a.value + b.value);
|
|
252
|
+
|
|
253
|
+
// Add Listener for Sum
|
|
254
|
+
sum.addListener((v) => console.log("sum:", v));
|
|
255
|
+
|
|
256
|
+
// Now let's change A
|
|
257
|
+
a.value = 10;
|
|
258
|
+
|
|
259
|
+
// And after 100ms change B, computed listener printed new value
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
261
|
+
a.value = 5;
|
|
262
|
+
```
|
|
72
263
|
|
|
73
264
|
### Selectors
|
|
74
|
-
|
|
265
|
+
**Selectors** are needed to respond to changes in only certain object fields without unnecessarily triggering listeners.
|
|
266
|
+
|
|
267
|
+
**Simple selector example:**
|
|
268
|
+
```javascript
|
|
269
|
+
import { ReactiveField, useSelect } from "@neurosell/reactivets";
|
|
270
|
+
|
|
271
|
+
// Create our user
|
|
272
|
+
const user = new ReactiveField({ id: 1, name: "Ada" });
|
|
273
|
+
|
|
274
|
+
// Select only name
|
|
275
|
+
const name = useSelect(user, u => u.name);
|
|
276
|
+
|
|
277
|
+
// Add Listener for name chages
|
|
278
|
+
name.addListener(n => console.log(n));
|
|
279
|
+
|
|
280
|
+
// Update Value
|
|
281
|
+
user.value = { ...user.value, name: "Grace" };
|
|
282
|
+
|
|
283
|
+
// Try to change ID after 100ms, Name listener not called after this action :)
|
|
284
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
285
|
+
user.value.id = 2;
|
|
286
|
+
```
|
|
75
287
|
|
|
76
288
|
### Effects
|
|
77
|
-
|
|
289
|
+
Side effects with automatic dependency tracking and cleanup.
|
|
290
|
+
|
|
291
|
+
**Use Case:**
|
|
292
|
+
```javascript
|
|
293
|
+
import { ReactiveField, useEffect } from "@neurosell/reactivets";
|
|
294
|
+
|
|
295
|
+
// Create our Reactive Field
|
|
296
|
+
const count = new ReactiveField(0);
|
|
297
|
+
|
|
298
|
+
const stop = useEffect(() => {
|
|
299
|
+
console.log("count is", count.value);
|
|
300
|
+
|
|
301
|
+
const timer = setInterval(() => {}, 1000);
|
|
302
|
+
return () => clearInterval(timer);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Let's Change Value
|
|
306
|
+
count.value = 1;
|
|
307
|
+
|
|
308
|
+
// Wait 100ms and stop our effector
|
|
309
|
+
// The next value changes can't be called in useEffect listener
|
|
310
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
311
|
+
stop();
|
|
312
|
+
count.value = 2;
|
|
313
|
+
```
|
|
78
314
|
|
|
79
315
|
### Batching
|
|
80
|
-
|
|
316
|
+
Batch multiple mutations into one reactive wave. By defaults all mutations will be batched in first generation.
|
|
317
|
+
|
|
318
|
+
**Use Case:**
|
|
319
|
+
```javascript
|
|
320
|
+
import { ReactiveField, useBatch } from "@neurosell/reactivets";
|
|
321
|
+
|
|
322
|
+
// Let's create our field
|
|
323
|
+
const f = new ReactiveField(0);
|
|
324
|
+
f.addListener(v => console.log(v));
|
|
325
|
+
|
|
326
|
+
// Batch our calculation
|
|
327
|
+
useBatch(() => {
|
|
328
|
+
f.value = 1;
|
|
329
|
+
f.value = 2;
|
|
330
|
+
f.value = 3;
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
> Only one notification wave runs with ``useBatch`` helper.
|
|
81
335
|
|
|
82
336
|
### History and Transactions
|
|
83
|
-
|
|
337
|
+
**ReactiveTS** supports powerful built-in undo/redo system with transactions support.
|
|
338
|
+
|
|
339
|
+
**Simple use case:**
|
|
340
|
+
```javascript
|
|
341
|
+
import { ReactiveField, ReactiveHistoryStack } from "@neurosell/reactivets";
|
|
342
|
+
|
|
343
|
+
// Create our history stack
|
|
344
|
+
const history = new ReactiveHistoryStack();
|
|
345
|
+
|
|
346
|
+
// Create Reactive Field with History Stack
|
|
347
|
+
const count = new ReactiveField(0, { history });
|
|
348
|
+
|
|
349
|
+
// Fill our history
|
|
350
|
+
count.value = 1;
|
|
351
|
+
count.value = 2;
|
|
352
|
+
|
|
353
|
+
// Work with history
|
|
354
|
+
history.undo();
|
|
355
|
+
console.log(count.value); // 1
|
|
356
|
+
history.undo();
|
|
357
|
+
console.log(count.value); // 0
|
|
358
|
+
history.redo();
|
|
359
|
+
console.log(count.value); // 1
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
You can also group multiple changes into one undo step with transactions.
|
|
363
|
+
|
|
364
|
+
**Transaction Example:**
|
|
365
|
+
```javascript
|
|
366
|
+
import { useReactiveTransaction } from "@neurosell/reactivets";
|
|
367
|
+
|
|
368
|
+
console.log(count.value); // 1
|
|
369
|
+
|
|
370
|
+
// Will be applied as single step
|
|
371
|
+
useReactiveTransaction(history, () => {
|
|
372
|
+
count.value = 10;
|
|
373
|
+
count.value = 20;
|
|
374
|
+
count.value = 30;
|
|
375
|
+
});
|
|
376
|
+
console.log(count.value); // 30
|
|
377
|
+
|
|
378
|
+
// Back to history
|
|
379
|
+
history.undo();
|
|
380
|
+
console.log(count.value); // 1
|
|
381
|
+
```
|
|
84
382
|
|
|
85
383
|
### Path Subscriptions
|
|
86
|
-
|
|
384
|
+
With **ReactiveTS** you can listen to specific paths of objects.
|
|
385
|
+
|
|
386
|
+
**Usage sample:**
|
|
387
|
+
```javascript
|
|
388
|
+
// Create our Reactive Object
|
|
389
|
+
const state = new ReactiveObject({
|
|
390
|
+
user: {
|
|
391
|
+
name: "Igor",
|
|
392
|
+
age: 15
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// This Listener reacts only at user.name changes
|
|
397
|
+
state.addPathListener("user.name", (patch) => {
|
|
398
|
+
console.log("name changed");
|
|
399
|
+
}, { mode: "exact" });
|
|
400
|
+
|
|
401
|
+
// This Listener reacts at all user changes
|
|
402
|
+
state.addPathListener("user", (patch) => {
|
|
403
|
+
console.log("anything under user changed");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Change our object
|
|
407
|
+
state.value.user.name = "Elijah"; // Calls both listeners
|
|
408
|
+
state.value.user.age = 10; // Calls only second listener
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Path Subscription supports:**
|
|
412
|
+
* exact mode (``item.data.key``);
|
|
413
|
+
* prefix mode (``item``);
|
|
414
|
+
* wildcard mode (``items.*.id``)
|
|
87
415
|
|
|
88
416
|
### Async and Cancellation
|
|
89
|
-
|
|
417
|
+
You can use cancellation tokens and async listeners for your reactive fields.
|
|
418
|
+
|
|
419
|
+
**For Example:**
|
|
420
|
+
```javascript
|
|
421
|
+
const field = new ReactiveField(0);
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
|
|
424
|
+
field.addListener(async (v, ctx) => {
|
|
425
|
+
await someAsyncTask();
|
|
426
|
+
if (ctx.signal.aborted) return;
|
|
427
|
+
}, { signal: controller.signal });
|
|
428
|
+
|
|
429
|
+
controller.abort();
|
|
430
|
+
```
|
|
90
431
|
|
|
91
432
|
### Views (Filtering, Mapping, Sorting)
|
|
92
|
-
|
|
433
|
+
To simplify working with **Reactive Arrays**, you can also use auxiliary functionality for filtering, mapping, and sorting data.
|
|
434
|
+
|
|
435
|
+
**Usage Example:**
|
|
436
|
+
```javascript
|
|
437
|
+
import { ReactiveArray, useFiltered, useMapped, useSorted } from "@neurosell/reactivets";
|
|
438
|
+
|
|
439
|
+
// Create our Array
|
|
440
|
+
const list = new ReactiveArray([1, 2, 3, 4]);
|
|
441
|
+
|
|
442
|
+
// Filtered Array
|
|
443
|
+
const evens = useFiltered(list, x => x % 2 === 0);
|
|
444
|
+
evens.addListener(arr => console.log(arr));
|
|
445
|
+
|
|
446
|
+
// Push new value
|
|
447
|
+
list.value.push(6);
|
|
448
|
+
```
|
|
93
449
|
|
|
94
450
|
### Adapters and Converters
|
|
95
|
-
|
|
451
|
+
**Adapters** are **helper functions** for converting reactive events, fields, and other elements into **asynchronous methods**, **Observables**, etc.
|
|
452
|
+
|
|
453
|
+
**Conversion to Promise:**
|
|
454
|
+
```javascript
|
|
455
|
+
import { toPromise } from "@neurosell/reactivets";
|
|
456
|
+
|
|
457
|
+
toPromise(event, {
|
|
458
|
+
predicate: v => v > 10
|
|
459
|
+
}).then(v => console.log(v));
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Conversion to Promise Field:**
|
|
463
|
+
```javascript
|
|
464
|
+
import { toPromiseField } from "@neurosell/reactivets";
|
|
465
|
+
|
|
466
|
+
toPromiseField(field, {
|
|
467
|
+
predicate: v => v === 5
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Conversion from DOM Event:**
|
|
472
|
+
```javascript
|
|
473
|
+
import { fromEvent } from "@neurosell/reactivets";
|
|
474
|
+
|
|
475
|
+
const { event, dispose } = fromEvent(document, "click");
|
|
476
|
+
event.addListener(e => console.log(e));
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Conversion from Observable:**
|
|
480
|
+
```javascript
|
|
481
|
+
import { fromObservable } from "@neurosell/reactivets";
|
|
482
|
+
|
|
483
|
+
// Observable Example
|
|
484
|
+
const obs = {
|
|
485
|
+
subscribe(next) {
|
|
486
|
+
const t = setInterval(() => next(Date.now()), 1000);
|
|
487
|
+
return () => clearInterval(t);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const { event } = fromObservable(obs);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Transaction Middleware and Profiler
|
|
495
|
+
Use middleware function around transactions and collect profiling data.
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
import {
|
|
499
|
+
ReactiveHistoryStack,
|
|
500
|
+
ReactiveTransactionManager,
|
|
501
|
+
createTransactionProfiler,
|
|
502
|
+
ReactiveField
|
|
503
|
+
} from "@neurosell/reactivets";
|
|
504
|
+
|
|
505
|
+
const history = new ReactiveHistoryStack();
|
|
506
|
+
const tx = new ReactiveTransactionManager(history);
|
|
507
|
+
const timings = [];
|
|
508
|
+
|
|
509
|
+
tx.use(createTransactionProfiler(timings));
|
|
510
|
+
|
|
511
|
+
const count = new ReactiveField(0, { history });
|
|
512
|
+
|
|
513
|
+
tx.run(() => {
|
|
514
|
+
count.value = 1;
|
|
515
|
+
count.value = 2;
|
|
516
|
+
}, "update-count");
|
|
517
|
+
|
|
518
|
+
console.log(timings[0]?.durationMs);
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Snapshot API
|
|
522
|
+
Take a snapshot and restore it later.
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
import { ReactiveObject, createSnapshot, restoreSnapshot } from "@neurosell/reactivets";
|
|
526
|
+
|
|
527
|
+
const state = new ReactiveObject({ user: { name: "Ada" }, count: 1 });
|
|
528
|
+
const snap = createSnapshot(state);
|
|
529
|
+
|
|
530
|
+
state.value.user.name = "Grace";
|
|
531
|
+
state.value.count = 10;
|
|
532
|
+
|
|
533
|
+
restoreSnapshot(state, snap);
|
|
534
|
+
console.log(state.value.user.name); // Ada
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Sync API
|
|
538
|
+
Synchronize two reactive fields.
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
import { ReactiveField, useSync } from "@neurosell/reactivets";
|
|
542
|
+
|
|
543
|
+
const left = new ReactiveField("A");
|
|
544
|
+
const right = new ReactiveField("B");
|
|
545
|
+
|
|
546
|
+
const stop = useSync(left, right);
|
|
547
|
+
left.value = "Hello";
|
|
548
|
+
console.log(right.value); // Hello
|
|
549
|
+
|
|
550
|
+
stop();
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Lens and Atom features
|
|
554
|
+
`ReactiveAtom` is a thin alias over `ReactiveField`; `useLens` focuses into nested state.
|
|
555
|
+
|
|
556
|
+
```javascript
|
|
557
|
+
import { ReactiveObject, useLens, useAtom } from "@neurosell/reactivets";
|
|
558
|
+
|
|
559
|
+
const state = new ReactiveObject({ profile: { name: "Ada" } });
|
|
560
|
+
const nameLens = useLens(state, ["profile", "name"]);
|
|
561
|
+
const localFlag = useAtom(false);
|
|
562
|
+
|
|
563
|
+
nameLens.value = "Grace";
|
|
564
|
+
console.log(state.value.profile.name); // Grace
|
|
565
|
+
|
|
566
|
+
localFlag.value = true;
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Inspect Dependencies
|
|
570
|
+
Inspect collected dependencies for computed values (useful for debugging).
|
|
571
|
+
|
|
572
|
+
```javascript
|
|
573
|
+
import { ReactiveField, useComputed } from "@neurosell/reactivets";
|
|
574
|
+
|
|
575
|
+
const a = new ReactiveField(1);
|
|
576
|
+
const b = new ReactiveField(2);
|
|
577
|
+
const sum = useComputed(() => a.value + b.value);
|
|
578
|
+
|
|
579
|
+
console.log(sum.inspectDependencies().length); // 2
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Worker Bridge
|
|
583
|
+
Bridge browser `Worker` messages with ReactiveTS events.
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
import { createWorkerBridge } from "@neurosell/reactivets";
|
|
587
|
+
|
|
588
|
+
const worker = new Worker("./worker.js", { type: "module" });
|
|
589
|
+
const bridge = createWorkerBridge(worker);
|
|
590
|
+
|
|
591
|
+
bridge.onMessage.addListener((message) => {
|
|
592
|
+
console.log(message.type, message.payload);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
bridge.post({ type: "PING", payload: { at: Date.now() } });
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### DevTools
|
|
599
|
+
Use a minimal built-in event bus for state/debug records.
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
import { ReactiveDevTools } from "@neurosell/reactivets";
|
|
603
|
+
|
|
604
|
+
const devtools = new ReactiveDevTools();
|
|
605
|
+
devtools.addListener((record) => console.log(record.type, record.payload));
|
|
606
|
+
|
|
607
|
+
devtools.emit("state:update", { feature: "counter", next: 10 });
|
|
608
|
+
console.log(devtools.inspect().length); // 1
|
|
609
|
+
```
|
|
96
610
|
|
|
97
611
|
### Reactive Watcher
|
|
98
|
-
|
|
612
|
+
**Reactive Watcher** in **ReactiveTS** needed to track dependent listeners and further automatically unsubscribe all listeners from specific reactive fields, events, objects, and arrays.
|
|
613
|
+
|
|
614
|
+
**Use Case:**
|
|
615
|
+
```javascript
|
|
616
|
+
import { ReactiveWatcher } from "@neurosell/reactivets";
|
|
617
|
+
|
|
618
|
+
// Create Watcher
|
|
619
|
+
const watcher = new ReactiveWatcher();
|
|
620
|
+
watcher.own(field.addListener(console.log));
|
|
621
|
+
watcher.dispose(); // removes all listeners
|
|
622
|
+
```
|
|
99
623
|
|
|
100
624
|
### Performance Notes and Benchmark
|
|
101
|
-
|
|
625
|
+
Now let's talk about **ReactiveTS** **performance and optimization** under the hood, and take a look at the **benchmarks**.
|
|
626
|
+
|
|
627
|
+
**ReactiveTS uses:**
|
|
628
|
+
* **Microtask batching**;
|
|
629
|
+
* **WeakMap proxy caching**;
|
|
630
|
+
* **Deduplicated scheduler** queue;
|
|
631
|
+
* **Version-based dependency tracking**;
|
|
632
|
+
|
|
633
|
+
**For extreme hot paths:**
|
|
634
|
+
1. Prefer **ReactiveField** over deep Proxy objects;
|
|
635
|
+
2. Use **batching**;
|
|
636
|
+
3. Use **transactions** for grouped updates and history optimisation;
|
|
637
|
+
|
|
638
|
+
#### Benchmarks
|
|
639
|
+
**ReactiveTS** is optimized for typical UI/state scenarios (frequent changes to small fields + batching + effects). To fairly compare performance between versions/configurations, use reproducible microbenchmarks.
|
|
640
|
+
|
|
641
|
+
**The benchmarks below include the following scenarios:**
|
|
642
|
+
* **ReactiveField:** speed of ``set`` and listener notifications;
|
|
643
|
+
* **Computed:** recalculation of derived value chains;
|
|
644
|
+
* **Effect:** restarting effects when changes occur;
|
|
645
|
+
* **ReactiveObject / ReactiveArray (Proxy):** cost of ``set``/``splice`` and patch generation;
|
|
646
|
+
* **Path subscriptions:** filtering patches by path/mask;
|
|
647
|
+
* **Batching & Transactions:** how well the wave of updates coalesces;
|
|
648
|
+
* **History undo/redo:** cost of recording/rolling back changes;
|
|
649
|
+
|
|
650
|
+
> **Important:** Proxies and patches are inevitably more expensive than simple signals. For hot paths, use ``ReactiveField`` and computed/selectors.
|
|
651
|
+
|
|
652
|
+
#### Benchmark Results (NodeJS VPS 1vCPU, 4GB Ram), 200K Iterations
|
|
653
|
+
| Scenario (200K Iterations) | ops/s | Notes |
|
|
654
|
+
|-----------------------------|-------------:|-----------------------|
|
|
655
|
+
| Field.set (no listeners) | 9,1M (21ms) | baseline |
|
|
656
|
+
| Field.set (10 listeners) | 768K (260ms) | fan-out |
|
|
657
|
+
| Computed chain (3 nodes) | 79K (2500ms) | dep tracking cost |
|
|
658
|
+
| ReactiveArray push | 4,4m (22ms) | reactive array push |
|
|
659
|
+
| ReactiveObject set (deep) | 1,1m (176ms) | Proxy + patch |
|
|
660
|
+
| Batch(100 sets) => 1 wave | 58K (859ms) | coalescing |
|
|
661
|
+
| Transaction(100 sets)+undo | 114K (174ms) | grouped history |
|
|
662
|
+
| Event.invoke (10 listeners) | 864K (231ms) | reactive event invoke |
|
|
102
663
|
|
|
103
664
|
### Comparison Philosophy
|
|
104
|
-
|
|
665
|
+
In this section, we have provided you with the main comparisons with other popular reactive extension libraries.
|
|
666
|
+
|
|
667
|
+
**ReactiveTS focuses on:**
|
|
668
|
+
1. Reactive state management;
|
|
669
|
+
2. Deterministic undo/redo and transactions;
|
|
670
|
+
3. Path-level reactivity and simple API;
|
|
671
|
+
4. TypeScript-first API;
|
|
672
|
+
|
|
673
|
+
> It is not a stream algebra engine like RxJS. It is your simple reactive state management engine!
|
|
674
|
+
|
|
675
|
+
#### ReactiveTS vs RxJS
|
|
676
|
+
| Feature | ReactiveTS | RxJS |
|
|
677
|
+
|-------------------------------------------------|---------------|--------------------------|
|
|
678
|
+
| ReactiveField | ✅ | ⚠️ using BehaviorSubject |
|
|
679
|
+
| ReactiveObject (Proxy) | ✅ | ❌ |
|
|
680
|
+
| Path subscriptions | ✅ | ❌ |
|
|
681
|
+
| Computed (auto deps) | ✅ | ⚠️ using combineLatest |
|
|
682
|
+
| useEffect-подобное | ✅ | ⚠️ subscribe |
|
|
683
|
+
| Undo/Redo history | ✅ | ❌ |
|
|
684
|
+
| Transaction history | ✅ | ❌ |
|
|
685
|
+
| Stream combinators (switchMap, retry, debounce) | ⚠️ partial | ✅ powerful |
|
|
686
|
+
| Cancellation | ✅ AbortSignal | ✅ |
|
|
687
|
+
| Async operators | ⚠️ basic | ✅ large ecosystem |
|
|
688
|
+
|
|
689
|
+
#### ReactiveTS vs MobX
|
|
690
|
+
| Feature | ReactiveTS | MobX |
|
|
691
|
+
|---------------------|-----------------|---------------------------------|
|
|
692
|
+
| Proxy-based | ✅ | ❌ (only using getters/observables) |
|
|
693
|
+
| Dependency tracking | ✅ | ✅ |
|
|
694
|
+
| History | ✅ | ❌ |
|
|
695
|
+
| Transaction | ✅ | ⚠️ runInAction |
|
|
696
|
+
| Devtools ecosystem | ⚠️ in development | ✅ |
|
|
697
|
+
| Battle-tested | ✅ | ✅ |
|
|
105
698
|
|
|
106
699
|
### License
|
|
107
|
-
|
|
700
|
+
Our library is distributed under the **MIT license**. You can use it however you like. We would appreciate any feedback and suggestions for improvement.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
[Get Started](#installation) | [Other Libraries](https://github.com/neurosell) | [Telegram](https://t.me/devsdaddy) | [Contacts](https://neurosell.top/)
|