palistor 0.0.25 → 0.0.26
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 +574 -542
- package/README.ru.md +979 -0
- package/dist/index.d.ts +16 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -28
- package/dist/index.js.map +1 -1
- package/dist/react/createTrackingProxy.d.ts +27 -27
- package/dist/react/createTrackingProxy.d.ts.map +1 -1
- package/dist/react/createTrackingProxy.js +54 -53
- package/dist/react/createTrackingProxy.js.map +1 -1
- package/dist/react/useForm.d.ts +42 -43
- package/dist/react/useForm.d.ts.map +1 -1
- package/dist/react/useForm.js +38 -38
- package/dist/react/useForm.js.map +1 -1
- package/dist/react/usePersist.d.ts +14 -14
- package/dist/react/usePersist.js +18 -18
- package/dist/react/usePersist.js.map +1 -1
- package/dist/react/useStoreContext.d.ts +11 -11
- package/dist/react/useStoreContext.js +11 -11
- package/dist/react/useTranslator.d.ts +12 -12
- package/dist/react/useTranslator.js +12 -12
- package/dist/store/applyPatch/applyPatch.d.ts +8 -8
- package/dist/store/applyPatch/applyPatch.js +12 -12
- package/dist/store/applyPatch/applyPatch.js.map +1 -1
- package/dist/store/buildProxy/buildEntityProjectionProxy.js +6 -6
- package/dist/store/buildProxy/buildEntityProjectionProxy.js.map +1 -1
- package/dist/store/buildProxy/buildListProxy.d.ts +8 -8
- package/dist/store/buildProxy/buildListProxy.js +30 -30
- package/dist/store/buildProxy/buildListProxy.js.map +1 -1
- package/dist/store/buildProxy/buildProxy.d.ts +8 -8
- package/dist/store/buildProxy/buildProxy.js +39 -39
- package/dist/store/buildProxy/buildProxy.js.map +1 -1
- package/dist/store/buildProxy/computeProxyKeys.d.ts +8 -8
- package/dist/store/buildProxy/computeProxyKeys.js +9 -9
- package/dist/store/buildProxy/computeProxyKeys.js.map +1 -1
- package/dist/store/buildProxy/handleLazyResolve.d.ts +3 -3
- package/dist/store/buildProxy/handleLazyResolve.js +3 -3
- package/dist/store/buildProxy/initProxyCaches.d.ts +5 -5
- package/dist/store/buildProxy/initProxyCaches.js +5 -5
- package/dist/store/compute/computeFieldState.d.ts +3 -3
- package/dist/store/compute/computeFieldState.js +8 -8
- package/dist/store/compute/computeFieldState.js.map +1 -1
- package/dist/store/compute/fieldStateChanged.d.ts +1 -1
- package/dist/store/compute/fieldStateChanged.js +1 -1
- package/dist/store/compute/recompute/collectGroupComputeNodes.d.ts +4 -4
- package/dist/store/compute/recompute/collectGroupComputeNodes.js +7 -7
- package/dist/store/compute/recompute/collectGroupComputeNodes.js.map +1 -1
- package/dist/store/compute/recompute/recomputeAndNotify.d.ts +3 -3
- package/dist/store/compute/recompute/recomputeAndNotify.js +3 -3
- package/dist/store/compute/recompute/recomputeLeaves.d.ts +11 -10
- package/dist/store/compute/recompute/recomputeLeaves.d.ts.map +1 -1
- package/dist/store/compute/recompute/recomputeLeaves.js +26 -27
- package/dist/store/compute/recompute/recomputeLeaves.js.map +1 -1
- package/dist/store/compute/recompute/recomputeTargeted.d.ts +7 -7
- package/dist/store/compute/recompute/recomputeTargeted.js +12 -12
- package/dist/store/compute/recompute/recomputeTargeted.js.map +1 -1
- package/dist/store/compute/recompute/topologicalSortComputed.d.ts +4 -4
- package/dist/store/compute/recompute/topologicalSortComputed.d.ts.map +1 -1
- package/dist/store/compute/recompute/topologicalSortComputed.js +11 -13
- package/dist/store/compute/recompute/topologicalSortComputed.js.map +1 -1
- package/dist/store/compute/recompute/types.d.ts +4 -4
- package/dist/store/compute/resolveFlag.d.ts +2 -2
- package/dist/store/compute/resolveFlag.js +2 -2
- package/dist/store/compute/resolveString.d.ts +5 -5
- package/dist/store/compute/resolveString.js +6 -6
- package/dist/store/compute/resolveString.js.map +1 -1
- package/dist/store/compute/types.d.ts +3 -3
- package/dist/store/constants.d.ts +48 -48
- package/dist/store/constants.d.ts.map +1 -1
- package/dist/store/constants.js +45 -45
- package/dist/store/defineFieldMapping.d.ts +21 -19
- package/dist/store/defineFieldMapping.d.ts.map +1 -1
- package/dist/store/defineFieldMapping.js +21 -19
- package/dist/store/defineFieldMapping.js.map +1 -1
- package/dist/store/dirtyTracking/collectInitialSnapshot.js +1 -1
- package/dist/store/dirtyTracking/collectInitialSnapshot.js.map +1 -1
- package/dist/store/dirtyTracking/recomputeDirtyTargeted.d.ts +3 -3
- package/dist/store/dirtyTracking/recomputeDirtyTargeted.js +11 -11
- package/dist/store/dirtyTracking/recomputeDirtyTargeted.js.map +1 -1
- package/dist/store/dirtyTracking/setGroupRevalidate.d.ts +6 -6
- package/dist/store/dirtyTracking/setGroupRevalidate.js +7 -7
- package/dist/store/dirtyTracking/setGroupRevalidate.js.map +1 -1
- package/dist/store/entityRegistry/entityRegistry.d.ts +72 -72
- package/dist/store/entityRegistry/entityRegistry.d.ts.map +1 -1
- package/dist/store/entityRegistry/entityRegistry.js +91 -91
- package/dist/store/entityRegistry/entityRegistry.js.map +1 -1
- package/dist/store/entityRegistry/generateId.d.ts +3 -3
- package/dist/store/entityRegistry/generateId.js +3 -3
- package/dist/store/entityRegistry/types.d.ts +17 -17
- package/dist/store/entityRegistry/types.d.ts.map +1 -1
- package/dist/store/flow/buildFlowStepsProxy.d.ts +9 -9
- package/dist/store/flow/buildFlowStepsProxy.js +13 -13
- package/dist/store/flow/buildFlowStepsProxy.js.map +1 -1
- package/dist/store/flow/defineFlow.d.ts +27 -25
- package/dist/store/flow/defineFlow.d.ts.map +1 -1
- package/dist/store/flow/defineFlow.js +15 -13
- package/dist/store/flow/defineFlow.js.map +1 -1
- package/dist/store/flow/flowApi.d.ts +3 -2
- package/dist/store/flow/flowApi.d.ts.map +1 -1
- package/dist/store/flow/flowApi.js +1 -1
- package/dist/store/flow/flowApi.js.map +1 -1
- package/dist/store/flow/flowNavigation.d.ts +65 -63
- package/dist/store/flow/flowNavigation.d.ts.map +1 -1
- package/dist/store/flow/flowNavigation.js +74 -72
- package/dist/store/flow/flowNavigation.js.map +1 -1
- package/dist/store/flow/flowState.d.ts +16 -16
- package/dist/store/flow/flowState.d.ts.map +1 -1
- package/dist/store/flow/flowState.js +5 -5
- package/dist/store/flow/flowState.js.map +1 -1
- package/dist/store/groupDeps/createGroupDeps.d.ts +3 -3
- package/dist/store/groupDeps/createGroupDeps.js +6 -6
- package/dist/store/groupDeps/createGroupDeps.js.map +1 -1
- package/dist/store/groupDeps/createTrackingValues.d.ts +9 -9
- package/dist/store/groupDeps/createTrackingValues.js +11 -11
- package/dist/store/groupDeps/createTrackingValues.js.map +1 -1
- package/dist/store/groupDeps/getNodeGroupPath.d.ts +3 -3
- package/dist/store/groupDeps/getNodeGroupPath.js +5 -5
- package/dist/store/groupDeps/getNodeGroupPath.js.map +1 -1
- package/dist/store/groupDeps/getRecipientGroups.d.ts +1 -1
- package/dist/store/groupDeps/getRecipientGroups.js +1 -1
- package/dist/store/groupDeps/groupDeps.d.ts +7 -7
- package/dist/store/groupDeps/groupDeps.js +7 -7
- package/dist/store/groupDeps/pairKey.d.ts +3 -3
- package/dist/store/groupDeps/pairKey.d.ts.map +1 -1
- package/dist/store/groupDeps/pairKey.js +4 -4
- package/dist/store/groupDeps/pairKey.js.map +1 -1
- package/dist/store/groupDeps/resolveGroupByPath.d.ts +3 -3
- package/dist/store/groupDeps/resolveGroupByPath.js +3 -3
- package/dist/store/init/createNotificationHub.d.ts +31 -31
- package/dist/store/init/createNotificationHub.d.ts.map +1 -1
- package/dist/store/init/createNotificationHub.js +36 -37
- package/dist/store/init/createNotificationHub.js.map +1 -1
- package/dist/store/init/createResolveManager.d.ts +72 -69
- package/dist/store/init/createResolveManager.d.ts.map +1 -1
- package/dist/store/init/createResolveManager.js +101 -98
- package/dist/store/init/createResolveManager.js.map +1 -1
- package/dist/store/init/initGroupSubmitting.d.ts +2 -2
- package/dist/store/init/initGroupSubmitting.js +4 -4
- package/dist/store/init/initGroupSubmitting.js.map +1 -1
- package/dist/store/normalizeConfig.d.ts +20 -18
- package/dist/store/normalizeConfig.d.ts.map +1 -1
- package/dist/store/normalizeConfig.js +31 -29
- package/dist/store/normalizeConfig.js.map +1 -1
- package/dist/store/onChangePipeline/computeFieldKey.d.ts +2 -2
- package/dist/store/onChangePipeline/computeFieldKey.js +2 -2
- package/dist/store/onChangePipeline/findOnChangeAncestors.d.ts +2 -2
- package/dist/store/onChangePipeline/findOnChangeAncestors.js +4 -4
- package/dist/store/onChangePipeline/findOnChangeAncestors.js.map +1 -1
- package/dist/store/onChangePipeline/onChangePipeline.d.ts +13 -12
- package/dist/store/onChangePipeline/onChangePipeline.d.ts.map +1 -1
- package/dist/store/onChangePipeline/onChangePipeline.js +14 -13
- package/dist/store/onChangePipeline/onChangePipeline.js.map +1 -1
- package/dist/store/persist/drivers.d.ts +7 -7
- package/dist/store/persist/drivers.js +8 -8
- package/dist/store/persist/drivers.js.map +1 -1
- package/dist/store/persist/index.d.ts +1 -1
- package/dist/store/persist/persistManager.d.ts +23 -23
- package/dist/store/persist/persistManager.d.ts.map +1 -1
- package/dist/store/persist/persistManager.js +51 -51
- package/dist/store/persist/persistManager.js.map +1 -1
- package/dist/store/persist/types.d.ts +23 -23
- package/dist/store/persist/types.d.ts.map +1 -1
- package/dist/store/persist/types.js +4 -4
- package/dist/store/resetPipeline/buildResetPatch.d.ts +5 -5
- package/dist/store/resetPipeline/buildResetPatch.js +5 -5
- package/dist/store/resetPipeline/collectDefaults.d.ts +5 -5
- package/dist/store/resetPipeline/collectDefaults.js +6 -6
- package/dist/store/resetPipeline/collectDefaults.js.map +1 -1
- package/dist/store/resetPipeline/resetPipeline.d.ts +7 -7
- package/dist/store/resetPipeline/resetPipeline.js +15 -15
- package/dist/store/resetPipeline/resetPipeline.js.map +1 -1
- package/dist/store/resolvePipeline/applyPendingWrites.d.ts +10 -10
- package/dist/store/resolvePipeline/applyPendingWrites.d.ts.map +1 -1
- package/dist/store/resolvePipeline/applyPendingWrites.js +35 -38
- package/dist/store/resolvePipeline/applyPendingWrites.js.map +1 -1
- package/dist/store/resolvePipeline/createContextTrackingProxy.d.ts +8 -7
- package/dist/store/resolvePipeline/createContextTrackingProxy.d.ts.map +1 -1
- package/dist/store/resolvePipeline/createContextTrackingProxy.js +6 -5
- package/dist/store/resolvePipeline/createContextTrackingProxy.js.map +1 -1
- package/dist/store/resolvePipeline/createValuesTrackingProxy.d.ts +43 -43
- package/dist/store/resolvePipeline/createValuesTrackingProxy.d.ts.map +1 -1
- package/dist/store/resolvePipeline/createValuesTrackingProxy.js +70 -70
- package/dist/store/resolvePipeline/createValuesTrackingProxy.js.map +1 -1
- package/dist/store/resolvePipeline/executeListResolve.d.ts +14 -14
- package/dist/store/resolvePipeline/executeListResolve.js +27 -27
- package/dist/store/resolvePipeline/executeListResolve.js.map +1 -1
- package/dist/store/resolvePipeline/executeResolve.d.ts +15 -15
- package/dist/store/resolvePipeline/executeResolve.js +46 -46
- package/dist/store/resolvePipeline/executeResolve.js.map +1 -1
- package/dist/store/store/NodeRegistry/nodeRegistry.d.ts +61 -61
- package/dist/store/store/NodeRegistry/nodeRegistry.d.ts.map +1 -1
- package/dist/store/store/NodeRegistry/nodeRegistry.js +85 -86
- package/dist/store/store/NodeRegistry/nodeRegistry.js.map +1 -1
- package/dist/store/store/NodeRegistry/nodeUtils.d.ts +5 -5
- package/dist/store/store/NodeRegistry/nodeUtils.d.ts.map +1 -1
- package/dist/store/store/NodeRegistry/nodeUtils.js +5 -5
- package/dist/store/store/NodeRegistry/nodeUtils.js.map +1 -1
- package/dist/store/store/groupDepsMap.d.ts +23 -23
- package/dist/store/store/groupDepsMap.d.ts.map +1 -1
- package/dist/store/store/groupDepsMap.js +35 -34
- package/dist/store/store/groupDepsMap.js.map +1 -1
- package/dist/store/store/hasComputedProps.d.ts +2 -2
- package/dist/store/store/hasComputedProps.js +2 -2
- package/dist/store/store/nodeMap.d.ts +5 -5
- package/dist/store/store/nodeMap.js +7 -7
- package/dist/store/store/nodeMap.js.map +1 -1
- package/dist/store/store/palistor.d.ts +80 -80
- package/dist/store/store/palistor.d.ts.map +1 -1
- package/dist/store/store/palistor.js +149 -169
- package/dist/store/store/palistor.js.map +1 -1
- package/dist/store/store/registerNodes.d.ts +10 -10
- package/dist/store/store/registerNodes.js +15 -15
- package/dist/store/store/registerNodes.js.map +1 -1
- package/dist/store/store/serviceRegistry.d.ts +8 -8
- package/dist/store/store/serviceRegistry.d.ts.map +1 -1
- package/dist/store/store/serviceRegistry.js +8 -8
- package/dist/store/store/serviceRegistry.js.map +1 -1
- package/dist/store/store/types.d.ts +236 -234
- package/dist/store/store/types.d.ts.map +1 -1
- package/dist/store/submitPipeline/applyLeafBeforeSubmit.d.ts +2 -2
- package/dist/store/submitPipeline/applyLeafBeforeSubmit.js +2 -2
- package/dist/store/submitPipeline/collectLeafStates.d.ts +2 -2
- package/dist/store/submitPipeline/collectLeafStates.js +2 -2
- package/dist/store/submitPipeline/submitPipeline.d.ts +8 -8
- package/dist/store/submitPipeline/submitPipeline.js +12 -12
- package/dist/store/submitPipeline/submitPipeline.js.map +1 -1
- package/dist/store/submitPipeline/types.d.ts +1 -1
- package/dist/store/submitPipeline/types.d.ts.map +1 -1
- package/dist/store/traversal/nodeClassifier.d.ts +9 -9
- package/dist/store/traversal/nodeClassifier.d.ts.map +1 -1
- package/dist/store/traversal/nodeClassifier.js +11 -11
- package/dist/store/traversal/nodeClassifier.js.map +1 -1
- package/dist/store/traversal/walkFull.d.ts +14 -14
- package/dist/store/traversal/walkFull.js +2 -2
- package/dist/store/valuesCache/valuesCache.d.ts +9 -9
- package/dist/store/valuesCache/valuesCache.d.ts.map +1 -1
- package/dist/store/valuesCache/valuesCache.js +6 -6
- package/dist/store/writePipeline/formatPatch.d.ts +5 -5
- package/dist/store/writePipeline/formatPatch.js +7 -7
- package/dist/store/writePipeline/formatPatch.js.map +1 -1
- package/dist/store/writePipeline/formatValue.d.ts +5 -5
- package/dist/store/writePipeline/formatValue.js +5 -5
- package/dist/store/writePipeline/mergeChanged.d.ts +4 -4
- package/dist/store/writePipeline/mergeChanged.js +4 -4
- package/dist/store/writePipeline/runSetter.d.ts +6 -6
- package/dist/store/writePipeline/runSetter.js +8 -8
- package/dist/store/writePipeline/storeValue.d.ts +3 -3
- package/dist/store/writePipeline/storeValue.js +3 -3
- package/dist/store/writePipeline/types.d.ts +4 -4
- package/dist/store/writePipeline/types.d.ts.map +1 -1
- package/dist/store/writePipeline/writePipeline.d.ts +7 -7
- package/dist/store/writePipeline/writePipeline.d.ts.map +1 -1
- package/dist/store/writePipeline/writePipeline.js +15 -15
- package/dist/store/writePipeline/writePipeline.js.map +1 -1
- package/package.json +11 -4
package/README.ru.md
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
# Palistor
|
|
2
|
+
|
|
3
|
+
> Реактивный state manager для форм в React с гранулярными ре-рендерами
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | **Русский**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/palistor)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://react.dev)
|
|
10
|
+
|
|
11
|
+
Palistor — библиотека управления состоянием форм, построенная на двухслойной proxy-архитектуре: framework-agnostic ядро плюс React-интеграция с точечным трекингом подписок. Компонент перерендеривается **только** при изменении тех полей, которые он реально читал во время рендера.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
const store = new Palistor({
|
|
15
|
+
config: {
|
|
16
|
+
email: { value: "", isRequired: true },
|
|
17
|
+
phone: { value: "", isVisible: (v) => v.email !== "" },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function Form() {
|
|
22
|
+
const form = useForm(store);
|
|
23
|
+
return (
|
|
24
|
+
<input
|
|
25
|
+
value={form.email.value}
|
|
26
|
+
onChange={(e) => (form.email.value = e.target.value)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Содержание
|
|
35
|
+
|
|
36
|
+
- [Возможности](#возможности)
|
|
37
|
+
- [Установка](#установка)
|
|
38
|
+
- [Быстрый старт](#быстрый-старт)
|
|
39
|
+
- [Концепции](#концепции)
|
|
40
|
+
- [API Reference](#api-reference)
|
|
41
|
+
- [Async-резолверы](#async-резолверы)
|
|
42
|
+
- [Списки и сущности](#списки-и-сущности)
|
|
43
|
+
- [Flows (пошаговые мастера)](#flows-пошаговые-мастера)
|
|
44
|
+
- [Field mapping](#field-mapping)
|
|
45
|
+
- [Persist](#persist)
|
|
46
|
+
- [i18n](#i18n)
|
|
47
|
+
- [Уведомления](#уведомления)
|
|
48
|
+
- [Контекст store](#контекст-store)
|
|
49
|
+
- [TypeScript](#typescript)
|
|
50
|
+
- [Лицензия](#лицензия)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Возможности
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| **Гранулярные ре-рендеры** | Компонент подписывается только на те поля, которые он читал — больше ничего не триггерит ре-рендер |
|
|
59
|
+
| **Computed field state** | `isVisible`, `isRequired`, `label`, ошибки валидации пересчитываются автоматически по конфигу |
|
|
60
|
+
| **Proxy API** | Нативный синтаксис: `form.email.value = x` вместо диспатча экшенов |
|
|
61
|
+
| **Submit pipeline** | `beforeSubmit → validate → onSubmit → afterSubmit`; ошибки показываются после первого неудачного submit |
|
|
62
|
+
| **Dirty tracking** | Per-field и per-group флаги изменений; baseline обновляется после resolve и reset |
|
|
63
|
+
| **Async-резолверы** | Загрузка данных с авто-трекингом зависимостей, retry, optimistic updates и React Suspense |
|
|
64
|
+
| **Списки и сущности** | Нормализованный реестр сущностей, list proxy с `add / remove / setItems`, per-entity шаблоны |
|
|
65
|
+
| **Flows** | Пошаговые мастера через `defineFlow` / `defineStep`: навигация, ветвление, валидация по шагам |
|
|
66
|
+
| **Field mapping** | Переименование пропсов состояния поля под конвенцию вашего UI-кита (`isRequired` → `required`, …) |
|
|
67
|
+
| **Persist** | Автосохранение в `localStorage`, `sessionStorage` или любой кастомный драйвер — вместе с навигацией флоу |
|
|
68
|
+
| **i18n** | Одна регистрация транслятора — `label`, `placeholder`, `description` переводятся везде |
|
|
69
|
+
| **Тестируемость** | Framework-agnostic ядро полностью тестируется без React |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Установка
|
|
74
|
+
|
|
75
|
+
Пакет публикуется в **публичный npm-реестр** под именем `palistor`.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install palistor
|
|
79
|
+
# или
|
|
80
|
+
yarn add palistor
|
|
81
|
+
# или
|
|
82
|
+
pnpm add palistor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Peer-зависимость:** `react ^19`
|
|
86
|
+
|
|
87
|
+
> **Альтернатива — GitHub Packages.** Тот же пакет доступен под scoped-именем
|
|
88
|
+
> `@projectint/palistor`. Добавьте в `.npmrc`:
|
|
89
|
+
> ```
|
|
90
|
+
> @projectint:registry=https://npm.pkg.github.com
|
|
91
|
+
> ```
|
|
92
|
+
> затем `npm install @projectint/palistor`. Каноническим считается имя `palistor`.
|
|
93
|
+
|
|
94
|
+
Все публичные символы доступны из корневого модуля:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {
|
|
98
|
+
Palistor,
|
|
99
|
+
useForm,
|
|
100
|
+
usePersist,
|
|
101
|
+
useTranslator,
|
|
102
|
+
useNotifier,
|
|
103
|
+
useStoreContext,
|
|
104
|
+
defineList,
|
|
105
|
+
defineFlow,
|
|
106
|
+
defineStep,
|
|
107
|
+
defineFieldMapping,
|
|
108
|
+
localStorageDriver,
|
|
109
|
+
sessionStorageDriver,
|
|
110
|
+
} from "palistor";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Быстрый старт
|
|
116
|
+
|
|
117
|
+
### 1. Опишите форму
|
|
118
|
+
|
|
119
|
+
Конфиг декларативен: значения полей, валидация, видимость и lifecycle-колбэки живут в одном дереве. Store создаётся на уровне модуля.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { Palistor } from "palistor";
|
|
123
|
+
|
|
124
|
+
export const paymentStore = new Palistor({
|
|
125
|
+
config: {
|
|
126
|
+
paymentType: {
|
|
127
|
+
value: "card",
|
|
128
|
+
label: "Способ оплаты",
|
|
129
|
+
},
|
|
130
|
+
cardNumber: {
|
|
131
|
+
value: "",
|
|
132
|
+
label: "Номер карты",
|
|
133
|
+
placeholder: "0000 0000 0000 0000",
|
|
134
|
+
isVisible: (v) => v.paymentType === "card",
|
|
135
|
+
isRequired: (v) => v.paymentType === "card",
|
|
136
|
+
validate: (value, v) =>
|
|
137
|
+
v.paymentType === "card" && value.length < 16
|
|
138
|
+
? "Введите 16 цифр"
|
|
139
|
+
: undefined,
|
|
140
|
+
},
|
|
141
|
+
passport: {
|
|
142
|
+
isVisible: (v) => v.paymentType === "bank",
|
|
143
|
+
number: { value: "", label: "Серия и номер", isRequired: true },
|
|
144
|
+
issueDate: { value: "", label: "Дата выдачи" },
|
|
145
|
+
},
|
|
146
|
+
amount: { value: 0, label: "Сумма", isRequired: true },
|
|
147
|
+
},
|
|
148
|
+
initialValues: { paymentType: "card" },
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 2. Подключите компонент
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { useForm } from "palistor";
|
|
156
|
+
import { paymentStore } from "./paymentStore";
|
|
157
|
+
|
|
158
|
+
function PaymentForm() {
|
|
159
|
+
const form = useForm(paymentStore);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<form onSubmit={(e) => { e.preventDefault(); paymentStore.submit(); }}>
|
|
163
|
+
<Select
|
|
164
|
+
value={form.paymentType.value}
|
|
165
|
+
onChange={(e) => (form.paymentType.value = e.target.value)}
|
|
166
|
+
label={form.paymentType.label}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{form.cardNumber.isVisible && (
|
|
170
|
+
<Input
|
|
171
|
+
value={form.cardNumber.value}
|
|
172
|
+
onChange={(e) => (form.cardNumber.value = e.target.value)}
|
|
173
|
+
label={form.cardNumber.label}
|
|
174
|
+
isRequired={form.cardNumber.isRequired}
|
|
175
|
+
isInvalid={form.cardNumber.isInvalid}
|
|
176
|
+
errorMessage={form.cardNumber.errorMessage}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{form.passport.isVisible && <PassportSection passport={form.passport} />}
|
|
181
|
+
|
|
182
|
+
<Button type="submit" isLoading={form.submitting}>Оплатить</Button>
|
|
183
|
+
</form>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 3. Изолированный ре-рендер дочернего компонента
|
|
189
|
+
|
|
190
|
+
Если дочернему компоненту нужен **независимый** трекинг, передайте поддерево пропсом и вызовите `useForm` на нём:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
function PassportSection({ passport }) {
|
|
194
|
+
// Собственный tracking proxy — ре-рендер только при изменении полей passport
|
|
195
|
+
const p = useForm(passport);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<>
|
|
199
|
+
<Input value={p.number.value} onChange={(e) => (p.number.value = e.target.value)} label={p.number.label} />
|
|
200
|
+
<Input value={p.issueDate.value} onChange={(e) => (p.issueDate.value = e.target.value)} label={p.issueDate.label} />
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> **Без `useForm` в дочернем** компонент рендерится каскадно вместе с родителем. Для простых листовых компонентов это нормально.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Концепции
|
|
211
|
+
|
|
212
|
+
### Два вида узлов: лист и группа
|
|
213
|
+
|
|
214
|
+
Вид узла определяется наличием свойства `value`:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
Есть "value" → листовой узел (поле формы)
|
|
218
|
+
Нет "value" → групповой узел (контейнер / секция)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const config = {
|
|
223
|
+
// Листовой узел — управляет значением
|
|
224
|
+
email: { value: "", isRequired: true },
|
|
225
|
+
|
|
226
|
+
// Групповой узел — контейнер с вычисляемыми свойствами
|
|
227
|
+
address: {
|
|
228
|
+
isVisible: (v) => v.showAddress,
|
|
229
|
+
city: { value: "" },
|
|
230
|
+
country: { value: "RU" },
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Как работает трекинг
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
Рендер: компонент читает form.email.value и form.phone.value
|
|
239
|
+
→ accessed = { emailNode, phoneNode }
|
|
240
|
+
|
|
241
|
+
SET form.city.value → версия cityNode++ → snapshot не изменился → нет ре-рендера ✓
|
|
242
|
+
SET form.email.value → версия emailNode++ → snapshot изменился → ре-рендер ✓
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Родитель, который только навигирует (`form.passport`), но не читает состояние полей, — **не** перерендеривается при изменении полей внутри `passport`.
|
|
246
|
+
|
|
247
|
+
### Поток данных при записи
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
form.email.value = "user@example.com"
|
|
251
|
+
│
|
|
252
|
+
├─ 1. formatter(rawValue, allValues) → нормализованное значение
|
|
253
|
+
├─ 2. запись значения → nodeState обновлён, valuesCache за O(1)
|
|
254
|
+
├─ 3. setter(value, allValues, prev)? → patch → применяется к смежным полям
|
|
255
|
+
├─ 4. recompute(changedNodes) → таргетированный пересчёт FieldState
|
|
256
|
+
├─ 5. dirty-флаги → распространяются вверх по дереву
|
|
257
|
+
├─ 6. notify → version++ → useSyncExternalStore → ре-рендер
|
|
258
|
+
└─ 7. onChange → onChange группы (fire-and-forget)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Когда показываются ошибки (`revalidate`)
|
|
262
|
+
|
|
263
|
+
Ошибки валидации скрыты до первого неудачного `submit()` объемлющей группы. После него флаг группы `revalidate` становится `true`, и `isInvalid` / `errorMessage` обновляются вживую при каждом вводе.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## API Reference
|
|
268
|
+
|
|
269
|
+
### `new Palistor(options)`
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { Palistor } from "palistor";
|
|
273
|
+
|
|
274
|
+
const store = new Palistor({
|
|
275
|
+
config, // дерево ConfigNode — обязательно
|
|
276
|
+
initialValues, // deep-partial значения поверх дефолтов из конфига
|
|
277
|
+
context, // начальный нереактивный контекст (см. «Контекст store»)
|
|
278
|
+
fieldMapping, // карта переименования пропсов (см. «Field mapping»)
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Store — публичный API:**
|
|
283
|
+
|
|
284
|
+
| Свойство / метод | Возвращает | Описание |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `store.proxy` | proxy | Реактивный прокси, повторяющий конфиг. **Нельзя** передавать его (и его поддеревья) в `useForm` — передавайте сам store |
|
|
287
|
+
| `store.getValues()` | values | Глубокий **клон** всех текущих значений вложенным объектом |
|
|
288
|
+
| `store.submit()` | `Promise<SubmitResult>` | Submit корневой группы |
|
|
289
|
+
| `store.reset(values?)` | `void` | Сброс к дефолтам конфига (или к переданным значениям) |
|
|
290
|
+
| `store.setValues(patch)` | `void` | Bulk-патч: один recompute + notify; setters и formatters не вызываются |
|
|
291
|
+
| `store.set(data)` | `void` | Upsert entity или массива entities в реестре |
|
|
292
|
+
| `store.delete(id)` | `void` | Удалить entity (каскадно удаляет принадлежащие ей дочерние entity) |
|
|
293
|
+
| `store.rekey(oldId, newId)` | `void` | Переименовать entity в реестре и во всех списках |
|
|
294
|
+
| `store.invalidate(id, template?)` | `void` | Сбросить resolved-кэш entity, чтобы её resolve выполнился заново |
|
|
295
|
+
| `store.subscribe(node, fn)` | unsubscribe | Подписка на изменения одного узла |
|
|
296
|
+
| `store.subscribeGlobal(fn)` | unsubscribe | Подписка на любые изменения |
|
|
297
|
+
| `store.getVersion()` | `number` | Глобальная версия (инкремент при каждом изменении) |
|
|
298
|
+
| `store.getNodeVersion(node)` | `number` | Версия конкретного узла |
|
|
299
|
+
| `store.setTranslator(fn \| null)` | `void` | Зарегистрировать i18n-функцию |
|
|
300
|
+
| `store.setNotifier(fn \| null)` | `void` | Зарегистрировать функцию уведомлений |
|
|
301
|
+
| `store.setContext(ctx)` | `void` | Домержить нереактивный контекст (см. «Контекст store») |
|
|
302
|
+
| `store.context` | object | Текущий нереактивный контекст |
|
|
303
|
+
| `store.persist` | `PersistManager` | Менеджер персистенции (`enable / disable / flush`) |
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### `useForm(source)`
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { useForm } from "palistor";
|
|
311
|
+
|
|
312
|
+
const form = useForm(store); // tracking proxy поверх всего store
|
|
313
|
+
const section = useForm(form.address); // независимый tracking поддерева (из пропса)
|
|
314
|
+
const entity = useForm(item, (s) => s.editForm); // привязка entity к template
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Возвращает типизированный tracking proxy. Компонент перерендеривается только при изменении узлов, к которым обращался.
|
|
318
|
+
|
|
319
|
+
| Перегрузка | Когда использовать |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `useForm(store)` | Корень формы; поддеревья передаются вниз пропсами |
|
|
322
|
+
| `useForm(subtreeProp)` | Крупная секция с независимыми ре-рендерами |
|
|
323
|
+
| `useForm(entityProxy, templateSelector)` | Отображение/редактирование entity из списка через template (bind при mount, unbind при unmount) |
|
|
324
|
+
|
|
325
|
+
> ⚠️ Передача сырого поддерева `store.proxy` (например, `useForm(store.proxy.address)`) — **ошибка компиляции и рантайма**. Всегда сначала `useForm(store)`, а дальше — через возвращённый прокси.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
### Листовой узел — свойства прокси
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Чтение реактивно — регистрирует узел в tracking set
|
|
333
|
+
form.email.value // → TValue
|
|
334
|
+
form.email.label // → string | undefined
|
|
335
|
+
form.email.placeholder // → string | undefined
|
|
336
|
+
form.email.description // → string | undefined
|
|
337
|
+
form.email.isRequired // → boolean
|
|
338
|
+
form.email.isReadOnly // → boolean
|
|
339
|
+
form.email.isDisabled // → boolean
|
|
340
|
+
form.email.isVisible // → boolean
|
|
341
|
+
form.email.isInvalid // → boolean | undefined
|
|
342
|
+
form.email.errorMessage // → string | undefined
|
|
343
|
+
form.email.dirty // → boolean
|
|
344
|
+
form.email.loading // → boolean (per-field резолвер)
|
|
345
|
+
|
|
346
|
+
// Запись триггерит formatter → setter → recompute → notify
|
|
347
|
+
form.email.value = "new@example.com";
|
|
348
|
+
form.email.onValueChange("new@example.com"); // эквивалент, удобно как callback-проп
|
|
349
|
+
|
|
350
|
+
// Лист можно сабмитить и отдельно (тот же pipeline)
|
|
351
|
+
form.email.submitting // → boolean
|
|
352
|
+
await form.email.submit(); // → SubmitResult
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Групповой узел — свойства прокси
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
form.passport.isVisible // → boolean
|
|
359
|
+
form.passport.isRequired // → boolean | undefined
|
|
360
|
+
form.passport.isReadOnly // → boolean | undefined
|
|
361
|
+
form.passport.isDisabled // → boolean | undefined
|
|
362
|
+
form.passport.isInvalid // → boolean | undefined
|
|
363
|
+
form.passport.errorMessage // → string | undefined
|
|
364
|
+
form.passport.submitting // → boolean
|
|
365
|
+
form.passport.loading // → boolean (выполняется async-резолвер)
|
|
366
|
+
form.passport.dirty // → boolean (хотя бы одно поле изменилось)
|
|
367
|
+
form.passport.revalidate // → boolean (true после первого неудачного submit)
|
|
368
|
+
form.passport.values // → живой снапшот значений группы (стабильная ссылка)
|
|
369
|
+
|
|
370
|
+
await form.passport.submit(); // → SubmitResult
|
|
371
|
+
form.passport.reset({ number: "" }); // сброс поддерева
|
|
372
|
+
form.passport.setValues({ number: "AB1234" }); // bulk-патч без setters/formatters
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`values` — живая ссылка внутрь кэша значений: обновляется in-place при каждой записи, сама ссылка стабильна — безопасно передавать в API-вызов. Для отвязанного глубокого клона используйте `store.getValues()`.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### `ConfigNode` — схема поля
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// Листовой узел (есть "value")
|
|
383
|
+
{
|
|
384
|
+
value?: TValue | ((values: TValues) => TValue), // константа или computed
|
|
385
|
+
validate?: (value, values, t) => string | undefined | false,
|
|
386
|
+
formatter?: (raw, values) => TValue, // нормализация при записи
|
|
387
|
+
setter?: (value, values, previousValue) => DeepPartialValues<TValues>, // патч смежных полей
|
|
388
|
+
beforeSubmit?: (value, groupValues) => TValue, // трансформация перед submit (store не мутируется)
|
|
389
|
+
resolve?: Resolve<TValue>, // per-field резолвер (внутри template списка)
|
|
390
|
+
dependencies?: string[], // топологический порядок для цепочек computed
|
|
391
|
+
componentProps?: Record<string, unknown>,
|
|
392
|
+
|
|
393
|
+
label?: string | ((t, values) => string),
|
|
394
|
+
placeholder?: string | ((t, values) => string),
|
|
395
|
+
description?: string | ((t, values) => string),
|
|
396
|
+
isVisible?: boolean | ((values) => boolean), // default: true
|
|
397
|
+
isRequired?: boolean | ((values) => boolean), // default: false
|
|
398
|
+
isDisabled?: boolean | ((values) => boolean), // default: false
|
|
399
|
+
isReadOnly?: boolean | ((values) => boolean), // default: false
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Групповой узел (нет "value")
|
|
403
|
+
{
|
|
404
|
+
beforeSubmit?: (values) => values,
|
|
405
|
+
onSubmit?: (values, store, parentProxy) => Promise<unknown> | unknown,
|
|
406
|
+
afterSubmit?: (result, { reset }) => void | Promise<void>,
|
|
407
|
+
reset?: (defaults) => values, // трансформация при reset
|
|
408
|
+
onChange?: ({ fieldKey, newValue, previousValue, allValues }) => patch | void,
|
|
409
|
+
resolve?: Resolve, // async-резолвер (см. ниже)
|
|
410
|
+
|
|
411
|
+
isVisible?, isRequired?, isDisabled?, isReadOnly?, // как у листа
|
|
412
|
+
|
|
413
|
+
[childKey]: LeafNode | GroupNode | ListNode, // дочерние узлы
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Узел-список — массив длиной 1 или 2 (или defineList)
|
|
417
|
+
[templateGroupNode]
|
|
418
|
+
[templateGroupNode, { resolve: { resolver, deps?, onError? } }]
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### `SubmitResult`
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
type SubmitResult =
|
|
425
|
+
| { success: true; result?: unknown }
|
|
426
|
+
| { success: false; errors: Array<{ path: string; message: string }> };
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Рецепты
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// Computed value
|
|
433
|
+
total: { value: (v) => v.price * v.quantity, isReadOnly: true },
|
|
434
|
+
|
|
435
|
+
// Цепочка computed — dependencies задают топологический порядок
|
|
436
|
+
tax: { value: (v) => v.price * 0.2, dependencies: ["price"] },
|
|
437
|
+
total: { value: (v) => v.price + v.tax, dependencies: ["price", "tax"] },
|
|
438
|
+
|
|
439
|
+
// formatter — нормализация при записи
|
|
440
|
+
email: { value: "", formatter: (v) => String(v).trim().toLowerCase() },
|
|
441
|
+
|
|
442
|
+
// setter — каскадное изменение других полей
|
|
443
|
+
country: { value: "RU", setter: (value) => ({ city: "" }) },
|
|
444
|
+
|
|
445
|
+
// onChange группы — реакция на изменение любого поля внутри
|
|
446
|
+
passport: {
|
|
447
|
+
onChange: ({ fieldKey }) => {
|
|
448
|
+
if (fieldKey === "number") return { issueDate: "" };
|
|
449
|
+
},
|
|
450
|
+
number: { value: "" },
|
|
451
|
+
issueDate: { value: "" },
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// Групповой submit с валидацией
|
|
455
|
+
company: {
|
|
456
|
+
onSubmit: async (values) => api.saveCompany(values),
|
|
457
|
+
afterSubmit: (_result, { reset }) => { showSuccessToast(); reset(); },
|
|
458
|
+
name: { value: "", isRequired: true },
|
|
459
|
+
taxId: { value: "" },
|
|
460
|
+
},
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Async-резолверы
|
|
466
|
+
|
|
467
|
+
Резолвер конфигурируется на групповом узле. Загружает данные асинхронно — с авто-трекингом зависимостей, retry и поддержкой React Suspense.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
const store = new Palistor({
|
|
471
|
+
config: {
|
|
472
|
+
userId: { value: "" },
|
|
473
|
+
|
|
474
|
+
userInfo: {
|
|
475
|
+
resolve: {
|
|
476
|
+
// `values` — tracking proxy: каждый GET становится зависимостью.
|
|
477
|
+
// При изменении userId резолвер перезапустится автоматически.
|
|
478
|
+
// `store` даёт доступ к остальному store (и к store.context).
|
|
479
|
+
resolver: async (values, store) => {
|
|
480
|
+
const data = await api.getUser(values.userId);
|
|
481
|
+
return { name: data.name, email: data.email };
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
// Мгновенный placeholder, пока резолвер выполняется
|
|
485
|
+
optimisticResolver: (values) => ({ name: "Загрузка…" }),
|
|
486
|
+
|
|
487
|
+
onError: (error, ctx) => {
|
|
488
|
+
ctx.notify("Не удалось загрузить данные", "USER_LOAD_ERROR");
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
deps: ["userId"], // явные зависимости (для первого запуска)
|
|
492
|
+
contextDeps: ["accountId"], // ждать, пока context.accountId != null
|
|
493
|
+
|
|
494
|
+
options: {
|
|
495
|
+
lazy: true, // ждать первого обращения к узлу (default: true)
|
|
496
|
+
suspense: false, // бросать promise для React Suspense (default: false)
|
|
497
|
+
retry: { attempts: 3, delay: 1000 },
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
name: { value: "" },
|
|
502
|
+
email: { value: "" },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
const form = useForm(store);
|
|
510
|
+
|
|
511
|
+
// Без Suspense — ручная проверка loading
|
|
512
|
+
if (form.userInfo.loading) return <Spinner />;
|
|
513
|
+
|
|
514
|
+
// С Suspense — автоматически
|
|
515
|
+
<Suspense fallback={<Spinner />}>
|
|
516
|
+
<UserInfoSection />
|
|
517
|
+
</Suspense>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
При изменении зависимости resolve-состояние сбрасывается, `optimisticResolver` применяется мгновенно, резолвер перезапускается. После успеха обновляется dirty-baseline — загруженные данные не считаются «грязными».
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Списки и сущности
|
|
525
|
+
|
|
526
|
+
Списки объявляются через `defineList` (предпочтительно — полная типизация) или сырым массивом длиной 1–2, где `[0]` — шаблон элемента.
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { defineList, Palistor } from "palistor";
|
|
530
|
+
|
|
531
|
+
interface User { id: string; name: string; email: string }
|
|
532
|
+
|
|
533
|
+
const users = defineList<User>({
|
|
534
|
+
template: {
|
|
535
|
+
id: { value: "" },
|
|
536
|
+
name: { value: "", isRequired: true },
|
|
537
|
+
email: { value: "" },
|
|
538
|
+
},
|
|
539
|
+
resolve: {
|
|
540
|
+
resolver: async (values, store) => api.getUsers(values.filter), // → Promise<User[]>
|
|
541
|
+
deps: ["filter"],
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const store = new Palistor({
|
|
546
|
+
config: { filter: { value: "" }, users },
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Элементы хранятся в **нормализованном реестре сущностей**: одна entity может входить в несколько списков и отображаться через разные шаблоны без дублирования.
|
|
551
|
+
|
|
552
|
+
### List Proxy API
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
const form = useForm(store);
|
|
556
|
+
|
|
557
|
+
// Чтение
|
|
558
|
+
form.users.items // ReadonlyArray<entity proxy>
|
|
559
|
+
form.users.length // number
|
|
560
|
+
form.users.loading // boolean
|
|
561
|
+
form.users.dirty // boolean — состав списка изменился vs. baseline
|
|
562
|
+
form.users.getValues() // Array<plain object>
|
|
563
|
+
|
|
564
|
+
// Итерация
|
|
565
|
+
form.users.map((item, index, id) => <Row key={id} item={item} />)
|
|
566
|
+
for (const item of form.users) { /* … */ }
|
|
567
|
+
|
|
568
|
+
// Мутации
|
|
569
|
+
form.users.add({ name: "Alice", email: "alice@ex.com" }); // объект → upsert + добавить; вернёт прокси элемента
|
|
570
|
+
form.users.add("existing-id"); // строка → добавить существующую entity
|
|
571
|
+
form.users.remove("user-id");
|
|
572
|
+
form.users.setItems(["id1", "id2", "id3"]); // bulk-замена
|
|
573
|
+
form.users.getById("user-id"); // → прокси элемента | undefined
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Элемент списка — свойства прокси
|
|
577
|
+
|
|
578
|
+
Каждый элемент `form.users.items` — проекция entity через шаблон:
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
const item = form.users.items[0];
|
|
582
|
+
|
|
583
|
+
item.id // string — идентификатор entity
|
|
584
|
+
item.name.value // значение поля через template (работают formatter/validate/isRequired)
|
|
585
|
+
item.name.label // computed label из template
|
|
586
|
+
// …все leaf-свойства: value, label, placeholder, isRequired, isReadOnly, isDisabled,
|
|
587
|
+
// isVisible, isInvalid, errorMessage, dirty, loading, onValueChange
|
|
588
|
+
|
|
589
|
+
item.loading // boolean — для этой entity выполняется resolve
|
|
590
|
+
item.submitting // boolean — для этой entity выполняется submit
|
|
591
|
+
item.values // plain-объект текущих значений entity
|
|
592
|
+
await item.submit(); // → SubmitResult — валидация + onSubmit из template
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Отображение entity через другой шаблон
|
|
596
|
+
|
|
597
|
+
Привяжите entity к любой template-группе (например, форме редактирования) через двухаргументный `useForm`:
|
|
598
|
+
|
|
599
|
+
```tsx
|
|
600
|
+
function UserRow({ user }: { user: PalistorRef<User> }) {
|
|
601
|
+
const u = useForm(user, (s) => s.editUserForm);
|
|
602
|
+
return <span>{u.name.value}</span>;
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
При mount entity привязывается к шаблону, и резолвер шаблона выполняется один раз на пару entity+template (кэшируется). `store.invalidate(id, template?)` заставит его выполниться заново.
|
|
607
|
+
|
|
608
|
+
### Работа с entity напрямую
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
// Создать / обновить entities (одна или батч — один recompute + notify)
|
|
612
|
+
store.set({ id: "u1", name: "Bob" });
|
|
613
|
+
store.set([{ id: "u1" }, { id: "u2" }]);
|
|
614
|
+
|
|
615
|
+
// Если id не передан — генерируется временный (_tmp_…).
|
|
616
|
+
// После того как сервер присвоил настоящий id:
|
|
617
|
+
store.rekey(tmpId, serverAssignedId);
|
|
618
|
+
|
|
619
|
+
// Удалить entity (принадлежащие ей дочерние удаляются каскадно)
|
|
620
|
+
store.delete("u1");
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Flows (пошаговые мастера)
|
|
626
|
+
|
|
627
|
+
`defineFlow` / `defineStep` строят пошаговый мастер поверх обычных групповых узлов: навигационное состояние, статусы шагов, ветвление и валидация по шагам.
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import { defineFlow, defineStep, Palistor } from "palistor";
|
|
631
|
+
|
|
632
|
+
const onboarding = defineFlow({
|
|
633
|
+
steps: [
|
|
634
|
+
defineStep("account", {
|
|
635
|
+
fullName: { value: "", isRequired: true },
|
|
636
|
+
email: { value: "", isRequired: true },
|
|
637
|
+
// Третий аргумент onSubmit — flow-proxy; навигационные методы bound,
|
|
638
|
+
// поэтому деструктуризация работает:
|
|
639
|
+
onSubmit: (_values, _store, { nextStep }) => nextStep(),
|
|
640
|
+
}),
|
|
641
|
+
|
|
642
|
+
defineStep("plan", {
|
|
643
|
+
plan: { value: "", isRequired: true },
|
|
644
|
+
onSubmit: (_values, _store, { nextStep }) => nextStep(),
|
|
645
|
+
}),
|
|
646
|
+
|
|
647
|
+
// Ветвление: скрытый шаг пропускается nextStep() и исключается из
|
|
648
|
+
// финальной валидации.
|
|
649
|
+
defineStep("company", {
|
|
650
|
+
isVisible: (values) => values.plan.plan === "enterprise",
|
|
651
|
+
companyName: { value: "", isRequired: true },
|
|
652
|
+
onSubmit: (_values, _store, { nextStep }) => nextStep(),
|
|
653
|
+
}),
|
|
654
|
+
|
|
655
|
+
defineStep("summary", {}), // read-only шаг-сводка
|
|
656
|
+
],
|
|
657
|
+
|
|
658
|
+
// Финализация уровня флоу — идёт через стандартный submit pipeline
|
|
659
|
+
onSubmit: async (allValues, store) => api.completeOnboarding(allValues),
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const store = new Palistor({ config: { onboarding } });
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Flow Proxy API
|
|
666
|
+
|
|
667
|
+
```tsx
|
|
668
|
+
const form = useForm(store);
|
|
669
|
+
const flow = form.onboarding;
|
|
670
|
+
|
|
671
|
+
// Навигационное состояние (реактивное)
|
|
672
|
+
flow.currentStepKey // "account" | "plan" | …
|
|
673
|
+
flow.currentStepIndex // number
|
|
674
|
+
flow.canGoBack // boolean — стек посещений непуст
|
|
675
|
+
flow.history // readonly string[] — [...visitStack, currentStepKey]
|
|
676
|
+
flow.errors // FlowError[] — от последнего validate() / финализации
|
|
677
|
+
|
|
678
|
+
// Коллекция шагов
|
|
679
|
+
flow.steps.current // прокси активного шага
|
|
680
|
+
flow.steps.account // по ключу
|
|
681
|
+
flow.steps[0] // по индексу
|
|
682
|
+
flow.steps.length // число шагов
|
|
683
|
+
[...flow.steps] // итерируемо
|
|
684
|
+
|
|
685
|
+
// Прокси шага = обычный group proxy + status
|
|
686
|
+
flow.steps.account.status // "active" | "completed" | null (ещё не посещался)
|
|
687
|
+
flow.steps.account.email // прокси поля — обычный лист
|
|
688
|
+
|
|
689
|
+
// Навигация
|
|
690
|
+
flow.nextStep(); // следующий ВИДИМЫЙ шаг; впереди нет → финализация через flow.submit()
|
|
691
|
+
flow.back(); // pop стека посещений; no-op при пустом стеке
|
|
692
|
+
flow.goTo("plan"); // прыжок по ключу или индексу; throw при неизвестном ключе
|
|
693
|
+
flow.validate(); // валидация посещённых видимых шагов → flow.errors
|
|
694
|
+
|
|
695
|
+
// Значения и финализация
|
|
696
|
+
flow.values // аккумулированные значения всех шагов, по ключам шагов
|
|
697
|
+
await flow.submit(); // стандартный pipeline; ошибки скрытых шагов отфильтровываются
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Lifecycle шага
|
|
701
|
+
|
|
702
|
+
Конфиг шага, помимо всего, что умеет группа, принимает два flow-колбэка:
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
defineStep("details", {
|
|
706
|
+
onEnter: (flowValues, store) => { /* при входе в шаг */ },
|
|
707
|
+
onReady: (flowValues, store) => { /* после завершения resolve шага */ },
|
|
708
|
+
resolve: { resolver: async (values, store) => api.getDetails(), onError: () => {} },
|
|
709
|
+
// …поля
|
|
710
|
+
})
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
При входе в шаг: `onEnter → resolve (запускается сразу) → onReady`. Первый шаг каждого флоу «входится» при создании store. Результат resolve шага кэшируется — повторный вход его не перезапускает. `reset()` флоу (или любого предка) сбрасывает навигацию на первый шаг и заново выполняет entry lifecycle.
|
|
714
|
+
|
|
715
|
+
Навигация флоу (текущий шаг, история) входит в снапшот [persist](#persist) и восстанавливается при гидратации.
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Field mapping
|
|
720
|
+
|
|
721
|
+
`fieldMapping` переименовывает пропсы состояния поля **на границе proxy** — GET, SET, tracking и spread, — так что `{...form.email}` можно спредить напрямую в компонент MUI, Ant Design или нативный HTML без адаптеров. Внутреннее ядро не меняется.
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import { Palistor, defineFieldMapping, useForm } from "palistor";
|
|
725
|
+
|
|
726
|
+
// defineFieldMapping сохраняет литеральные типы — переименованные пропсы
|
|
727
|
+
// статически типизированы на store.proxy / useForm(store).
|
|
728
|
+
const uiFieldMapping = defineFieldMapping({
|
|
729
|
+
isRequired: "required",
|
|
730
|
+
isDisabled: "disabled",
|
|
731
|
+
isReadOnly: "readOnly",
|
|
732
|
+
isInvalid: "error",
|
|
733
|
+
errorMessage: "helperText",
|
|
734
|
+
description: "helpText",
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const store = new Palistor({
|
|
738
|
+
// Конфиг пишется в ТОМ ЖЕ публичном словаре (external-имена).
|
|
739
|
+
// Написать internal-имя (isRequired) ремапленного ключа — ошибка типа.
|
|
740
|
+
config: {
|
|
741
|
+
email: {
|
|
742
|
+
value: "",
|
|
743
|
+
label: "Email",
|
|
744
|
+
required: true,
|
|
745
|
+
helpText: "Мы никому не передаём ваш email",
|
|
746
|
+
validate: (v: string) => (!v.includes("@") ? "Некорректный email" : undefined),
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
fieldMapping: uiFieldMapping,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
function EmailField() {
|
|
753
|
+
const form = useForm(store);
|
|
754
|
+
return <TextField {...form.email} />; // required / error / helperText — как ждёт MUI
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Переименовываемые ключи: `value`, `label`, `placeholder`, `description`, `isRequired`, `isReadOnly`, `isDisabled`, `isVisible`, `isInvalid`, `errorMessage`, `dirty`, `loading`, `onValueChange`. Без `fieldMapping` — нулевой оверхед и никаких изменений поведения.
|
|
759
|
+
|
|
760
|
+
> Для переиспользуемой карты используйте `defineFieldMapping` (или `as const`) — аннотация `: FieldMapping` или `satisfies FieldMapping` расширит литералы до `string`, и статическое переименование потеряется.
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## Persist
|
|
765
|
+
|
|
766
|
+
Автосохранение состояния формы в любое хранилище.
|
|
767
|
+
|
|
768
|
+
### React-хук (рекомендуется)
|
|
769
|
+
|
|
770
|
+
```tsx
|
|
771
|
+
import { usePersist, localStorageDriver } from "palistor";
|
|
772
|
+
|
|
773
|
+
function PaymentPage({ orderId }: { orderId: string }) {
|
|
774
|
+
usePersist(paymentStore, {
|
|
775
|
+
key: `payment-${orderId}`, // ключ может зависеть от пропсов / роутера
|
|
776
|
+
driver: localStorageDriver,
|
|
777
|
+
debounce: 500, // ms, default: 100
|
|
778
|
+
pick: ["cardNumber"], // персистить только эти top-level поля
|
|
779
|
+
// omit: ["cvv"], // …или исключить чувствительные
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const form = useForm(paymentStore);
|
|
783
|
+
// …
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Хук гидратирует при mount, автосохраняет при изменениях, при unmount делает flush и отключается.
|
|
788
|
+
|
|
789
|
+
### Вне React
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import { localStorageDriver } from "palistor";
|
|
793
|
+
|
|
794
|
+
paymentStore.persist.enable({ key: "payment", driver: localStorageDriver });
|
|
795
|
+
await paymentStore.persist.flush(); // принудительное сохранение
|
|
796
|
+
paymentStore.persist.disable();
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Кастомный драйвер
|
|
800
|
+
|
|
801
|
+
Синхронный или асинхронный — поддерживаются оба (localStorage, IndexedDB, AsyncStorage, …):
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
import type { PersistDriver } from "palistor";
|
|
805
|
+
|
|
806
|
+
const myDriver: PersistDriver = {
|
|
807
|
+
getItem: (key) => myStorage.get(key), // string | null | Promise<…>
|
|
808
|
+
setItem: (key, value) => myStorage.set(key, value),
|
|
809
|
+
removeItem: (key) => myStorage.delete(key),
|
|
810
|
+
};
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
**`PersistOptions`:**
|
|
814
|
+
|
|
815
|
+
| Опция | Тип | По умолчанию | Описание |
|
|
816
|
+
|---|---|---|---|
|
|
817
|
+
| `key` | `string` | — | Ключ хранения |
|
|
818
|
+
| `driver` | `PersistDriver` | — | Реализация хранилища |
|
|
819
|
+
| `debounce` | `number` | `100` | Задержка записи, ms (`0` = мгновенно) |
|
|
820
|
+
| `serialize` | `fn` | `JSON.stringify` | Кастомный сериализатор |
|
|
821
|
+
| `deserialize` | `fn` | `JSON.parse` | Кастомный десериализатор |
|
|
822
|
+
| `pick` | `string[]` | — | Персистить только эти top-level ключи |
|
|
823
|
+
| `omit` | `string[]` | — | Исключить эти ключи (игнорируется при заданном `pick`) |
|
|
824
|
+
|
|
825
|
+
В снапшот входят состав списков и навигация флоу; и то и другое восстанавливается при гидратации.
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
## i18n
|
|
830
|
+
|
|
831
|
+
Зарегистрируйте функцию перевода один раз — в layout или провайдере. Все компоненты с `useForm` автоматически получат переведённые `label` / `placeholder` / `description`; при смене локали подписанные компоненты перерендерятся.
|
|
832
|
+
|
|
833
|
+
```tsx
|
|
834
|
+
import { useTranslations } from "next-intl";
|
|
835
|
+
import { useTranslator } from "palistor";
|
|
836
|
+
|
|
837
|
+
function RootLayout({ children }: { children: React.ReactNode }) {
|
|
838
|
+
const t = useTranslations();
|
|
839
|
+
useTranslator(paymentStore, t);
|
|
840
|
+
return <>{children}</>;
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
В конфиге переводимые строки — функции от `(t, values)`:
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
cardNumber: {
|
|
848
|
+
value: "",
|
|
849
|
+
label: (t) => t("fields.cardNumber"),
|
|
850
|
+
placeholder: (t) => t("fields.cardNumber.placeholder"),
|
|
851
|
+
},
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
Колбэк `validate` тоже получает `t` третьим аргументом. Без зарегистрированного транслятора функции получают identity-`t` — удобно для тестов и SSR без i18n-окружения.
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## Уведомления
|
|
859
|
+
|
|
860
|
+
Зарегистрируйте функцию toast/alert один раз; резолверы получат её в `onError` через `ctx.notify`:
|
|
861
|
+
|
|
862
|
+
```tsx
|
|
863
|
+
import { useCallback } from "react";
|
|
864
|
+
import { useNotifier } from "palistor";
|
|
865
|
+
|
|
866
|
+
function RootLayout({ children }: { children: React.ReactNode }) {
|
|
867
|
+
const notifyError = useCallback((error: unknown, code?: string) => {
|
|
868
|
+
addToast({ title: code ?? "Ошибка", color: "danger" });
|
|
869
|
+
}, []);
|
|
870
|
+
|
|
871
|
+
useNotifier(paymentStore, notifyError);
|
|
872
|
+
return <>{children}</>;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## Контекст store
|
|
879
|
+
|
|
880
|
+
Нереактивные данные (id аккаунта, tenant, токены…), доступные во всех колбэках через `store.context`. Не является частью формы — не попадает в `getValues()`, submit-payload и persist.
|
|
881
|
+
|
|
882
|
+
```tsx
|
|
883
|
+
import { useStoreContext } from "palistor";
|
|
884
|
+
|
|
885
|
+
function Layout({ children }) {
|
|
886
|
+
const accountId = useAccountId();
|
|
887
|
+
useStoreContext(store, { accountId }); // мержится в store.context
|
|
888
|
+
return <>{children}</>;
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
resolve: {
|
|
894
|
+
resolver: async (values, store) => api.fetchUsers(store.context.accountId),
|
|
895
|
+
contextDeps: ["accountId"], // не запускаться, пока context.accountId == null
|
|
896
|
+
},
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Изменение ключа контекста перезапускает зависящие от него резолверы (через `contextDeps` или затреканный путь `$context.…`). Начальный контекст можно передать в конструктор: `new Palistor({ config, context: { accountId } })`.
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## TypeScript
|
|
904
|
+
|
|
905
|
+
Palistor полностью типизирован — значения, прокси, entity и даже переименования `fieldMapping` выводятся статически.
|
|
906
|
+
|
|
907
|
+
### Вывод типа значений из конфига
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
import type { ExtractValues, DeepPartialValues } from "palistor";
|
|
911
|
+
|
|
912
|
+
const config = {
|
|
913
|
+
name: { value: "" },
|
|
914
|
+
age: { value: 0 },
|
|
915
|
+
address: { city: { value: "" }, country: { value: "RU" } },
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
type FormValues = ExtractValues<typeof config>;
|
|
919
|
+
// → { name: string; age: number; address: { city: string; country: string } }
|
|
920
|
+
|
|
921
|
+
const initial: DeepPartialValues<FormValues> = { address: { city: "Москва" } };
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### Типизация пропсов дочерних компонентов без импорта конфига
|
|
925
|
+
|
|
926
|
+
`PalistorProxy<T>` маппит простой интерфейс значений на прокси-дерево (тип называется `PalistorProxy`, потому что имя `Palistor` занято классом store):
|
|
927
|
+
|
|
928
|
+
```typescript
|
|
929
|
+
import type { PalistorProxy } from "palistor";
|
|
930
|
+
|
|
931
|
+
interface UserData { name: string; email: string; address: { city: string } }
|
|
932
|
+
|
|
933
|
+
function UserForm({ user }: { user: PalistorProxy<UserData> }) {
|
|
934
|
+
const u = useForm(user);
|
|
935
|
+
return <input value={u.name.value} onChange={(e) => (u.name.value = e.target.value)} />;
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### Типизированные ссылки на entity
|
|
940
|
+
|
|
941
|
+
```typescript
|
|
942
|
+
import type { PalistorRef, PalistorList, InferEntity } from "palistor";
|
|
943
|
+
|
|
944
|
+
interface User { id: string; name: string }
|
|
945
|
+
|
|
946
|
+
function UserRow({ user }: { user: PalistorRef<User> }) {
|
|
947
|
+
const u = useForm(user, (s) => s.editUserForm);
|
|
948
|
+
return <span>{u.name.value}</span>;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
type UserEntity = InferEntity<PalistorRef<User>>; // → User
|
|
952
|
+
type UsersList = PalistorList<User>; // типизированный list proxy
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Справочник типов
|
|
956
|
+
|
|
957
|
+
| Тип | Назначение |
|
|
958
|
+
|-----|-----------|
|
|
959
|
+
| `ExtractValues<TConfig>` | Тип значений, выведенный из конфига |
|
|
960
|
+
| `ConfigProxy<TConfig>` | Полный тип прокси — то, что возвращает `useForm(store)` |
|
|
961
|
+
| `PalistorProxy<T>` | Прокси из простого интерфейса значений — для пропсов дочерних компонентов |
|
|
962
|
+
| `PalistorRef<TEntity>` | Непрозрачная ссылка на entity — для пропсов одного элемента |
|
|
963
|
+
| `PalistorList<TEntity>` | Типизированный list proxy |
|
|
964
|
+
| `InferEntity<T>` | Извлечь тип entity из `PalistorRef` |
|
|
965
|
+
| `FieldMapping` / `defineFieldMapping` | Карта переименования пропсов (сохраняет литералы) |
|
|
966
|
+
| `FlowProxyNode<S>` / `FlowStepProxy<C>` / `StepStatus` | Типы flow-прокси |
|
|
967
|
+
| `MaybeComputed<TResult, TValues>` | Константа или `(values) => T` — `isVisible`, `isRequired`, `value` |
|
|
968
|
+
| `MaybeTranslatable<TResult, TValues>` | Константа или `(t, values) => string` — `label`, `placeholder` |
|
|
969
|
+
| `DeepPartialValues<T>` | Глубокий partial значений: `initialValues`, патчи `setter`, `setValues` |
|
|
970
|
+
| `TranslateFn` | Совместим с next-intl `t`, i18next `t`, любым `(...args) => string` |
|
|
971
|
+
| `TemplateConfig<TEntity>` / `ListResolver<TEntity>` | Типизированный шаблон / резолвер списка |
|
|
972
|
+
| `PersistDriver` / `PersistOptions` | Контракты персистенции |
|
|
973
|
+
| `Resolve<T>` / `NotifyFn` / `ResolveErrorContext` | Контракты резолверов |
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
## Лицензия
|
|
978
|
+
|
|
979
|
+
[MIT](./LICENSE) © Yuri Palienko
|