@mirta/store 0.3.4 → 0.4.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
@@ -3,5 +3,299 @@
3
3
  [![en](https://img.shields.io/badge/lang-en-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-store/README.md)
4
4
  [![ru](https://img.shields.io/badge/lang-ru-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-store/README.ru.md)
5
5
  [![NPM Version](https://img.shields.io/npm/v/@mirta/store?style=flat-square)](https://npmjs.com/package/@mirta/store)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/store?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/store)
6
7
 
7
- Type-safe storage solution for wb-rules, shared among all scripts and modules.
8
+ > Type-safe storage solution for automation scenarios, inspired by the Pinia architecture.
9
+
10
+ Each script in the `wb-rules` folder runs in an isolated context — with a separate namespace. This means that functions and variables from one script are inaccessible to others.
11
+
12
+ `@mirta/store` enables moving data into **centralized states**, available across any scripts and modules. Provides a convenient API for:
13
+ - defining state structure,
14
+ - type-safe access,
15
+ - reactive updates,
16
+ - instance isolation.
17
+
18
+ Works on Wiren Board controllers, supports TypeScript, and is compatible with `wb-rules`.
19
+
20
+ ## 📦 Installation
21
+
22
+ ```sh
23
+ pnpm add @mirta/store
24
+ ```
25
+
26
+ ✅ The package is processed by the configuration from `@mirta/rollup` and automatically embedded as a `wb-rules-modules` module when used in code.
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ ### 1. Define the store structure
31
+
32
+ Use `defineStore` to describe the structure. Do this **once**, preferably in a module.
33
+
34
+ ```ts
35
+ // src/wb-rules-modules/counter.ts
36
+ import { defineStore } from '@mirta/store'
37
+
38
+ export const useCounter = defineStore('counter', {
39
+ state: () => ({
40
+ count: 0,
41
+ }),
42
+ getters: {
43
+ double: (state) => state.count * 2,
44
+ },
45
+ actions: {
46
+ increment() {
47
+ this.count++
48
+ },
49
+ setCount(value: number) {
50
+ this.count = value
51
+ },
52
+ },
53
+ })
54
+ ```
55
+
56
+ ### 2. Use in scripts and modules
57
+
58
+ Import the store in any `wb-rules` script or `wb-rules-modules` module — the state will be shared.
59
+
60
+ ```ts
61
+ // src/wb-rules/01-init.ts
62
+ import { useCounter } from '#wbm/counter'
63
+
64
+ const store = useCounter()
65
+ log(`Counter: ${store.count}`) // 0
66
+
67
+ store.increment()
68
+ ```
69
+
70
+ Changes in one script are instantly available in another.
71
+
72
+ ## 📚 API
73
+
74
+ ### `defineStore(typeId, options)`
75
+
76
+ Creates a store definition.
77
+ - **`typeId: string`** — store type identifier (must be unique),
78
+ - **`options: DefineStoreOptions`** — configuration: `state`, `getters`, `actions`.
79
+
80
+ > ❗ Repeated calls with the same `typeId` throw a `StoreError` — definitions are unique.
81
+
82
+ Returns the `useStore()` function.
83
+
84
+ ---
85
+
86
+ ### `useStore(scope?)`
87
+
88
+ Returns a store instance.
89
+
90
+ - **`scope?: string`** — optional context identifier (e.g., `'kitchen'`).
91
+
92
+ If `scope` is provided, `storeId = "${typeId}/${scope}"`.
93
+ Otherwise, the general `storeId = typeId` is used.
94
+
95
+ #### Instance properties
96
+
97
+ | Property | Type | Description |
98
+ |--------|-----|----------|
99
+ | `$id` | `string` | Unique instance identifier |
100
+ | `$state` | `TState` | Reference to the state |
101
+ | `$patch` | `(patch: Partial<TState>) => void`<br/>`(mutator: (state: TState) => void) => void` | Updates the state |
102
+ | `$reset` | `() => void` | Resets the state to initial |
103
+
104
+ ---
105
+
106
+ ### `StoreError`
107
+
108
+ A specialized error class with the following codes:
109
+
110
+ - `'alreadyDefined'` — repeated definition,
111
+ - `'alreadyDefinedOutside'` — repeated definition in another file,
112
+ - `'readonlyProperty'` — modify a readonly property,
113
+ - `'unknownProperty'` — access to unknown property.
114
+
115
+ ## 🔧 Features
116
+
117
+ ### ✅ Full type safety
118
+ Autocompletion, type checking, and support for `this` in getters and actions.
119
+
120
+ <details>
121
+ <summary>Details</summary>
122
+
123
+ ```ts
124
+ getters: {
125
+ double: (state) => state.count * 2,
126
+ doublePlusOne(): number { // ← explicit return type required
127
+ return this.double + 1
128
+ }
129
+ },
130
+ actions: {
131
+ increment() {
132
+ this.count++
133
+ this.$patch({ count: 5 })
134
+ }
135
+ }
136
+ ```
137
+
138
+ > ⚠️ Getters using `this` **must have an explicit return type**.
139
+ </details>
140
+
141
+ ---
142
+
143
+ ### 📦 Deep updates with `$patch`
144
+
145
+ Safely update nested objects — `@mirta/store` performs **deep merging**.
146
+
147
+ <details>
148
+ <summary>Details</summary>
149
+
150
+ Two update methods are supported:
151
+
152
+ ```ts
153
+ store.$patch({ count: 10, config: { debug: true } })
154
+ store.$patch((state) => {
155
+ state.tags.push('new')
156
+ })
157
+ ```
158
+
159
+ > Uses `deepMerge`: objects are merged, arrays are overwritten.
160
+ </details>
161
+
162
+ ---
163
+
164
+ ### 🔁 Reset state with `$reset`
165
+
166
+ Restore the store to its initial state — as when first created.
167
+
168
+ <details>
169
+ <summary>Details</summary>
170
+
171
+ ```ts
172
+ store.$reset()
173
+ ```
174
+
175
+ - Calls `state()`
176
+ - Preserves reactivity
177
+ - Resets state values for testing, logic restart, or configuration reset
178
+ </details>
179
+
180
+ ---
181
+
182
+ ### 🛑 Protection against duplication
183
+
184
+ A store cannot be defined twice with the same `typeId` — prevents conflicts.
185
+
186
+ <details>
187
+ <summary>Details</summary>
188
+
189
+ ```ts
190
+ defineStore('sensor', { ... }) // ✅
191
+ defineStore('sensor', { ... }) // ❌ StoreError: alreadyDefined
192
+ ```
193
+
194
+ > Check runs in both `development` and `production`.
195
+ > Ensures only one module controls a store type.
196
+ </details>
197
+
198
+ ---
199
+
200
+ ### 🧩 Scoped States — isolated instances
201
+
202
+ Create separate store instances for different contexts: rooms, devices, sessions.
203
+
204
+ <details>
205
+ <summary>Details</summary>
206
+
207
+ ```ts
208
+ const useSensor = defineStore('sensor', { ... })
209
+
210
+ const kitchen = useSensor('kitchen')
211
+ const bathroom = useSensor('bathroom')
212
+ ```
213
+
214
+ Each instance has `storeId = "sensor/kitchen"` — state is isolated.
215
+
216
+ > Useful when managing multiple similar entities.
217
+ </details>
218
+
219
+ ---
220
+
221
+ ### 🔐 Internal Store — state encapsulation
222
+
223
+ Make a store inaccessible from outside by not exporting `useStore()`.
224
+
225
+ <details>
226
+ <summary>Details</summary>
227
+
228
+ If `useStore()` is not exported:
229
+ - external modules cannot access the state,
230
+ - redefining with the same `typeId` is prohibited,
231
+ - the state becomes an **internal implementation detail**.
232
+
233
+ > Useful in NPM packages where implementation must be hidden.
234
+ >
235
+ > ❗ Not meaningful in local modules where `defineStore()` and `useStore()` are in the same file.
236
+ </details>
237
+
238
+ ### 💾 State Serialization
239
+
240
+ To save or transfer state, use $state.
241
+
242
+ <details>
243
+ <summary>Details</summary>
244
+
245
+ ```ts
246
+ const store = useCounter()
247
+
248
+ // ✅ Correct — serializes only the state
249
+ const json = JSON.stringify(store.$state)
250
+
251
+ // ❌ Incorrect — includes functions and internal properties
252
+ const json = JSON.stringify(store)
253
+ ```
254
+ The `$state` property contains a plain state object without methods or proxy-related data.
255
+
256
+ </details>
257
+
258
+ ## 🔄 When to use
259
+
260
+ ### 1. Temporary state: `@mirta/store` vs `global.__proto__`
261
+
262
+ | Feature | `@mirta/store` | `global.__proto__` |
263
+ |--------|----------------|--------------------|
264
+ | Encapsulation | ✔️ | ❌ |
265
+ | Type safety | ✔️ | ❌ |
266
+ | API (`$patch`, `$reset`) | ✔️ | ❌ |
267
+ | Instance isolation | ✔️ `useStore('kitchen')` | ❌ |
268
+ | Readability | ✔️ | ❌ |
269
+ | Performance | ✔️ Minimal overhead | ✔️ Direct access |
270
+
271
+ > ❌ `global.__proto__` — **an anti-pattern**. Do not use.
272
+
273
+ ---
274
+
275
+ ### 2. Temporary vs persistent storage
276
+
277
+ | Feature | `@mirta/store` | `PersistentStorage` |
278
+ |--------|----------------|---------------------|
279
+ | Storage | RAM | Flash / FS |
280
+ | Survives reboot | ❌ | ✔️ |
281
+ | Type safety | ✔️ | ❌ |
282
+ | Speed | High | Slower (IO) |
283
+ | API | `$patch`, `$reset` | `get`, `set`, `remove` |
284
+
285
+ > ✅ Use together:
286
+ > - `@mirta/store` — for current runtime state,
287
+ > - `PersistentStorage` — for saving and restoring.
288
+
289
+ ## 🧪 Testing
290
+
291
+ The package is fully covered with unit tests using `@mirta/testing` and `vitest`.
292
+ Tests verify:
293
+ - Store creation and usage
294
+ - `$patch`, `$reset` functionality
295
+ - Support for isolated instances
296
+ - Protection against duplication
297
+
298
+ ## ⚠️ Limitations
299
+
300
+ - State is **lost** upon restarting the `wb-rules.service` or the controller.
301
+ - Store instances are cached globally and **not automatically cleared**. Dynamically creating many instances with unique `scope` values may lead to memory leaks. Use predictable, bounded `scope` values.
package/README.ru.md CHANGED
@@ -3,5 +3,290 @@
3
3
  [![en](https://img.shields.io/badge/lang-en-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-store/README.md)
4
4
  [![ru](https://img.shields.io/badge/lang-ru-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-store/README.ru.md)
5
5
  [![NPM Version](https://img.shields.io/npm/v/@mirta/store?style=flat-square)](https://npmjs.com/package/@mirta/store)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/store?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/store)
6
7
 
7
- Типизированное хранилище для wb-rules, общее для всех скриптов и модулей.
8
+ > Типизированное хранилище состояний для сценариев автоматизации, вдохновлённое архитектурой Pinia.
9
+
10
+ Каждый скрипт в папке `wb-rules` выполняется в изолированном контексте — с отдельным пространством имён. Это означает, что функции и переменные одного скрипта недоступны другим.
11
+
12
+ `@mirta/store` позволяет выносить данные в **централизованные состояния**, доступные из любых скриптов и модулей. Предоставляет удобный API для:
13
+ - определения структуры состояния,
14
+ - типизированного доступа,
15
+ - реактивного обновления,
16
+ - изоляции экземпляров.
17
+
18
+ Работает на контроллерах Wiren Board, поддерживает TypeScript и совместим с `wb-rules`.
19
+
20
+ ## 📦 Установка
21
+
22
+ ```sh
23
+ pnpm add @mirta/store
24
+ ```
25
+
26
+ ✅ Пакет проходит сборку конфигурацией из пакета `@mirta/rollup` и при вызове в коде автоматически встраивается как модуль `wb-rules-modules`.
27
+
28
+ ## 🚀 Быстрый старт
29
+
30
+ ### 1. Задайте структуру хранилища
31
+
32
+ Используйте `defineStore` для описания структуры. Делайте это **один раз**, лучше в модуле.
33
+
34
+ ```ts
35
+ // src/wb-rules-modules/counter.ts
36
+ import { defineStore } from '@mirta/store'
37
+
38
+ export const useCounter = defineStore('counter', {
39
+ state: () => ({
40
+ count: 0,
41
+ }),
42
+ getters: {
43
+ double: (state) => state.count * 2,
44
+ },
45
+ actions: {
46
+ increment() {
47
+ this.count++
48
+ },
49
+ setCount(value: number) {
50
+ this.count = value
51
+ },
52
+ },
53
+ })
54
+ ```
55
+
56
+ ### 2. Используйте в скриптах и модулях
57
+
58
+ Подключите хранилище в любом скрипте `wb-rules` или модуле `wb-rules-modules` — состояние будет общим.
59
+
60
+ ```ts
61
+ // src/wb-rules/01-init.ts
62
+ import { useCounter } from '#wbm/counter'
63
+
64
+ const store = useCounter()
65
+ log(`Счётчик: ${store.count}`) // 0
66
+
67
+ store.increment()
68
+ ```
69
+ Изменения в одном скрипте мгновенно доступны в другом.
70
+
71
+ ## 📚 API
72
+
73
+ ### `defineStore(typeId, options)`
74
+
75
+ Создаёт определение хранилища.
76
+ - **`typeId: string`** — идентификатор типа хранилища (должен быть уникальным),
77
+ - **`options: DefineStoreOptions`** — конфигурация: `state`, `getters`, `actions`.
78
+
79
+ > ❗ Повторный вызов с тем же `typeId` вызывает ошибку `StoreError` — определение уникально.
80
+
81
+ Возвращает функцию `useStore()`.
82
+
83
+ ---
84
+
85
+ ### `useStore(scope?)`
86
+
87
+ Возвращает экземпляр хранилища.
88
+
89
+ - **`scope?: string`** — опциональный идентификатор контекста (например, `'kitchen'`).
90
+
91
+ Если `scope` указан, `storeId = "${typeId}/${scope}"`.
92
+ Если нет — используется общий `storeId = typeId`.
93
+
94
+ #### Свойства экземпляра
95
+
96
+ | Свойство | Тип | Описание |
97
+ |--------|-----|----------|
98
+ | `$id` | `string` | Уникальный идентификатор экземпляра |
99
+ | `$state` | `TState` | Ссылка на состояние |
100
+ | `$patch` | `(patch: Partial<TState>) => void`<br/>`(mutator: (state: TState) => void) => void` | Обновляет состояние |
101
+ | `$reset` | `() => void` | Сбрасывает состояние к начальному |
102
+
103
+ ---
104
+
105
+ ### `StoreError`
106
+
107
+ Специализированный класс ошибок с кодами:
108
+
109
+ - `'alreadyDefined'` — повторное определение,
110
+ - `'alreadyDefinedOutside'` — повторное определение в другом файле,
111
+ - `'readonlyProperty'` — изменение служебного поля,
112
+ - `'unknownProperty'` — обращение к неизвестному полю.
113
+
114
+ ## 🔧 Особенности
115
+
116
+ ### ✅ Полная типобезопасность
117
+
118
+ Автодополнение, проверка типов, поддержка `this` в геттерах и действиях.
119
+
120
+ <details>
121
+ <summary>Подробнее</summary>
122
+
123
+ ```ts
124
+ getters: {
125
+ double: (state) => state.count * 2,
126
+ doublePlusOne(): number { // ← явный тип обязателен
127
+ return this.double + 1
128
+ }
129
+ },
130
+ actions: {
131
+ increment() {
132
+ this.count++
133
+ this.$patch({ count: 5 })
134
+ }
135
+ }
136
+ ```
137
+
138
+ > ⚠️ Геттеры, использующие `this`, **должны иметь явный возвращаемый тип**.
139
+ </details>
140
+
141
+ ### 📦 Глубокое обновление через `$patch`
142
+
143
+ Обновляйте вложенные объекты безопасно — `@mirta/store` выполняет **глубокое слияние**.
144
+
145
+ <details>
146
+ <summary>Подробнее</summary>
147
+
148
+ Поддерживается два способа:
149
+
150
+ ```ts
151
+ store.$patch({ count: 10, config: { debug: true } })
152
+ store.$patch((state) => {
153
+ state.tags.push('new')
154
+ })
155
+ ```
156
+ > Использует `deepMerge`: объекты сливаются, массивы перезаписываются.
157
+ </details>
158
+
159
+ ### 🔁 Сброс состояния через `$reset`
160
+
161
+ Верните хранилище к начальному состоянию — как при первом создании.
162
+
163
+ <details>
164
+ <summary>Подробнее</summary>
165
+
166
+ ```ts
167
+ store.$reset()
168
+ ```
169
+ - Вызывает `state()`
170
+ - Сохраняет реактивность
171
+ - Удаляет значения состояния для тестов, перезапуска логики, сброса настроек
172
+
173
+ </details>
174
+
175
+ ### 🛑 Защита от дублирования
176
+
177
+ Хранилище нельзя определить дважды с одним `typeId` — это предотвращает конфликты.
178
+
179
+ <details>
180
+ <summary>Подробнее</summary>
181
+
182
+ ```ts
183
+ defineStore('sensor', { ... }) // ✅
184
+ defineStore('sensor', { ... }) // ❌ StoreError: alreadyDefined
185
+ ```
186
+ > Проверка работает всегда — в `development` и `production`.
187
+ > Гарантирует, что только один модуль может контролировать тип хранилища.
188
+
189
+ </details>
190
+
191
+ ### 🧩 Scoped States — изолированные экземпляры
192
+
193
+ Создавайте отдельные экземпляры хранилища для разных контекстов: комнат, устройств, сессий.
194
+
195
+ <details>
196
+ <summary>Подробнее</summary>
197
+
198
+ ```ts
199
+ const useSensor = defineStore('sensor', { ... })
200
+
201
+ const kitchen = useSensor('kitchen')
202
+ const bathroom = useSensor('bathroom')
203
+ ```
204
+
205
+ Каждый экземпляр имеет `storeId = "sensor/kitchen"` — состояние изолировано.
206
+
207
+ > Полезно при управлении множеством однотипных сущностей.
208
+
209
+ </details>
210
+
211
+ ### 🔐 Internal Store — инкапсуляция состояния
212
+
213
+ Сделайте хранилище недоступным извне, не экспортируя `useStore()`.
214
+
215
+ <details>
216
+ <summary>Подробнее</summary>
217
+
218
+ Если `useStore()` не экспортирован:
219
+ - внешние модули не могут получить доступ к состоянию,
220
+ - повторное определение с тем же `typeId` запрещено,
221
+ - состояние становится **внутренней деталью реализации**.
222
+
223
+ > Полезно в NPM-пакетах, где нужно скрыть реализацию.
224
+ >
225
+ > ❗ Не имеет смысла в локальных модулях, где `defineStore()` и `useStore()` в одном файле.
226
+
227
+ </details>
228
+
229
+ ### 💾 Сериализация состояния
230
+ Для сохранения или передачи состояния используйте **`$state`**.
231
+
232
+ <details>
233
+ <summary>Подробнее</summary>
234
+
235
+ ```ts
236
+ const store = useCounter()
237
+
238
+ // ✅ Правильно — сериализует только состояние
239
+ const json = JSON.stringify(store.$state)
240
+
241
+ // ❌ Неправильно — содержит функции и служебные поля
242
+ const json = JSON.stringify(store)
243
+ ```
244
+
245
+ Свойство `$state` содержит чистый объект состояния без методов и прокси-данных.
246
+
247
+ </details>
248
+
249
+ ## 🔄 Когда что использовать
250
+
251
+ ### 1. Временное состояние: `@mirta/store` vs `global.__proto__`
252
+
253
+ | Характеристика | `@mirta/store` | `global.__proto__` |
254
+ |----------------|----------------|--------------------|
255
+ | Инкапсуляция | ✔️ | ❌ |
256
+ | Типизация | ✔️ | ❌ |
257
+ | API (`$patch`, `$reset`) | ✔️ | ❌ |
258
+ | Изоляция экземпляров | ✔️ `useStore('kitchen')` | ❌ |
259
+ | Читаемость | ✔️ | ❌ |
260
+ | Производительность | ✔️ Минимальный оверхед | ✔️ Прямой доступ |
261
+
262
+ > ❌ `global.__proto__` — **антипаттерн**. Не используйте.
263
+
264
+ ---
265
+
266
+ ### 2. Временное vs постоянное хранение
267
+
268
+ | Характеристика | `@mirta/store` | `PersistentStorage` |
269
+ |----------------|----------------|---------------------|
270
+ | Хранение | RAM | Flash / FS |
271
+ | После перезагрузки | ❌ | ✔️ |
272
+ | Типизация | ✔️ | ❌ |
273
+ | Скорость | Высокая | Ниже (IO) |
274
+ | API | `$patch`, `$reset` | `get`, `set`, `remove` |
275
+
276
+ > ✅ Используйте совместно:
277
+ > - `@mirta/store` — для текущего состояния,
278
+ > - `PersistentStorage` — для сохранения и восстановления.
279
+
280
+ ## 🧪 Тестирование
281
+
282
+ Пакет полностью покрыт модульными тестами с использованием `@mirta/testing` и `vitest`.
283
+ Тесты проверяют:
284
+ - Создание и использование хранилищ
285
+ - Работу `$patch`, `$reset`
286
+ - Поддержку изолированных экземпляров
287
+ - Защиту от дублирования
288
+
289
+ ## ⚠️ Ограничения
290
+
291
+ - Состояние **теряется** при перезагрузке сервиса `wb-rules.service` или контроллера.
292
+ - Экземпляры хранилищ кэшируются глобально и **не удаляются автоматически**. При динамическом создании большого количества экземпляров с уникальными `scope` может возникнуть утечка памяти. Рекомендуется использовать предсказуемые, ограниченные значения `scope`.