@ivlev-dev/silent-reactivity 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +228 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @ivlev-dev/silent-reactivity
2
+
3
+ Гранулярный граф сигналов с глубокими прокси для объектов и массивов — ядро реактивности Silent Framework.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@ivlev-dev/silent-reactivity)](https://www.npmjs.com/package/@ivlev-dev/silent-reactivity)
6
+ [![license](https://img.shields.io/npm/l/@ivlev-dev/silent-reactivity)](https://github.com/ivlev-dev/silent-framework/blob/main/LICENSE)
7
+ [![node](https://img.shields.io/node/v/@ivlev-dev/silent-reactivity)](https://www.npmjs.com/package/@ivlev-dev/silent-reactivity)
8
+
9
+ ## Возможности
10
+
11
+ - Сигналы, мемо, эффекты с точным отслеживанием зависимостей без виртуального DOM.
12
+ - Мемо ленивые: не вычисляются до первого чтения; наблюдатели уведомляются только при реальном изменении значения (с учётом `equals`).
13
+ - `batch` объединяет несколько записей в один прогон, `flushSync` — принудительный синхронный flush.
14
+ - Глубокая реактивность: отдельный сигнал на каждое свойство, индекс и `length` массива; мутирующие методы (`push`, `splice`, `sort`, `reverse` и др.) уведомляют только затронутые индексы.
15
+ - Владельцы (`createRoot`, `getOwner`, `runWithOwner`) и `onCleanup` для автоматической очистки эффектов и подписок.
16
+ - Store-примитивы для привязки реактивности к экземплярам классов: `readField`, `writeField`, `getComputed`, `copyReactiveState`.
17
+
18
+ ## Установка
19
+
20
+ ```bash
21
+ pnpm add @ivlev-dev/silent-reactivity
22
+ ```
23
+
24
+ ```bash
25
+ npm install @ivlev-dev/silent-reactivity
26
+ ```
27
+
28
+ ```bash
29
+ yarn add @ivlev-dev/silent-reactivity
30
+ ```
31
+
32
+ Пакет распространяется только как ESM (`type: "module"`) и не имеет рантайм-зависимостей.
33
+
34
+ ## Быстрый старт
35
+
36
+ ```ts
37
+ import { createEffect, createRoot, createSignal, onCleanup } from "@ivlev-dev/silent-reactivity";
38
+
39
+ createRoot((dispose) => {
40
+ const [count, setCount] = createSignal(0);
41
+
42
+ createEffect(() => {
43
+ console.log("count:", count());
44
+ });
45
+
46
+ onCleanup(() => console.log("cleanup"));
47
+
48
+ setCount(1);
49
+ setCount(2); // эффект увидит только итоговое значение — flush в микротаске
50
+
51
+ dispose(); // вызовет onCleanup и уничтожит эффекты поддерева
52
+ });
53
+ ```
54
+
55
+ `Setter` принимает значение или функцию-апдейтер: `setCount((prev) => prev + 1)`. Апдейтер читает предыдущее значение через `peek()` и не регистрирует зависимость.
56
+
57
+ ## Мемо и батчинг
58
+
59
+ ```ts
60
+ import { batch, createMemo, createSignal, untrack } from "@ivlev-dev/silent-reactivity";
61
+
62
+ const [count, setCount] = createSignal(2);
63
+ const [factor, setFactor] = createSignal(3);
64
+
65
+ const product = createMemo(() => count() * factor());
66
+
67
+ product(); // лениво: вычисляется при первом чтении
68
+
69
+ product() === 6;
70
+
71
+ batch(() => {
72
+ setCount(10);
73
+ setFactor(4);
74
+ setFactor(5);
75
+ });
76
+ // один прогон: промежуточные значения factor не видны эффектам
77
+
78
+ untrack(() => {
79
+ // чтение count() внутри untrack не регистрирует зависимость
80
+ console.log(count());
81
+ });
82
+ ```
83
+
84
+ Можно задать свою функцию равенства: `createMemo(() => fn(), { equals: (a, b) => a.id === b.id })`.
85
+
86
+ ## Глубокие прокси и массивы
87
+
88
+ `deepReactive` лениво оборачивает plain-объекты и массивы в прокси с сигналом на каждое свойство. Изменение `state.user.name` не трогает эффекты, читающие `state.user.address.city`.
89
+
90
+ ```ts
91
+ import { createEffect, deepReactive, isReactive, on, unwrap } from "@ivlev-dev/silent-reactivity";
92
+
93
+ const state = deepReactive({
94
+ user: {
95
+ name: "Ann",
96
+ tags: ["a", "b"],
97
+ },
98
+ });
99
+
100
+ createEffect(() => {
101
+ console.log(state.user.name);
102
+ });
103
+
104
+ createEffect(() => {
105
+ console.log(state.user.tags.join(", "));
106
+ });
107
+
108
+ // уведомляются только индексы от 1 и length, а не всё состояние
109
+ state.user.tags.push("c");
110
+ state.user.tags.splice(0, 1);
111
+
112
+ console.log(isReactive(state.user), state.user.tags); // true true
113
+ console.log(unwrap(state.user)); // исходный объект без прокси
114
+ ```
115
+
116
+ `on` запускает функцию при изменении конкретных зависимостей и передаёт `(value, prev)`:
117
+
118
+ ```ts
119
+ on(() => state.user.name, (name, prev) => console.log(prev, name));
120
+ ```
121
+
122
+ Компоненты store-примитивов привязывают сигналы к экземплярам классов:
123
+
124
+ ```ts
125
+ import {
126
+ createReactiveStore,
127
+ getComputed,
128
+ readField,
129
+ registerCleanup,
130
+ writeField,
131
+ } from "@ivlev-dev/silent-reactivity";
132
+
133
+ const store = createReactiveStore();
134
+
135
+ writeField(instance, "count", () => 0, 5);
136
+ readField(instance, "count", () => 0); // реактивное чтение поля
137
+
138
+ const double = getComputed(instance, "double", () => readField(instance, "count", () => 0) * 2);
139
+
140
+ registerCleanup(store, () => console.log("store disposed"));
141
+ ```
142
+
143
+ Реальная привязка к классам делается в `@ivlev-dev/silent-core` декораторами `@reactive`/`@computed` — пакет то же самое предоставляет как функции.
144
+
145
+ ## Владение и очистка
146
+
147
+ `createRoot` создаёт корневой `Owner`: все эффекты, мемо и cleanup-функции, зарегистрированные внутри, привязываются к нему и уничтожаются при `dispose`. Обнаружить текущего владельца можно через `getOwner`, а выполнять код в контексте конкретного `Owner` — через `runWithOwner`.
148
+
149
+ ```ts
150
+ import {
151
+ createRoot,
152
+ createSignal,
153
+ flushSync,
154
+ getOwner,
155
+ onCleanup,
156
+ runWithOwner,
157
+ } from "@ivlev-dev/silent-reactivity";
158
+
159
+ const [count, setCount] = createSignal(0);
160
+
161
+ const stop = createRoot((dispose) => {
162
+ console.log(getOwner()); // текущий Owner
163
+
164
+ onCleanup(() => console.log("root disposed"));
165
+
166
+ runWithOwner(getOwner(), () => {
167
+ // эффекты здесь привязаны к внешнему owner, а не к глобальному контексту
168
+ });
169
+
170
+ return dispose;
171
+ });
172
+
173
+ setCount(1);
174
+ flushSync(); // принудительно прогоняет все запланированные и начальные эффекты сейчас
175
+
176
+ stop();
177
+ ```
178
+
179
+ `createEffect` планирует обновления в микротаске; `createRenderEffect` выполняется синхронно при создании. `flushSync()` прогоняет очередь вручную, минуя микротаску.
180
+
181
+ ## API
182
+
183
+ | Символ | Назначение |
184
+ | --- | --- |
185
+ | `createSignal` | Сигнал: пара `[read, set]`, опция `equals` |
186
+ | `createMemo` | Ленивое мемо с отслеживанием зависимостей, опция `equals` |
187
+ | `createEffect` | Эффект с автоочисткой зависимостей, обновляется в микротаске |
188
+ | `createRenderEffect` | Эффект, выполняющийся синхронно при создании |
189
+ | `batch` | Откладывает flush до выхода из самого внешнего `batch` |
190
+ | `flushSync` | Принудительный синхронный прогон эффектов |
191
+ | `untrack` | Выполняет функцию без регистрации зависимостей |
192
+ | `createRoot` | Создаёт корневой `Owner` с функцией `dispose` |
193
+ | `getOwner` | Возвращает текущий `Owner` или `null` |
194
+ | `runWithOwner` | Выполняет функцию в контексте конкретного `Owner` |
195
+ | `onCleanup` | Регистрирует очистку в текущем `Owner` |
196
+ | `on` | Запускает функцию на изменение конкретных зависимостей, опция `defer` |
197
+ | `deepReactive` | Ленивый глубокий прокси для plain-объектов и массивов |
198
+ | `unwrap` | Возвращает исходный объект из-под прокси |
199
+ | `isReactive` | Проверяет, является ли значение прокси `deepReactive` |
200
+ | `markReactiveClass` / `isReactiveClass` | Управляет списком классов, которые `deepReactive` не оборачивает повторно |
201
+ | `createReactiveStore` | Создаёт самостоятельный `ReactiveStore` |
202
+ | `storeFor` | Привязывает store к экземпляру (WeakMap) |
203
+ | `readField` / `writeField` | Реактивное чтение и запись поля через store |
204
+ | `getComputed` | Мемо на экземпляре, привязанный к lifecycle store |
205
+ | `registerCleanup` | Очистка в store (или синхронный вызов, если store disposed) |
206
+ | `copyReactiveState` | Переносит сигналы полей между store (используется HMR) |
207
+ | `Accessor`, `Setter` | Типы пары `createSignal` |
208
+ | `SignalOptions`, `MemoOptions`, `OnOptions` | Типы опций: `equals`, `defer` |
209
+ | `Owner`, `ReactiveStore` | Типы владельца и store |
210
+
211
+ ## Ограничения и совместимость
212
+
213
+ - Только ESM, Node.js >= 22, без рантайм-зависимостей.
214
+ - `deepReactive` не оборачивает DOM-узлы, `Date`, `Map`, `Set`, `Promise`, `RegExp`, `Error`, буферы и другие не-plain объекты.
215
+ - Мутирующие методы массивов (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin`) уведомляют только затронутые индексы и `length`; сигналы ключей обновляются только при изменении состава ключей.
216
+ - Апдейтер `Setter` использует `peek()` и не регистрирует зависимость; запись с неизменным значением (по `equals`) не уведомляет наблюдателей.
217
+ - `createMemo` ленивое: до первого чтения значение не вычисляется.
218
+
219
+ ## Ссылки
220
+
221
+ - Репозиторий: <https://github.com/ivlev-dev/silent-framework>
222
+ - Документация: <https://github.com/ivlev-dev/silent-framework/blob/main/docs/api/reactivity.md>
223
+ - CHANGELOG: <https://github.com/ivlev-dev/silent-framework/blob/main/CHANGELOG.md>
224
+ - Баги и предложения: <https://github.com/ivlev-dev/silent-framework/issues>
225
+
226
+ ---
227
+
228
+ MIT © ivlev-dev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivlev-dev/silent-reactivity",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",