@ivlev-dev/silent-reactivity 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ivlev-dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,228 @@
1
- # Temporary Holding Version
1
+ # @ivlev-dev/silent-reactivity
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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/dist/deep.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export declare function markReactiveClass(ctor: Function): void;
2
+ export declare function isReactiveClass(value: unknown): boolean;
3
+ export declare function isReactive(value: unknown): boolean;
4
+ export declare function unwrap<T>(value: T): T;
5
+ export declare function deepReactive<T>(value: T): T;
6
+ //# sourceMappingURL=deep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep.d.ts","sourceRoot":"","sources":["../src/deep.ts"],"names":[],"mappings":"AAymBA,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEvD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAElD;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAUrC;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAgB3C"}