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

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 (30) hide show
  1. package/dist/persistenceDrivers/MemoryPersistenceDriver.d.ts +11 -0
  2. package/dist/persistenceDrivers/MemoryPersistenceDriver.js +22 -0
  3. package/dist/persistenceDrivers/index.d.ts +2 -1
  4. package/dist/persistenceDrivers/index.js +2 -1
  5. package/dist/requests/BaseRequest.d.ts +2 -0
  6. package/dist/requests/BaseRequest.js +7 -2
  7. package/dist/requests/contracts/BaseRequestContract.d.ts +2 -0
  8. package/dist/requests/drivers/mock/MockRequestAssertionError.d.ts +3 -0
  9. package/dist/requests/drivers/mock/MockRequestAssertionError.js +6 -0
  10. package/dist/requests/drivers/mock/MockRequestDriver.d.ts +160 -0
  11. package/dist/requests/drivers/mock/MockRequestDriver.js +588 -0
  12. package/dist/requests/drivers/mock/MockRequestTestHelpers.d.ts +20 -0
  13. package/dist/requests/drivers/mock/MockRequestTestHelpers.js +70 -0
  14. package/dist/requests/drivers/mock/MockResponseHandler.d.ts +22 -0
  15. package/dist/requests/drivers/mock/MockResponseHandler.js +59 -0
  16. package/dist/requests/index.d.ts +5 -2
  17. package/dist/requests/index.js +4 -1
  18. package/dist/vue/forms/BaseForm.d.ts +4 -0
  19. package/dist/vue/forms/BaseForm.js +40 -43
  20. package/dist/vue/forms/index.d.ts +5 -2
  21. package/dist/vue/forms/index.js +3 -1
  22. package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.d.ts +4 -0
  23. package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.js +42 -0
  24. package/dist/vue/forms/persistence/index.d.ts +3 -0
  25. package/dist/vue/forms/persistence/index.js +2 -0
  26. package/dist/vue/forms/persistence/types.d.ts +23 -0
  27. package/dist/vue/forms/persistence/types.js +1 -0
  28. package/dist/vue/forms/persistence/utils.d.ts +2 -0
  29. package/dist/vue/forms/persistence/utils.js +77 -0
  30. package/package.json +1 -1
@@ -0,0 +1,22 @@
1
+ import { type ResolvedHeadersContract } from '../../contracts/HeadersContract';
2
+ import { type ResponseHandlerContract } from '../contracts/ResponseHandlerContract';
3
+ export type MockResponseBody = string | Blob | BufferSource | object | null | undefined;
4
+ export interface MockResponseDefinition {
5
+ status?: number;
6
+ statusText?: string;
7
+ headers?: ResolvedHeadersContract;
8
+ body?: MockResponseBody;
9
+ }
10
+ export declare class MockResponseHandler implements ResponseHandlerContract {
11
+ protected response: Response;
12
+ constructor(definition: MockResponseDefinition);
13
+ getStatusCode(): number | undefined;
14
+ getHeaders(): ResolvedHeadersContract;
15
+ getRawResponse(): Response;
16
+ json<ResponseBodyInterface>(): Promise<ResponseBodyInterface>;
17
+ text(): Promise<string>;
18
+ blob(): Promise<Blob>;
19
+ protected resolveBody(body: MockResponseBody, headers: Headers): BodyInit | null | undefined;
20
+ protected isJsonValue(value: MockResponseBody): value is object;
21
+ protected hasHeader(headers: Headers, key: string): boolean;
22
+ }
@@ -0,0 +1,59 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ export class MockResponseHandler {
11
+ constructor(definition) {
12
+ var _a;
13
+ const headers = new Headers(definition.headers);
14
+ const body = this.resolveBody(definition.body, headers);
15
+ this.response = new Response(body, Object.assign({ status: (_a = definition.status) !== null && _a !== void 0 ? _a : 200, headers }, (definition.statusText !== undefined ? { statusText: definition.statusText } : {})));
16
+ }
17
+ getStatusCode() {
18
+ return this.response.status;
19
+ }
20
+ getHeaders() {
21
+ return Object.fromEntries(this.response.headers);
22
+ }
23
+ getRawResponse() {
24
+ return this.response;
25
+ }
26
+ json() {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ return yield this.response.clone().json();
29
+ });
30
+ }
31
+ text() {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ return yield this.response.clone().text();
34
+ });
35
+ }
36
+ blob() {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ return yield this.response.clone().blob();
39
+ });
40
+ }
41
+ resolveBody(body, headers) {
42
+ if (body === undefined || body === null) {
43
+ return body;
44
+ }
45
+ if (this.isJsonValue(body)) {
46
+ if (!this.hasHeader(headers, 'content-type')) {
47
+ headers.set('content-type', 'application/json');
48
+ }
49
+ return JSON.stringify(body);
50
+ }
51
+ return body;
52
+ }
53
+ isJsonValue(value) {
54
+ return typeof value === 'object' && value !== null && !(value instanceof Blob) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value);
55
+ }
56
+ hasHeader(headers, key) {
57
+ return Array.from(headers.keys()).some((headerKey) => headerKey.toLowerCase() === key.toLowerCase());
58
+ }
59
+ }
@@ -13,6 +13,9 @@ import { BinaryBody } from './bodies/BinaryBody';
13
13
  import { JsonBodyFactory } from './factories/JsonBodyFactory';
14
14
  import { BinaryBodyFactory, type BinaryBodyContent } from './factories/BinaryBodyFactory';
15
15
  import { FormDataFactory } from './factories/FormDataFactory';
16
+ import { MockRequestDriver, MockRequestExpectationBuilder, getMockRequestJsonBody, getMockRequestQuery, getMockRequestTextBody, type MockNormalizedRequestBody, type MockRequestBody, type MockRequestBodyMatchContext, type MockRequestBodyMatcher, type MockRequestDriverOptions, type MockRequestExpectation, type MockRequestExpectationCriteria, type MockRequestHeadersMatcher, type MockRequestHistoryEntry, type MockRequestMatchMode, type MockRequestMethodMatcher, type MockRequestPredicate, type MockRequestQuery, type MockRequestQueryMatcher, type MockRequestUrlMatcher, type MockResponseBody, type MockResponseDefinition } from './drivers/mock/MockRequestDriver';
17
+ import { MockRequestAssertionError } from './drivers/mock/MockRequestAssertionError';
18
+ import { emptyResponse, expectJsonBody, installMockRequestDriver, jsonResponse, matchHeaders, matchQuery, resetMockRequestDriver, validationError, type InstallMockRequestDriverOptions } from './drivers/mock/MockRequestTestHelpers';
16
19
  import { type BodyContent, type BodyContract } from './contracts/BodyContract';
17
20
  import { type RequestLoaderContract } from './contracts/RequestLoaderContract';
18
21
  import { type RequestDriverContract } from './contracts/RequestDriverContract';
@@ -27,5 +30,5 @@ import { type HeaderValue, type HeadersContract, type ResolvedHeadersContract }
27
30
  import { type RequestConcurrencyOptions } from './types/RequestConcurrencyOptions';
28
31
  import { type RequestUploadProgress } from './types/RequestUploadProgress';
29
32
  import { XMLHttpRequestDriver } from './drivers/xhr/XMLHttpRequestDriver';
30
- export { FetchDriver, BaseResponse, JsonResponse, BlobResponse, PlainTextResponse, BaseRequest, ErrorHandler, RequestErrorRouter, RequestEvents, RequestMethodEnum, RequestConcurrencyMode, ResponseException, StaleResponseException, BinaryBody, JsonBodyFactory, BinaryBodyFactory, FormDataFactory, XMLHttpRequestDriver };
31
- export type { BodyContent, BinaryBodyContent, RequestDriverContract, RequestLoaderContract, BodyContract, RequestLoaderFactoryContract, DriverConfigContract, BodyFactoryContract, ResponseHandlerContract, BaseRequestContract, HeaderValue, HeadersContract, ResolvedHeadersContract, RequestConcurrencyOptions, RequestUploadProgress };
33
+ export { FetchDriver, BaseResponse, JsonResponse, BlobResponse, PlainTextResponse, BaseRequest, ErrorHandler, RequestErrorRouter, RequestEvents, RequestMethodEnum, RequestConcurrencyMode, ResponseException, StaleResponseException, BinaryBody, JsonBodyFactory, BinaryBodyFactory, FormDataFactory, XMLHttpRequestDriver, MockRequestDriver, MockRequestExpectationBuilder, MockRequestAssertionError, expectJsonBody, matchHeaders, matchQuery, jsonResponse, validationError, emptyResponse, installMockRequestDriver, resetMockRequestDriver, getMockRequestJsonBody, getMockRequestTextBody, getMockRequestQuery };
34
+ export type { BodyContent, BinaryBodyContent, RequestDriverContract, RequestLoaderContract, BodyContract, RequestLoaderFactoryContract, DriverConfigContract, BodyFactoryContract, ResponseHandlerContract, BaseRequestContract, HeaderValue, HeadersContract, ResolvedHeadersContract, RequestConcurrencyOptions, RequestUploadProgress, MockNormalizedRequestBody, MockRequestBody, MockRequestBodyMatchContext, MockRequestBodyMatcher, MockRequestDriverOptions, MockRequestExpectation, MockRequestExpectationCriteria, MockRequestHeadersMatcher, MockRequestHistoryEntry, MockRequestMatchMode, MockRequestMethodMatcher, MockRequestPredicate, MockRequestQuery, MockRequestQueryMatcher, MockRequestUrlMatcher, MockResponseBody, MockResponseDefinition, InstallMockRequestDriverOptions };
@@ -13,7 +13,10 @@ import { BinaryBody } from './bodies/BinaryBody';
13
13
  import { JsonBodyFactory } from './factories/JsonBodyFactory';
14
14
  import { BinaryBodyFactory } from './factories/BinaryBodyFactory';
15
15
  import { FormDataFactory } from './factories/FormDataFactory';
16
+ import { MockRequestDriver, MockRequestExpectationBuilder, getMockRequestJsonBody, getMockRequestQuery, getMockRequestTextBody } from './drivers/mock/MockRequestDriver';
17
+ import { MockRequestAssertionError } from './drivers/mock/MockRequestAssertionError';
18
+ import { emptyResponse, expectJsonBody, installMockRequestDriver, jsonResponse, matchHeaders, matchQuery, resetMockRequestDriver, validationError } from './drivers/mock/MockRequestTestHelpers';
16
19
  import { ResponseException } from './exceptions/ResponseException';
17
20
  import { StaleResponseException } from './exceptions/StaleResponseException';
18
21
  import { XMLHttpRequestDriver } from './drivers/xhr/XMLHttpRequestDriver';
19
- export { FetchDriver, BaseResponse, JsonResponse, BlobResponse, PlainTextResponse, BaseRequest, ErrorHandler, RequestErrorRouter, RequestEvents, RequestMethodEnum, RequestConcurrencyMode, ResponseException, StaleResponseException, BinaryBody, JsonBodyFactory, BinaryBodyFactory, FormDataFactory, XMLHttpRequestDriver };
22
+ export { FetchDriver, BaseResponse, JsonResponse, BlobResponse, PlainTextResponse, BaseRequest, ErrorHandler, RequestErrorRouter, RequestEvents, RequestMethodEnum, RequestConcurrencyMode, ResponseException, StaleResponseException, BinaryBody, JsonBodyFactory, BinaryBodyFactory, FormDataFactory, XMLHttpRequestDriver, MockRequestDriver, MockRequestExpectationBuilder, MockRequestAssertionError, expectJsonBody, matchHeaders, matchQuery, jsonResponse, validationError, emptyResponse, installMockRequestDriver, resetMockRequestDriver, getMockRequestJsonBody, getMockRequestTextBody, getMockRequestQuery };
@@ -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
  }
@@ -1,10 +1,13 @@
1
1
  import { BaseForm, propertyAwareToRaw } from './BaseForm';
2
2
  import { type PersistedForm } from './types/PersistedForm';
3
3
  import { LocalStorageDriver } from '../../persistenceDrivers/LocalStorageDriver';
4
+ import { MemoryPersistenceDriver } from '../../persistenceDrivers/MemoryPersistenceDriver';
4
5
  import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver';
5
6
  import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDriver';
6
7
  import { type PersistenceDriver } from '../../persistenceDrivers/types/PersistenceDriver';
7
8
  import { PropertyAwareArray, type PropertyAwareField, type PropertyAware } from './PropertyAwareArray';
8
9
  import { PropertyAwareObject } from './PropertyAwareObject';
9
- export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver };
10
- export type { PersistedForm, PersistenceDriver, PropertyAwareField, PropertyAware };
10
+ import { StrictPersistenceRestorePolicy } from './persistence';
11
+ import type { PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult } from './persistence';
12
+ export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, MemoryPersistenceDriver, StrictPersistenceRestorePolicy };
13
+ export type { PersistedForm, PersistenceDriver, PropertyAwareField, PropertyAware, PersistenceDebugEvent, PersistenceRestoreContext, PersistenceRestorePolicy, PersistenceRestoreResult };
@@ -1,7 +1,9 @@
1
1
  import { BaseForm, propertyAwareToRaw } from './BaseForm';
2
2
  import { LocalStorageDriver } from '../../persistenceDrivers/LocalStorageDriver';
3
+ import { MemoryPersistenceDriver } from '../../persistenceDrivers/MemoryPersistenceDriver';
3
4
  import { NonPersistentDriver } from '../../persistenceDrivers/NonPersistentDriver';
4
5
  import { SessionStorageDriver } from '../../persistenceDrivers/SessionStorageDriver';
5
6
  import { PropertyAwareArray } from './PropertyAwareArray';
6
7
  import { PropertyAwareObject } from './PropertyAwareObject';
7
- export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver };
8
+ import { StrictPersistenceRestorePolicy } from './persistence';
9
+ export { BaseForm, propertyAwareToRaw, PropertyAwareArray, PropertyAwareObject, NonPersistentDriver, SessionStorageDriver, LocalStorageDriver, MemoryPersistenceDriver, 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.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"