@reformer/core 1.1.0-beta.6 → 1.1.0-beta.8

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 (80) hide show
  1. package/dist/behaviors-BRaiR-UY.js +528 -0
  2. package/dist/behaviors.d.ts +6 -2
  3. package/dist/behaviors.js +18 -227
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +3380 -10
  6. package/dist/validators-DjXtDVoE.js +455 -0
  7. package/dist/validators.d.ts +6 -2
  8. package/dist/validators.js +29 -281
  9. package/package.json +1 -1
  10. package/dist/core/behavior/behavior-applicator.js +0 -92
  11. package/dist/core/behavior/behavior-context.js +0 -43
  12. package/dist/core/behavior/behavior-registry.js +0 -198
  13. package/dist/core/behavior/behaviors/compute-from.js +0 -84
  14. package/dist/core/behavior/behaviors/copy-from.js +0 -64
  15. package/dist/core/behavior/behaviors/enable-when.js +0 -81
  16. package/dist/core/behavior/behaviors/index.js +0 -11
  17. package/dist/core/behavior/behaviors/reset-when.js +0 -63
  18. package/dist/core/behavior/behaviors/revalidate-when.js +0 -51
  19. package/dist/core/behavior/behaviors/sync-fields.js +0 -66
  20. package/dist/core/behavior/behaviors/transform-value.js +0 -110
  21. package/dist/core/behavior/behaviors/watch-field.js +0 -56
  22. package/dist/core/behavior/compose-behavior.js +0 -166
  23. package/dist/core/behavior/create-field-path.js +0 -69
  24. package/dist/core/behavior/index.js +0 -17
  25. package/dist/core/behavior/types.js +0 -7
  26. package/dist/core/context/form-context-impl.js +0 -37
  27. package/dist/core/factories/index.js +0 -6
  28. package/dist/core/factories/node-factory.js +0 -281
  29. package/dist/core/nodes/array-node.js +0 -534
  30. package/dist/core/nodes/field-node.js +0 -510
  31. package/dist/core/nodes/form-node.js +0 -343
  32. package/dist/core/nodes/group-node/field-registry.js +0 -215
  33. package/dist/core/nodes/group-node/index.js +0 -11
  34. package/dist/core/nodes/group-node/proxy-builder.js +0 -161
  35. package/dist/core/nodes/group-node/state-manager.js +0 -265
  36. package/dist/core/nodes/group-node.js +0 -770
  37. package/dist/core/types/deep-schema.js +0 -11
  38. package/dist/core/types/field-path.js +0 -4
  39. package/dist/core/types/form-context.js +0 -25
  40. package/dist/core/types/group-node-proxy.js +0 -31
  41. package/dist/core/types/index.js +0 -4
  42. package/dist/core/types/validation-schema.js +0 -10
  43. package/dist/core/utils/create-form.js +0 -24
  44. package/dist/core/utils/debounce.js +0 -197
  45. package/dist/core/utils/error-handler.js +0 -226
  46. package/dist/core/utils/field-path-navigator.js +0 -374
  47. package/dist/core/utils/index.js +0 -14
  48. package/dist/core/utils/registry-helpers.js +0 -79
  49. package/dist/core/utils/registry-stack.js +0 -86
  50. package/dist/core/utils/resources.js +0 -69
  51. package/dist/core/utils/subscription-manager.js +0 -214
  52. package/dist/core/utils/type-guards.js +0 -169
  53. package/dist/core/validation/core/apply-when.js +0 -41
  54. package/dist/core/validation/core/apply.js +0 -38
  55. package/dist/core/validation/core/index.js +0 -8
  56. package/dist/core/validation/core/validate-async.js +0 -45
  57. package/dist/core/validation/core/validate-tree.js +0 -43
  58. package/dist/core/validation/core/validate.js +0 -38
  59. package/dist/core/validation/field-path.js +0 -147
  60. package/dist/core/validation/index.js +0 -33
  61. package/dist/core/validation/validate-form.js +0 -152
  62. package/dist/core/validation/validation-applicator.js +0 -217
  63. package/dist/core/validation/validation-context.js +0 -75
  64. package/dist/core/validation/validation-registry.js +0 -298
  65. package/dist/core/validation/validators/array-validators.js +0 -86
  66. package/dist/core/validation/validators/date.js +0 -117
  67. package/dist/core/validation/validators/email.js +0 -60
  68. package/dist/core/validation/validators/index.js +0 -14
  69. package/dist/core/validation/validators/max-length.js +0 -60
  70. package/dist/core/validation/validators/max.js +0 -60
  71. package/dist/core/validation/validators/min-length.js +0 -60
  72. package/dist/core/validation/validators/min.js +0 -60
  73. package/dist/core/validation/validators/number.js +0 -90
  74. package/dist/core/validation/validators/pattern.js +0 -62
  75. package/dist/core/validation/validators/phone.js +0 -58
  76. package/dist/core/validation/validators/required.js +0 -69
  77. package/dist/core/validation/validators/url.js +0 -55
  78. package/dist/create-field-path-nXfTtl55.js +0 -283
  79. package/dist/hooks/useFormControl.js +0 -298
  80. package/dist/validation-context-cWXmh_Ho.js +0 -156
@@ -1,283 +1,31 @@
1
- import { e as c, t as l, c as i } from "./validation-context-cWXmh_Ho.js";
2
- import { T as H, V as P, f as W, v as M } from "./validation-context-cWXmh_Ho.js";
3
- import { g as u } from "./registry-helpers-BfCZcMkO.js";
4
- import { V as j } from "./registry-helpers-BfCZcMkO.js";
5
- function n(r, e, a) {
6
- if (!r) return;
7
- const m = c(r);
8
- u().registerSync(m, e, a);
9
- }
10
- function A(r, e, a) {
11
- const m = c(r);
12
- u().registerAsync(m, e, a);
13
- }
14
- function _(r, e) {
15
- u().registerTree(r, e);
16
- }
17
- function $(r, e) {
18
- if (!Array.isArray(r) && !Array.isArray(e) && r && !("__key" in r) && !("__path" in r)) {
19
- e(r);
20
- return;
21
- }
22
- const a = (Array.isArray(r) ? r : [r]).filter(
23
- Boolean
24
- ), m = Array.isArray(e) ? e : [e];
25
- for (const s of a) {
26
- const t = l(s);
27
- for (const g of m)
28
- g(t);
29
- }
30
- }
31
- function o(r, e, a) {
32
- const m = c(r);
33
- u().enterCondition(m, e);
34
- try {
35
- const s = i();
36
- a(s);
37
- } finally {
38
- u().exitCondition();
39
- }
40
- }
41
- function w(r, e) {
42
- r && n(r, (a) => a == null || a === "" ? {
43
- code: "required",
44
- message: e?.message || "Поле обязательно для заполнения",
45
- params: e?.params
46
- } : typeof a == "boolean" && a !== !0 ? {
47
- code: "required",
48
- message: e?.message || "Поле обязательно для заполнения",
49
- params: e?.params
50
- } : null);
51
- }
52
- function D(r, e, a) {
53
- r && n(r, (m) => m == null ? null : m < e ? {
54
- code: "min",
55
- message: a?.message || `Минимальное значение: ${e}`,
56
- params: { min: e, actual: m, ...a?.params }
57
- } : null);
58
- }
59
- function h(r, e, a) {
60
- r && n(r, (m) => m == null ? null : m > e ? {
61
- code: "max",
62
- message: a?.message || `Максимальное значение: ${e}`,
63
- params: { max: e, actual: m, ...a?.params }
64
- } : null);
65
- }
66
- function d(r, e, a) {
67
- r && n(r, (m) => m && m.length < e ? {
68
- code: "minLength",
69
- message: a?.message || `Минимальная длина: ${e} символов`,
70
- params: { minLength: e, actualLength: m.length, ...a?.params }
71
- } : null);
72
- }
73
- function b(r, e, a) {
74
- r && n(r, (m) => m && m.length > e ? {
75
- code: "maxLength",
76
- message: a?.message || `Максимальная длина: ${e} символов`,
77
- params: { maxLength: e, actualLength: m.length, ...a?.params }
78
- } : null);
79
- }
80
- function L(r, e) {
81
- if (!r) return;
82
- const a = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
83
- n(r, (m) => m ? a.test(m) ? null : {
84
- code: "email",
85
- message: e?.message || "Неверный формат email",
86
- params: e?.params
87
- } : null);
88
- }
89
- function R(r, e, a) {
90
- r && n(r, (m) => m ? e.test(m) ? null : {
91
- code: "pattern",
92
- message: a?.message || "Значение не соответствует требуемому формату",
93
- params: { pattern: e.source, ...a?.params }
94
- } : null);
95
- }
96
- function T(r, e) {
97
- if (!r) return;
98
- const a = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i, m = /^https?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/i;
99
- n(r, (s) => s ? (e?.requireProtocol ? m : a).test(s) ? e?.allowedProtocols && e.allowedProtocols.length > 0 && !e.allowedProtocols.some(
100
- (f) => s.toLowerCase().startsWith(`${f}://`)
101
- ) ? {
102
- code: "url_protocol",
103
- message: e?.message || `URL должен использовать один из протоколов: ${e.allowedProtocols.join(", ")}`,
104
- params: { allowedProtocols: e.allowedProtocols, ...e?.params }
105
- } : null : {
106
- code: "url",
107
- message: e?.message || "Неверный формат URL",
108
- params: e?.params
109
- } : null);
110
- }
111
- function C(r, e) {
112
- if (!r) return;
113
- const a = e?.format || "any", m = {
114
- // Международный формат: +1234567890 или +1 234 567 8900
115
- international: /^\+?[1-9]\d{1,14}$/,
116
- // Российский формат: +7 (XXX) XXX-XX-XX, 8 (XXX) XXX-XX-XX, и вариации
117
- ru: /^(\+7|7|8)?[\s\-]?\(?[489][0-9]{2}\)?[\s\-]?[0-9]{3}[\s\-]?[0-9]{2}[\s\-]?[0-9]{2}$/,
118
- // US формат: (123) 456-7890, 123-456-7890, 1234567890
119
- us: /^(\+?1)?[\s\-]?\(?[2-9]\d{2}\)?[\s\-]?\d{3}[\s\-]?\d{4}$/,
120
- // Любой формат: минимум 10 цифр с возможными разделителями
121
- any: /^[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,9}$/
122
- };
123
- n(r, (s) => {
124
- if (!s)
125
- return null;
126
- if (!m[a].test(s)) {
127
- const g = {
128
- international: "Введите телефон в международном формате (например, +1234567890)",
129
- ru: "Введите российский номер телефона (например, +7 900 123-45-67)",
130
- us: "Введите американский номер телефона (например, (123) 456-7890)",
131
- any: "Неверный формат телефона"
132
- };
133
- return {
134
- code: "phone",
135
- message: e?.message || g[a],
136
- params: { format: a, ...e?.params }
137
- };
138
- }
139
- return null;
140
- });
141
- }
142
- function N(r, e) {
143
- r && n(r, (a) => a == null ? null : typeof a != "number" || isNaN(a) ? {
144
- code: "number",
145
- message: e?.message || "Значение должно быть числом",
146
- params: e?.params
147
- } : e?.integer && !Number.isInteger(a) ? {
148
- code: "number_integer",
149
- message: e?.message || "Значение должно быть целым числом",
150
- params: e?.params
151
- } : e?.min !== void 0 && a < e.min ? {
152
- code: "number_min",
153
- message: e?.message || `Значение должно быть не менее ${e.min}`,
154
- params: { min: e.min, ...e?.params }
155
- } : e?.max !== void 0 && a > e.max ? {
156
- code: "number_max",
157
- message: e?.message || `Значение должно быть не более ${e.max}`,
158
- params: { max: e.max, ...e?.params }
159
- } : e?.multipleOf !== void 0 && a % e.multipleOf !== 0 ? {
160
- code: "number_multiple",
161
- message: e?.message || `Значение должно быть кратно ${e.multipleOf}`,
162
- params: { multipleOf: e.multipleOf, ...e?.params }
163
- } : e?.allowNegative === !1 && a < 0 ? {
164
- code: "number_negative",
165
- message: e?.message || "Отрицательные числа не допускаются",
166
- params: e?.params
167
- } : e?.allowZero === !1 && a === 0 ? {
168
- code: "number_zero",
169
- message: e?.message || "Ноль не допускается",
170
- params: e?.params
171
- } : null);
172
- }
173
- function q(r, e) {
174
- r && n(r, (a) => {
175
- if (!a)
176
- return null;
177
- let m;
178
- if (a instanceof Date)
179
- m = a;
180
- else if (typeof a == "string")
181
- m = new Date(a);
182
- else
183
- return {
184
- code: "date_invalid",
185
- message: e?.message || "Неверный формат даты",
186
- params: e?.params
187
- };
188
- if (isNaN(m.getTime()))
189
- return {
190
- code: "date_invalid",
191
- message: e?.message || "Неверный формат даты",
192
- params: e?.params
193
- };
194
- const s = /* @__PURE__ */ new Date();
195
- if (s.setHours(0, 0, 0, 0), e?.minDate) {
196
- const t = new Date(e.minDate);
197
- if (t.setHours(0, 0, 0, 0), m < t)
198
- return {
199
- code: "date_min",
200
- message: e?.message || `Дата должна быть не ранее ${t.toLocaleDateString()}`,
201
- params: { minDate: e.minDate, ...e?.params }
202
- };
203
- }
204
- if (e?.maxDate) {
205
- const t = new Date(e.maxDate);
206
- if (t.setHours(0, 0, 0, 0), m > t)
207
- return {
208
- code: "date_max",
209
- message: e?.message || `Дата должна быть не позднее ${t.toLocaleDateString()}`,
210
- params: { maxDate: e.maxDate, ...e?.params }
211
- };
212
- }
213
- if (e?.noFuture && m > s)
214
- return {
215
- code: "date_future",
216
- message: e?.message || "Дата не может быть в будущем",
217
- params: e?.params
218
- };
219
- if (e?.noPast && m < s)
220
- return {
221
- code: "date_past",
222
- message: e?.message || "Дата не может быть в прошлом",
223
- params: e?.params
224
- };
225
- if (e?.minAge !== void 0 || e?.maxAge !== void 0) {
226
- const t = Math.floor(
227
- (s.getTime() - m.getTime()) / 315576e5
228
- );
229
- if (e?.minAge !== void 0 && t < e.minAge)
230
- return {
231
- code: "date_min_age",
232
- message: e?.message || `Минимальный возраст: ${e.minAge} лет`,
233
- params: { minAge: e.minAge, currentAge: t, ...e?.params }
234
- };
235
- if (e?.maxAge !== void 0 && t > e.maxAge)
236
- return {
237
- code: "date_max_age",
238
- message: e?.message || `Максимальный возраст: ${e.maxAge} лет`,
239
- params: { maxAge: e.maxAge, currentAge: t, ...e?.params }
240
- };
241
- }
242
- return null;
243
- });
244
- }
245
- function z(r, e) {
246
- r && d(r, 1, {
247
- message: e?.message || "Массив не должен быть пустым",
248
- params: { minLength: 1, ...e?.params }
249
- });
250
- }
251
- function I(r, e) {
252
- if (!r) return;
253
- const a = c(r);
254
- u().registerArrayItemValidation(a, e);
255
- }
1
+ import { T as e, V as s, k as i, l, c as r, x as n, a as m, q as o, B as d, A as p, n as x, p as h, m as y, o as v, y as c, w as u, s as V, t as f, r as g, C, u as F, f as I, h as P, v as T, z as q, j as A } from "./validators-DjXtDVoE.js";
2
+ import { V as b } from "./registry-helpers-BfCZcMkO.js";
256
3
  export {
257
- H as TreeValidationContextImpl,
258
- P as ValidationContextImpl,
259
- j as ValidationRegistry,
260
- $ as apply,
261
- o as applyWhen,
262
- i as createFieldPath,
263
- q as date,
264
- L as email,
265
- W as extractKey,
266
- c as extractPath,
267
- h as max,
268
- b as maxLength,
269
- D as min,
270
- d as minLength,
271
- z as notEmpty,
272
- N as number,
273
- R as pattern,
274
- C as phone,
275
- w as required,
276
- l as toFieldPath,
277
- T as url,
278
- n as validate,
279
- A as validateAsync,
280
- M as validateForm,
281
- I as validateItems,
282
- _ as validateTree
4
+ e as TreeValidationContextImpl,
5
+ s as ValidationContextImpl,
6
+ b as ValidationRegistry,
7
+ i as apply,
8
+ l as applyWhen,
9
+ r as createFieldPath,
10
+ n as date,
11
+ m as default,
12
+ o as email,
13
+ d as extractKey,
14
+ p as extractPath,
15
+ x as max,
16
+ h as maxLength,
17
+ y as min,
18
+ v as minLength,
19
+ c as notEmpty,
20
+ u as number,
21
+ V as pattern,
22
+ f as phone,
23
+ g as required,
24
+ C as toFieldPath,
25
+ F as url,
26
+ I as validate,
27
+ P as validateAsync,
28
+ T as validateForm,
29
+ q as validateItems,
30
+ A as validateTree
283
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reformer/core",
3
- "version": "1.1.0-beta.6",
3
+ "version": "1.1.0-beta.8",
4
4
  "description": "Reactive form state management library for React with signals-based architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,92 +0,0 @@
1
- /**
2
- * Применение behavior схемы к форме
3
- *
4
- * Извлечено из GroupNode для соблюдения SRP (Single Responsibility Principle).
5
- * Управляет процессом регистрации и применения behaviors.
6
- *
7
- * @template T Тип формы
8
- *
9
- * @example
10
- * ```typescript
11
- * class GroupNode {
12
- * private readonly behaviorApplicator = new BehaviorApplicator(this);
13
- *
14
- * applyBehaviorSchema(schemaFn: BehaviorSchemaFn<T>): () => void {
15
- * return this.behaviorApplicator.apply(schemaFn);
16
- * }
17
- * }
18
- * ```
19
- */
20
- import { createFieldPath as createBehaviorFieldPath } from './create-field-path';
21
- import { FormErrorHandler, ErrorStrategy } from '../utils/error-handler';
22
- /**
23
- * Класс для применения behavior схемы к форме
24
- *
25
- * Выполняет:
26
- * 1. Начало регистрации behaviors (beginRegistration)
27
- * 2. Выполнение схемы (регистрация behaviors)
28
- * 3. Завершение регистрации (endRegistration) - применение behaviors
29
- * 4. Возврат функции cleanup для отписки
30
- *
31
- * @template T Тип формы (объект)
32
- */
33
- export class BehaviorApplicator {
34
- form;
35
- behaviorRegistry;
36
- constructor(form, behaviorRegistry) {
37
- this.form = form;
38
- this.behaviorRegistry = behaviorRegistry;
39
- }
40
- /**
41
- * Применить behavior схему к форме
42
- *
43
- * Этапы:
44
- * 1. Начать регистрацию (beginRegistration)
45
- * 2. Выполнить схему (регистрация behaviors)
46
- * 3. Завершить регистрацию (endRegistration) - применить behaviors
47
- * 4. Вернуть функцию cleanup для отписки
48
- *
49
- * @param schemaFn Функция-схема behavior
50
- * @returns Функция отписки от всех behaviors
51
- *
52
- * @example
53
- * ```typescript
54
- * const cleanup = behaviorApplicator.apply((path) => {
55
- * copyFrom(path.residenceAddress, path.registrationAddress, {
56
- * when: (form) => form.sameAsRegistration === true
57
- * });
58
- *
59
- * enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
60
- *
61
- * computeFrom(
62
- * path.initialPayment,
63
- * [path.propertyValue],
64
- * (propertyValue) => propertyValue ? propertyValue * 0.2 : null
65
- * );
66
- * });
67
- *
68
- * // Cleanup при unmount
69
- * useEffect(() => cleanup, []);
70
- * ```
71
- */
72
- apply(schemaFn) {
73
- this.behaviorRegistry.beginRegistration();
74
- try {
75
- // 1. Создать field path для type-safe доступа к полям
76
- const path = createBehaviorFieldPath();
77
- // 2. Выполнить схему (регистрация behaviors)
78
- schemaFn(path);
79
- // 3. Завершить регистрацию и применить behaviors
80
- // Используем публичный метод getProxy() для получения proxy-инстанса
81
- const formToUse = this.form.getProxy();
82
- const result = this.behaviorRegistry.endRegistration(formToUse);
83
- // 4. Вернуть функцию cleanup
84
- return result.cleanup;
85
- }
86
- catch (error) {
87
- FormErrorHandler.handle(error, 'BehaviorApplicator', ErrorStrategy.THROW);
88
- // TypeScript требует return, но код никогда не дойдет сюда
89
- throw error;
90
- }
91
- }
92
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * BehaviorContext - контекст для behavior callback функций
3
- *
4
- * Реализует FormContext для behavior схем
5
- */
6
- /**
7
- * Реализация BehaviorContext (FormContext)
8
- *
9
- * Предоставляет:
10
- * - `form` - прямой типизированный доступ к форме
11
- * - `setFieldValue` - безопасная установка значения (emitEvent: false)
12
- */
13
- export class BehaviorContextImpl {
14
- /**
15
- * Форма с типизированным Proxy-доступом к полям
16
- */
17
- form;
18
- _form;
19
- constructor(form) {
20
- this._form = form;
21
- // Используем _proxyInstance если доступен, иначе fallback на form
22
- const proxy = (form
23
- ._proxyInstance || form);
24
- this.form = proxy;
25
- }
26
- /**
27
- * Безопасно установить значение поля по строковому пути или FieldPath
28
- *
29
- * Автоматически использует emitEvent: false для предотвращения циклов
30
- *
31
- * @param path - Строковый путь к полю или FieldPath объект
32
- * @param value - Новое значение
33
- */
34
- setFieldValue(path, value) {
35
- // Преобразуем FieldPath в строку если необходимо
36
- const pathStr = typeof path === 'string' ? path : path.toString();
37
- const node = this._form.getFieldByPath(pathStr);
38
- if (node) {
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- node.setValue(value, { emitEvent: false });
41
- }
42
- }
43
- }
@@ -1,198 +0,0 @@
1
- /**
2
- * BehaviorRegistry - регистрация и управление behavior схемами
3
- *
4
- * Аналогично ValidationRegistry, но для реактивного поведения форм
5
- */
6
- import { BehaviorContextImpl } from './behavior-context';
7
- import { RegistryStack } from '../utils/registry-stack';
8
- /**
9
- * Реестр behaviors для формы
10
- *
11
- * Каждый экземпляр GroupNode создает собственный реестр (композиция).
12
- * Устраняет race conditions и изолирует формы друг от друга.
13
- *
14
- * Context stack используется для tracking текущего активного реестра:
15
- * - beginRegistration() помещает this в stack
16
- * - endRegistration() извлекает из stack
17
- * - getCurrent() возвращает текущий активный реестр
18
- *
19
- * @example
20
- * ```typescript
21
- * class GroupNode {
22
- * private readonly behaviorRegistry = new BehaviorRegistry();
23
- *
24
- * applyBehaviorSchema(schemaFn) {
25
- * this.behaviorRegistry.beginRegistration(); // Pushes this to stack
26
- * schemaFn(createBehaviorFieldPath(this)); // Uses getCurrent()
27
- * return this.behaviorRegistry.endRegistration(this); // Pops from stack
28
- * }
29
- * }
30
- * ```
31
- */
32
- export class BehaviorRegistry {
33
- /**
34
- * Stack активных контекстов регистрации
35
- * Используется для изоляции форм друг от друга
36
- */
37
- static contextStack = new RegistryStack();
38
- registrations = [];
39
- isRegistering = false;
40
- /**
41
- * Получить текущий активный реестр из context stack
42
- *
43
- * @returns Текущий активный реестр или null
44
- *
45
- * @example
46
- * ```typescript
47
- * // В schema-behaviors.ts
48
- * export function copyFrom(...) {
49
- * const registry = BehaviorRegistry.getCurrent();
50
- * if (registry) {
51
- * registry.register({ ... });
52
- * }
53
- * }
54
- * ```
55
- */
56
- static getCurrent() {
57
- return BehaviorRegistry.contextStack.getCurrent();
58
- }
59
- /**
60
- * Начать регистрацию behaviors
61
- * Вызывается перед применением схемы
62
- *
63
- * Помещает this в context stack для изоляции форм
64
- */
65
- beginRegistration() {
66
- this.isRegistering = true;
67
- this.registrations = [];
68
- // Помещаем this в stack для tracking текущего активного реестра
69
- BehaviorRegistry.contextStack.push(this);
70
- }
71
- /**
72
- * Зарегистрировать behavior handler
73
- * Вызывается функциями из schema-behaviors.ts
74
- *
75
- * @param handler - BehaviorHandlerFn функция
76
- * @param options - Опции behavior (debounce)
77
- *
78
- * @example
79
- * ```typescript
80
- * const handler = createCopyBehavior(target, source, { when: ... });
81
- * registry.register(handler, { debounce: 300 });
82
- * ```
83
- */
84
- register(handler, options) {
85
- if (!this.isRegistering) {
86
- if (import.meta.env.DEV) {
87
- throw new Error('BehaviorRegistry: call beginRegistration() before registering behaviors');
88
- }
89
- return;
90
- }
91
- this.registrations.push({
92
- // Type assertion безопасен: handler будет вызван с правильным типом формы в createEffect
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
- handler: handler,
95
- debounce: options?.debounce,
96
- });
97
- }
98
- /**
99
- * Завершить регистрацию и применить behaviors к форме
100
- * Создает effect подписки для всех зарегистрированных behaviors
101
- *
102
- * Извлекает this из context stack
103
- *
104
- * @param form - GroupNode формы
105
- * @returns Количество зарегистрированных behaviors и функция cleanup
106
- */
107
- endRegistration(form) {
108
- this.isRegistering = false;
109
- // Извлекаем из stack с проверкой
110
- BehaviorRegistry.contextStack.verify(this, 'BehaviorRegistry');
111
- const context = new BehaviorContextImpl(form);
112
- const disposeCallbacks = [];
113
- // Создаем effect подписки для каждого behavior
114
- for (const registered of this.registrations) {
115
- const dispose = this.createEffect(registered, form, context);
116
- if (dispose) {
117
- disposeCallbacks.push(dispose);
118
- }
119
- }
120
- // Функция cleanup для отписки от всех effects
121
- const cleanup = () => {
122
- disposeCallbacks.forEach((dispose) => dispose());
123
- };
124
- return {
125
- count: this.registrations.length,
126
- cleanup,
127
- };
128
- }
129
- /**
130
- * Создать effect подписку для behavior
131
- * @private
132
- */
133
- createEffect(registered, form, context) {
134
- const { handler, debounce: debounceMs = 0 } = registered;
135
- let debounceTimer = null;
136
- // Обертка для debounce
137
- const withDebounce = (callback) => {
138
- if (debounceMs > 0) {
139
- if (debounceTimer)
140
- clearTimeout(debounceTimer);
141
- debounceTimer = setTimeout(callback, debounceMs);
142
- }
143
- else {
144
- callback();
145
- }
146
- };
147
- // Cleanup функция для debounce таймера
148
- const cleanupDebounce = () => {
149
- if (debounceTimer) {
150
- clearTimeout(debounceTimer);
151
- debounceTimer = null;
152
- }
153
- };
154
- // Вызываем handler напрямую
155
- // Type assertion необходим из-за contravariance: handler хранится как
156
- // BehaviorHandlerFn<FormFields>, но вызывается с более специфичным типом T.
157
- // Используем any для обхода ограничений TypeScript при хранении generic handlers в массиве.
158
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
- const effectDispose = handler(form, context, withDebounce);
160
- if (!effectDispose) {
161
- return null;
162
- }
163
- // Возвращаем комбинированный cleanup
164
- // который очищает и effect, и debounce таймер
165
- return () => {
166
- cleanupDebounce();
167
- if (effectDispose) {
168
- effectDispose();
169
- }
170
- };
171
- }
172
- }
173
- // ============================================================================
174
- // Глобальный экземпляр BehaviorRegistry УДАЛЕН
175
- // ============================================================================
176
- //
177
- // Ранее здесь был глобальный Singleton экземпляр BehaviorRegistry,
178
- // который создавал race conditions и нарушал изоляцию форм.
179
- //
180
- // Теперь каждый GroupNode создает собственный экземпляр BehaviorRegistry:
181
- //
182
- // @example
183
- // ```typescript
184
- // class GroupNode {
185
- // private readonly behaviorRegistry = new BehaviorRegistry();
186
- //
187
- // applyBehaviorSchema(schemaFn) {
188
- // this.behaviorRegistry.beginRegistration();
189
- // schemaFn(createBehaviorFieldPath(this));
190
- // return this.behaviorRegistry.endRegistration(this);
191
- // }
192
- // }
193
- // ```
194
- //
195
- // Это обеспечивает:
196
- // - Полную изоляцию форм друг от друга
197
- // - Отсутствие race conditions при параллельной регистрации
198
- // - Возможность применять разные behavior схемы к разным формам одновременно