@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.
- package/dist/index.d.ts +19 -0
- package/dist/index.js +74 -0
- package/package.json +31 -0
- package/readme.md +113 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|