@blueprint-ts/core 4.1.0-beta.4 → 4.1.0-beta.5

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.
@@ -1,6 +1,7 @@
1
1
  import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver';
2
2
  import { PropertyAwareArray, type PropertyAwareField } from './PropertyAwareArray';
3
3
  import { PropertyAwareObject } from './PropertyAwareObject';
4
+ import { type PersistenceDebugEvent, type PersistenceRestorePolicy } from './persistence/types';
4
5
  import { type ValidationGroups, type ValidationRules } from './validation';
5
6
  type ErrorMessages = string[];
6
7
  interface ErrorObject {
@@ -82,6 +83,9 @@ export declare abstract class BaseForm<RequestBody extends object, FormBody exte
82
83
  * Child classes can override this method to return a different driver.
83
84
  */
84
85
  protected getPersistenceDriver(_suffix: string | undefined): PersistenceDriver;
86
+ protected getPersistenceRestorePolicy(): PersistenceRestorePolicy<FormBody>;
87
+ protected shouldLogPersistenceDebug(): boolean;
88
+ protected logPersistenceDebug(event: PersistenceDebugEvent<FormBody>): void;
85
89
  /**
86
90
  * Helper: recursively computes the dirty state for a value based on the original.
87
91
  * For plain arrays we compare the entire array (a single flag), not each element.
@@ -12,6 +12,7 @@ import { camelCase, upperFirst, cloneDeep, debounce, isEqual } from 'lodash-es';
12
12
  import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver';
13
13
  import { PropertyAwareArray } from './PropertyAwareArray';
14
14
  import { PropertyAwareObject, PROPERTY_AWARE_OBJECT_MARKER } from './PropertyAwareObject';
15
+ import { StrictPersistenceRestorePolicy } from './persistence/StrictPersistenceRestorePolicy';
15
16
  import { BaseRule } from './validation/rules/BaseRule';
16
17
  import { ValidationMode } from './validation';
17
18
  function isRecord(value) {
@@ -140,39 +141,6 @@ function restorePropertyAwareStructure(defaults, value) {
140
141
  }
141
142
  return restoreSerializedPropertyAwareValue(value);
142
143
  }
143
- function normalizePropertyAwareEqualityValue(value) {
144
- if (value instanceof PropertyAwareArray) {
145
- return Array.from(value, (item) => normalizePropertyAwareEqualityValue(item));
146
- }
147
- if (isPropertyAwareObject(value) || isSerializedPropertyAwareObject(value)) {
148
- const normalized = {};
149
- for (const [key, child] of Object.entries(value)) {
150
- if (key === PROPERTY_AWARE_OBJECT_MARKER) {
151
- continue;
152
- }
153
- normalized[key] = normalizePropertyAwareEqualityValue(child);
154
- }
155
- return normalized;
156
- }
157
- if (Array.isArray(value)) {
158
- return value.map((item) => normalizePropertyAwareEqualityValue(item));
159
- }
160
- if (isRecord(value)) {
161
- const normalized = {};
162
- for (const [key, child] of Object.entries(value)) {
163
- normalized[key] = normalizePropertyAwareEqualityValue(child);
164
- }
165
- return normalized;
166
- }
167
- return value;
168
- }
169
- /**
170
- * Compare values while ignoring persistence-only markers on property-aware structures.
171
- * This avoids false negatives when comparing persisted state to defaults.
172
- */
173
- function propertyAwareDeepEqual(a, b) {
174
- return isEqual(normalizePropertyAwareEqualityValue(a), normalizePropertyAwareEqualityValue(b));
175
- }
176
144
  /**
177
145
  * A generic base class for forms.
178
146
  *
@@ -191,6 +159,20 @@ export class BaseForm {
191
159
  getPersistenceDriver(_suffix) {
192
160
  return new NonPersistentDriver();
193
161
  }
162
+ getPersistenceRestorePolicy() {
163
+ return new StrictPersistenceRestorePolicy();
164
+ }
165
+ shouldLogPersistenceDebug() {
166
+ return false;
167
+ }
168
+ logPersistenceDebug(event) {
169
+ if (!this.shouldLogPersistenceDebug()) {
170
+ return;
171
+ }
172
+ const suffixLabel = event.persistSuffix ? ` (${event.persistSuffix})` : '';
173
+ const details = event.details ? ` ${JSON.stringify(event.details)}` : '';
174
+ console.debug(`[BaseForm persistence] ${event.formName}${suffixLabel}: ${event.action} (${event.reason})${details}`);
175
+ }
194
176
  /**
195
177
  * Helper: recursively computes the dirty state for a value based on the original.
196
178
  * For plain arrays we compare the entire array (a single flag), not each element.
@@ -324,6 +306,7 @@ export class BaseForm {
324
306
  }
325
307
  }
326
308
  constructor(defaults, options) {
309
+ var _a;
327
310
  this.options = options;
328
311
  this._errors = reactive({});
329
312
  this._asyncErrors = reactive({});
@@ -342,21 +325,35 @@ export class BaseForm {
342
325
  let initialData;
343
326
  const driver = this.getPersistenceDriver(options === null || options === void 0 ? void 0 : options.persistSuffix);
344
327
  if (persist) {
345
- const persisted = driver.get(this.constructor.name);
346
- if (persisted && propertyAwareDeepEqual(defaults, persisted.original)) {
347
- initialData = restorePropertyAwareStructure(defaults, persisted.state);
348
- this.original = restorePropertyAwareStructure(defaults, cloneDeep(persisted.original));
349
- this.dirty = reactive(persisted.dirty);
350
- this.touched = reactive(persisted.touched || {});
328
+ const persisted = (_a = driver.get(this.constructor.name)) !== null && _a !== void 0 ? _a : null;
329
+ const restoreDecision = this.getPersistenceRestorePolicy().resolve({
330
+ formName: this.constructor.name,
331
+ persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
332
+ defaults,
333
+ persisted
334
+ });
335
+ this.logPersistenceDebug({
336
+ formName: this.constructor.name,
337
+ persistSuffix: options === null || options === void 0 ? void 0 : options.persistSuffix,
338
+ action: restoreDecision.action,
339
+ reason: restoreDecision.reason,
340
+ details: restoreDecision.details
341
+ });
342
+ if (restoreDecision.action === 'restore' && restoreDecision.persisted) {
343
+ initialData = restorePropertyAwareStructure(defaults, restoreDecision.persisted.state);
344
+ this.original = restorePropertyAwareStructure(defaults, cloneDeep(restoreDecision.persisted.original));
345
+ this.dirty = reactive(restoreDecision.persisted.dirty);
346
+ this.touched = reactive(restoreDecision.persisted.touched || {});
351
347
  }
352
348
  else {
353
- console.log('Discarding persisted data for ' + this.constructor.name + " because it doesn't match the defaults.");
354
349
  initialData = defaults;
355
350
  this.original = restorePropertyAwareStructure(defaults, cloneDeep(defaults));
356
351
  const init = this.initDirtyTouched(defaults);
357
352
  this.dirty = init.dirty;
358
353
  this.touched = init.touched;
359
- driver.remove(this.constructor.name);
354
+ if (restoreDecision.action === 'discard') {
355
+ driver.remove(this.constructor.name);
356
+ }
360
357
  }
361
358
  }
362
359
  else {
@@ -379,7 +376,7 @@ export class BaseForm {
379
376
  set: (newVal) => {
380
377
  const next = Array.isArray(newVal) ? Array.from(newVal) : [];
381
378
  this.replacePropertyAwareArray(key, next);
382
- this.dirty[key] = next.map(() => false);
379
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
383
380
  this.markFieldUpdated(key, driver);
384
381
  }
385
382
  });
@@ -1193,7 +1190,7 @@ export class BaseForm {
1193
1190
  if (currentVal instanceof PropertyAwareArray) {
1194
1191
  const values = newVal instanceof PropertyAwareArray || Array.isArray(newVal) ? Array.from(newVal) : [];
1195
1192
  this.replacePropertyAwareArray(key, values);
1196
- this.dirty[key] = values.map(() => false);
1193
+ this.dirty[key] = this.computeDirtyState(this.state[key], this.original[key]);
1197
1194
  this.touched[key] = true;
1198
1195
  continue;
1199
1196
  }
@@ -6,5 +6,7 @@ import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDri
6
6
  import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver';
7
7
  import { PropertyAwareArray, type PropertyAwareField, type PropertyAware } from './PropertyAwareArray';
8
8
  import { PropertyAwareObject } from './PropertyAwareObject';
9
- export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver };
10
- export type { PersistedForm, PersistenceDriver, PropertyAwareField, PropertyAware };
9
+ import { StrictPersistenceRestorePolicy } from './persistence';
10
+ import type { PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult } from './persistence';
11
+ export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, StrictPersistenceRestorePolicy };
12
+ export type { PersistedForm, PersistenceDriver, PropertyAwareField, PropertyAware, PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult };
@@ -4,4 +4,5 @@ import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDrive
4
4
  import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDriver';
5
5
  import { PropertyAwareArray } from './PropertyAwareArray';
6
6
  import { PropertyAwareObject } from './PropertyAwareObject';
7
- export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver };
7
+ import { StrictPersistenceRestorePolicy } from './persistence';
8
+ export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, StrictPersistenceRestorePolicy };
@@ -0,0 +1,4 @@
1
+ import { type PersistenceRestoreContext, type PersistenceRestorePolicy, type PersistenceRestoreResult } from './types';
2
+ export declare class StrictPersistenceRestorePolicy<FormBody extends object> implements PersistenceRestorePolicy<FormBody> {
3
+ resolve(context: PersistenceRestoreContext<FormBody>): PersistenceRestoreResult<FormBody>;
4
+ }
@@ -0,0 +1,42 @@
1
+ import { collectPropertyAwareMismatchPaths, propertyAwareDeepEqual } from './utils';
2
+ function isRecord(value) {
3
+ return !!value && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ function isPersistedFormLike(value) {
6
+ if (!isRecord(value)) {
7
+ return false;
8
+ }
9
+ return 'state' in value && 'original' in value && 'dirty' in value;
10
+ }
11
+ export class StrictPersistenceRestorePolicy {
12
+ resolve(context) {
13
+ const { defaults, persisted } = context;
14
+ if (persisted === null) {
15
+ return {
16
+ action: 'ignore',
17
+ reason: 'no_persisted_state'
18
+ };
19
+ }
20
+ if (!isPersistedFormLike(persisted)) {
21
+ return {
22
+ action: 'discard',
23
+ reason: 'invalid_persisted_state'
24
+ };
25
+ }
26
+ if (propertyAwareDeepEqual(defaults, persisted.original)) {
27
+ return {
28
+ action: 'restore',
29
+ reason: 'defaults_match',
30
+ persisted
31
+ };
32
+ }
33
+ return {
34
+ action: 'discard',
35
+ reason: 'defaults_mismatch',
36
+ persisted,
37
+ details: {
38
+ mismatchPaths: collectPropertyAwareMismatchPaths(defaults, persisted.original)
39
+ }
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ import { StrictPersistenceRestorePolicy } from './StrictPersistenceRestorePolicy';
2
+ export { StrictPersistenceRestorePolicy };
3
+ export type { PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult } from './types';
@@ -0,0 +1,2 @@
1
+ import { StrictPersistenceRestorePolicy } from './StrictPersistenceRestorePolicy';
2
+ export { StrictPersistenceRestorePolicy };
@@ -0,0 +1,23 @@
1
+ import { type PersistedForm } from '../types/PersistedForm';
2
+ export interface PersistenceRestoreContext<FormBody extends object> {
3
+ formName: string;
4
+ persistSuffix?: string | undefined;
5
+ defaults: FormBody;
6
+ persisted: PersistedForm<FormBody> | null;
7
+ }
8
+ export interface PersistenceRestoreResult<FormBody extends object> {
9
+ action: 'restore' | 'discard' | 'ignore';
10
+ reason: string;
11
+ persisted?: PersistedForm<FormBody> | undefined;
12
+ details?: Record<string, unknown> | undefined;
13
+ }
14
+ export interface PersistenceDebugEvent<FormBody extends object> {
15
+ formName: string;
16
+ persistSuffix?: string | undefined;
17
+ action: PersistenceRestoreResult<FormBody>['action'];
18
+ reason: string;
19
+ details?: Record<string, unknown> | undefined;
20
+ }
21
+ export interface PersistenceRestorePolicy<FormBody extends object> {
22
+ resolve(context: PersistenceRestoreContext<FormBody>): PersistenceRestoreResult<FormBody>;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function propertyAwareDeepEqual<T>(a: T, b: T): boolean;
2
+ export declare function collectPropertyAwareMismatchPaths(a: unknown, b: unknown, path?: string, maxPaths?: number): string[];
@@ -0,0 +1,77 @@
1
+ import { isEqual } from 'lodash-es';
2
+ import { PropertyAwareArray } from '../PropertyAwareArray';
3
+ import { PropertyAwareObject, PROPERTY_AWARE_OBJECT_MARKER } from '../PropertyAwareObject';
4
+ function isRecord(value) {
5
+ return !!value && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ function isPropertyAwareObject(value) {
8
+ return value instanceof PropertyAwareObject;
9
+ }
10
+ function isSerializedPropertyAwareObject(value) {
11
+ return isRecord(value) && value[PROPERTY_AWARE_OBJECT_MARKER] === true;
12
+ }
13
+ function normalizePropertyAwareEqualityValue(value) {
14
+ if (value instanceof PropertyAwareArray) {
15
+ return Array.from(value, (item) => normalizePropertyAwareEqualityValue(item));
16
+ }
17
+ if (isPropertyAwareObject(value) || isSerializedPropertyAwareObject(value)) {
18
+ const normalized = {};
19
+ for (const [key, child] of Object.entries(value)) {
20
+ if (key === PROPERTY_AWARE_OBJECT_MARKER) {
21
+ continue;
22
+ }
23
+ normalized[key] = normalizePropertyAwareEqualityValue(child);
24
+ }
25
+ return normalized;
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map((item) => normalizePropertyAwareEqualityValue(item));
29
+ }
30
+ if (isRecord(value)) {
31
+ const normalized = {};
32
+ for (const [key, child] of Object.entries(value)) {
33
+ normalized[key] = normalizePropertyAwareEqualityValue(child);
34
+ }
35
+ return normalized;
36
+ }
37
+ return value;
38
+ }
39
+ export function propertyAwareDeepEqual(a, b) {
40
+ return isEqual(normalizePropertyAwareEqualityValue(a), normalizePropertyAwareEqualityValue(b));
41
+ }
42
+ export function collectPropertyAwareMismatchPaths(a, b, path = '', maxPaths = 10) {
43
+ const mismatches = [];
44
+ const visit = (left, right, currentPath) => {
45
+ if (mismatches.length >= maxPaths) {
46
+ return;
47
+ }
48
+ const normalizedLeft = normalizePropertyAwareEqualityValue(left);
49
+ const normalizedRight = normalizePropertyAwareEqualityValue(right);
50
+ if (isEqual(normalizedLeft, normalizedRight)) {
51
+ return;
52
+ }
53
+ if (Array.isArray(normalizedLeft) && Array.isArray(normalizedRight)) {
54
+ if (normalizedLeft.length !== normalizedRight.length) {
55
+ mismatches.push(currentPath || '(root)');
56
+ return;
57
+ }
58
+ normalizedLeft.forEach((item, index) => {
59
+ visit(item, normalizedRight[index], currentPath ? `${currentPath}.${index}` : String(index));
60
+ });
61
+ return;
62
+ }
63
+ if (isRecord(normalizedLeft) && isRecord(normalizedRight)) {
64
+ const keys = new Set([...Object.keys(normalizedLeft), ...Object.keys(normalizedRight)]);
65
+ for (const key of keys) {
66
+ visit(normalizedLeft[key], normalizedRight[key], currentPath ? `${currentPath}.${key}` : key);
67
+ if (mismatches.length >= maxPaths) {
68
+ return;
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ mismatches.push(currentPath || '(root)');
74
+ };
75
+ visit(a, b, path);
76
+ return mismatches;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprint-ts/core",
3
- "version": "4.1.0-beta.4",
3
+ "version": "4.1.0-beta.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"