@jay-framework/view-state-merge 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Type alias for trackBy map: maps property paths to trackBy field names.
3
+ * Example: { "items": "id", "products.variants": "sku" }
4
+ */
5
+ type TrackByMap = Record<string, string>;
6
+ /**
7
+ * Deep merge two view states using trackBy metadata for arrays.
8
+ * Used to combine view states from different rendering phases (slow + fast)
9
+ * or to merge default view state with interactive updates.
10
+ *
11
+ * @param base - Base ViewState (e.g., slow render result or default view state)
12
+ * @param overlay - Overlay ViewState (e.g., fast render result or interactive updates)
13
+ * @param trackByMap - Map from property path to trackBy field name (e.g., {"items": "id"})
14
+ * @param path - Current property path (used for recursion)
15
+ * @returns Merged ViewState combining properties from both inputs
16
+ */
17
+ declare function deepMergeViewStates(base: object | undefined, overlay: object | undefined, trackByMap: TrackByMap, path?: string): object;
18
+
19
+ export { type TrackByMap, deepMergeViewStates };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
2
+ if (!base && !overlay)
3
+ return {};
4
+ if (!base)
5
+ return overlay || {};
6
+ if (!overlay)
7
+ return base || {};
8
+ const result = {};
9
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
10
+ for (const key of allKeys) {
11
+ const baseValue = base[key];
12
+ const overlayValue = overlay[key];
13
+ const currentPath = path ? `${path}.${key}` : key;
14
+ if (overlayValue === void 0) {
15
+ result[key] = baseValue;
16
+ } else if (baseValue === void 0) {
17
+ result[key] = overlayValue;
18
+ } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
19
+ const trackByField = trackByMap[currentPath];
20
+ if (trackByField) {
21
+ result[key] = mergeArraysByTrackBy(
22
+ baseValue,
23
+ overlayValue,
24
+ trackByField,
25
+ trackByMap,
26
+ currentPath
27
+ );
28
+ } else {
29
+ result[key] = overlayValue;
30
+ }
31
+ } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
32
+ result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
33
+ } else {
34
+ result[key] = overlayValue;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
40
+ const baseByKey = /* @__PURE__ */ new Map();
41
+ for (const item of baseArray) {
42
+ const key = item[trackByField];
43
+ if (key !== void 0 && key !== null) {
44
+ if (baseByKey.has(key)) {
45
+ console.warn(
46
+ `Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
47
+ );
48
+ }
49
+ baseByKey.set(key, item);
50
+ }
51
+ }
52
+ const overlayByKey = /* @__PURE__ */ new Map();
53
+ for (const item of overlayArray) {
54
+ const key = item[trackByField];
55
+ if (key !== void 0 && key !== null) {
56
+ overlayByKey.set(key, item);
57
+ }
58
+ }
59
+ return baseArray.map((baseItem) => {
60
+ const key = baseItem[trackByField];
61
+ if (key === void 0 || key === null) {
62
+ return baseItem;
63
+ }
64
+ const overlayItem = overlayByKey.get(key);
65
+ if (overlayItem) {
66
+ return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
67
+ } else {
68
+ return baseItem;
69
+ }
70
+ });
71
+ }
72
+ export {
73
+ deepMergeViewStates
74
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@jay-framework/view-state-merge",
3
+ "version": "0.10.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "main": "./dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "readme.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "npm run build:js && npm run build:types",
13
+ "build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
14
+ "build:js": "vite build",
15
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
16
+ "build:check-types": "tsc",
17
+ "clean": "rimraf dist",
18
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest"
21
+ },
22
+ "devDependencies": {
23
+ "@jay-framework/dev-environment": "^0.10.0",
24
+ "@types/node": "^20.11.5",
25
+ "rimraf": "^5.0.5",
26
+ "tsup": "^8.0.1",
27
+ "typescript": "^5.3.3",
28
+ "vite": "^5.0.11",
29
+ "vitest": "^1.2.1"
30
+ }
31
+ }
package/readme.md ADDED
@@ -0,0 +1,113 @@
1
+ # @jay-framework/view-state-merge
2
+
3
+ Deep merge utility for combining ViewStates with identity-based array merging.
4
+
5
+ ## Purpose
6
+
7
+ This library provides a deep merge algorithm that correctly combines ViewStates from different rendering phases or interactive updates. Unlike simple object spread (`{...a, ...b}`), this algorithm:
8
+
9
+ 1. **Deep merges nested objects** - Preserves properties from both sources at any nesting depth
10
+ 2. **Merges arrays by identity** - Uses `trackBy` metadata to match array items by their identity field (e.g., `id`) and merge their properties
11
+ 3. **Base defines structure** - For tracked arrays, items only in overlay are NOT added; the base array defines which items exist
12
+ 4. **Array replacement without trackBy** - Arrays without trackBy info are completely replaced by overlay, enabling dynamic list updates (search results, filters, etc.)
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import { deepMergeViewStates, TrackByMap } from '@jay-framework/view-state-merge';
18
+
19
+ const base = {
20
+ name: 'Product',
21
+ items: [
22
+ { id: '1', title: 'Item 1' },
23
+ { id: '2', title: 'Item 2' },
24
+ ],
25
+ };
26
+
27
+ const overlay = {
28
+ price: 29.99,
29
+ items: [
30
+ { id: '1', selected: true },
31
+ { id: '2', selected: false },
32
+ ],
33
+ };
34
+
35
+ const trackByMap: TrackByMap = {
36
+ items: 'id', // 'items' array uses 'id' field for identity
37
+ };
38
+
39
+ const merged = deepMergeViewStates(base, overlay, trackByMap);
40
+ // Result:
41
+ // {
42
+ // name: 'Product',
43
+ // price: 29.99,
44
+ // items: [
45
+ // { id: '1', title: 'Item 1', selected: true },
46
+ // { id: '2', title: 'Item 2', selected: false },
47
+ // ],
48
+ // }
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `deepMergeViewStates(base, overlay, trackByMap, path?)`
54
+
55
+ Merges two ViewState objects, with overlay values taking precedence for conflicts.
56
+
57
+ - `base` - Base ViewState object
58
+ - `overlay` - Overlay ViewState object (values override base)
59
+ - `trackByMap` - Map from property paths to trackBy field names
60
+ - `path` - (internal) Current property path for recursion
61
+
62
+ ### `TrackByMap`
63
+
64
+ Type alias for the trackBy mapping:
65
+
66
+ ```typescript
67
+ type TrackByMap = Record<string, string>;
68
+ ```
69
+
70
+ Keys are dot-separated property paths (e.g., `"items"`, `"user.orders"`), values are the field names used for identity (e.g., `"id"`, `"orderId"`).
71
+
72
+ ## Array Behavior
73
+
74
+ ### With trackBy (identity-based merge)
75
+
76
+ When an array path is in `trackByMap`, items are matched by identity and merged:
77
+
78
+ ```typescript
79
+ const trackByMap = { items: 'id' };
80
+
81
+ // Base: [{ id: '1', name: 'A' }, { id: '2', name: 'B' }]
82
+ // Overlay: [{ id: '1', selected: true }]
83
+ // Result: [{ id: '1', name: 'A', selected: true }, { id: '2', name: 'B' }]
84
+ ```
85
+
86
+ - Items are matched by their `id` field
87
+ - Properties from both are merged
88
+ - Base array order is preserved
89
+ - Overlay-only items are NOT added (base defines structure)
90
+
91
+ ### Without trackBy (full replacement)
92
+
93
+ When an array path is NOT in `trackByMap`, the overlay array completely replaces the base:
94
+
95
+ ```typescript
96
+ const trackByMap = {}; // No trackBy for searchResults
97
+
98
+ // Base: [{ id: '1', title: 'Old' }]
99
+ // Overlay: [{ id: '3', title: 'New 1' }, { id: '4', title: 'New 2' }]
100
+ // Result: [{ id: '3', title: 'New 1' }, { id: '4', title: 'New 2' }]
101
+ ```
102
+
103
+ This is useful for:
104
+
105
+ - Search results that change entirely
106
+ - Filtered lists
107
+ - Paginated data
108
+ - Any dynamic list where items aren't being updated, but replaced
109
+
110
+ ## See Also
111
+
112
+ - Design Log #56: Deep Merge View States with Track-By
113
+ - Design Log #62: Relocate Deep Merge for Stack-Client-Runtime