@continuum-dev/runtime 0.1.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/LICENSE +21 -0
- package/README.md +403 -0
- package/index.d.ts +5 -0
- package/index.d.ts.map +1 -0
- package/index.js +4 -0
- package/lib/context.d.ts +128 -0
- package/lib/context.d.ts.map +1 -0
- package/lib/context.js +305 -0
- package/lib/reconcile.d.ts +17 -0
- package/lib/reconcile.d.ts.map +1 -0
- package/lib/reconcile.js +76 -0
- package/lib/reconciliation/collection-resolver.d.ts +13 -0
- package/lib/reconciliation/collection-resolver.d.ts.map +1 -0
- package/lib/reconciliation/collection-resolver.js +283 -0
- package/lib/reconciliation/component-resolver.d.ts +10 -0
- package/lib/reconciliation/component-resolver.d.ts.map +1 -0
- package/lib/reconciliation/differ.d.ts +12 -0
- package/lib/reconciliation/differ.d.ts.map +1 -0
- package/lib/reconciliation/differ.js +102 -0
- package/lib/reconciliation/migrator.d.ts +13 -0
- package/lib/reconciliation/migrator.d.ts.map +1 -0
- package/lib/reconciliation/migrator.js +92 -0
- package/lib/reconciliation/node-resolver.d.ts +10 -0
- package/lib/reconciliation/node-resolver.d.ts.map +1 -0
- package/lib/reconciliation/node-resolver.js +190 -0
- package/lib/reconciliation/state-builder.d.ts +13 -0
- package/lib/reconciliation/state-builder.d.ts.map +1 -0
- package/lib/reconciliation/state-builder.js +211 -0
- package/lib/reconciliation/validator.d.ts +14 -0
- package/lib/reconciliation/validator.d.ts.map +1 -0
- package/lib/reconciliation/validator.js +100 -0
- package/lib/reconciliation/view-traversal.d.ts +14 -0
- package/lib/reconciliation/view-traversal.d.ts.map +1 -0
- package/lib/reconciliation/view-traversal.js +72 -0
- package/lib/types.d.ts +153 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { getChildNodes } from '@continuum/contract';
|
|
2
|
+
import { ISSUE_CODES, ISSUE_SEVERITY } from '@continuum/contract';
|
|
3
|
+
import { attemptMigration } from './migrator.js';
|
|
4
|
+
const CONTAINER_TYPES = new Set(['row', 'grid', 'group']);
|
|
5
|
+
function areCompatibleContainerTypes(a, b) {
|
|
6
|
+
return CONTAINER_TYPES.has(a) && CONTAINER_TYPES.has(b);
|
|
7
|
+
}
|
|
8
|
+
export function resolveCollectionDefaultValues(node) {
|
|
9
|
+
if (!node.defaultValues || !Array.isArray(node.defaultValues)) {
|
|
10
|
+
return createInitialCollectionValue(node);
|
|
11
|
+
}
|
|
12
|
+
const { keyToPath } = buildTemplatePathMap(node.template);
|
|
13
|
+
const items = node.defaultValues.map((defaultItem) => {
|
|
14
|
+
const itemValues = {};
|
|
15
|
+
// Fill in default values based on semantic keys provided in `defaultValues`
|
|
16
|
+
for (const [key, value] of Object.entries(defaultItem)) {
|
|
17
|
+
const path = keyToPath.get(key) || key; // If path not mapped, use key as path
|
|
18
|
+
itemValues[path] = { value };
|
|
19
|
+
}
|
|
20
|
+
// Fill in missing fields with template defaults
|
|
21
|
+
const templateDefaults = collectTemplateDefaults(node.template);
|
|
22
|
+
for (const [path, defaultValue] of Object.entries(templateDefaults)) {
|
|
23
|
+
if (!(path in itemValues)) {
|
|
24
|
+
itemValues[path] = defaultValue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { values: itemValues };
|
|
28
|
+
});
|
|
29
|
+
return { value: { items } };
|
|
30
|
+
}
|
|
31
|
+
export function createInitialCollectionValue(node) {
|
|
32
|
+
if (node.defaultValues && Array.isArray(node.defaultValues)) {
|
|
33
|
+
return resolveCollectionDefaultValues(node);
|
|
34
|
+
}
|
|
35
|
+
const minItems = normalizeMinItems(node.minItems);
|
|
36
|
+
const items = Array.from({ length: minItems }, () => ({
|
|
37
|
+
values: collectTemplateDefaults(node.template),
|
|
38
|
+
}));
|
|
39
|
+
return { value: { items } };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a bidirectional mapping between node keys and their full relative paths
|
|
43
|
+
* within a template tree. Used to remap collection item value paths when the AI
|
|
44
|
+
* restructures template containers (renames IDs) while keeping the same keys.
|
|
45
|
+
*/
|
|
46
|
+
function buildTemplatePathMap(node, parentPath = '') {
|
|
47
|
+
const keyToPath = new Map();
|
|
48
|
+
const pathToKey = new Map();
|
|
49
|
+
const allPaths = new Set();
|
|
50
|
+
function walk(n, parent) {
|
|
51
|
+
const nodePath = parent.length > 0 ? `${parent}/${n.id}` : n.id;
|
|
52
|
+
allPaths.add(nodePath);
|
|
53
|
+
const effectiveKey = n.key ?? n.id;
|
|
54
|
+
// Only map the first occurrence of a key (no overwrites)
|
|
55
|
+
if (!keyToPath.has(effectiveKey)) {
|
|
56
|
+
keyToPath.set(effectiveKey, nodePath);
|
|
57
|
+
}
|
|
58
|
+
pathToKey.set(nodePath, effectiveKey);
|
|
59
|
+
const children = getChildNodes(n);
|
|
60
|
+
for (const child of children) {
|
|
61
|
+
walk(child, nodePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walk(node, parentPath);
|
|
65
|
+
return { keyToPath, pathToKey, allPaths };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Remap collection item value keys from old template paths to new template paths.
|
|
69
|
+
* For each value key in the item:
|
|
70
|
+
* 1. Look up what key (semantic name) that old path corresponded to
|
|
71
|
+
* 2. Find the new path for that same key
|
|
72
|
+
* 3. Move the value to the new path
|
|
73
|
+
* Values that can't be remapped are kept under their original key.
|
|
74
|
+
*/
|
|
75
|
+
function remapCollectionItemPaths(oldValues, priorMap, newMap) {
|
|
76
|
+
const remapped = {};
|
|
77
|
+
let hasChanges = false;
|
|
78
|
+
for (const [oldPath, value] of Object.entries(oldValues)) {
|
|
79
|
+
const semanticKey = priorMap.pathToKey.get(oldPath);
|
|
80
|
+
if (semanticKey) {
|
|
81
|
+
const newPath = newMap.keyToPath.get(semanticKey);
|
|
82
|
+
if (newPath && newPath !== oldPath) {
|
|
83
|
+
remapped[newPath] = value;
|
|
84
|
+
hasChanges = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// If the old path exists in the new template, keep it as-is.
|
|
89
|
+
// If it doesn't exist in either map, keep it as-is (legacy data).
|
|
90
|
+
remapped[oldPath] = value;
|
|
91
|
+
}
|
|
92
|
+
return hasChanges ? remapped : oldValues;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Determine whether the template structure has changed enough to warrant
|
|
96
|
+
* path remapping. This is true when any node paths differ between the two
|
|
97
|
+
* templates (container IDs changed, nodes reorganized, etc.)
|
|
98
|
+
*/
|
|
99
|
+
function needsPathRemapping(priorMap, newMap) {
|
|
100
|
+
if (priorMap.allPaths.size !== newMap.allPaths.size)
|
|
101
|
+
return true;
|
|
102
|
+
for (const path of priorMap.allPaths) {
|
|
103
|
+
if (!newMap.allPaths.has(path))
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
export function reconcileCollectionValue(priorNode, newNode, priorValue, options) {
|
|
109
|
+
const issues = [];
|
|
110
|
+
const normalized = normalizeCollectionValue(newNode, priorValue);
|
|
111
|
+
if (priorNode.template.type !== newNode.template.type && !areCompatibleContainerTypes(priorNode.template.type, newNode.template.type)) {
|
|
112
|
+
issues.push({
|
|
113
|
+
severity: ISSUE_SEVERITY.ERROR,
|
|
114
|
+
nodeId: newNode.id,
|
|
115
|
+
message: `Node type mismatch: ${priorNode.template.type} -> ${newNode.template.type}`,
|
|
116
|
+
code: ISSUE_CODES.TYPE_MISMATCH,
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
value: createInitialCollectionValue(newNode),
|
|
120
|
+
issues,
|
|
121
|
+
didMigrateItems: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let didMigrateItems = false;
|
|
125
|
+
const priorTemplate = priorNode.template;
|
|
126
|
+
const newTemplate = newNode.template;
|
|
127
|
+
// Build path maps for key-based remapping
|
|
128
|
+
const priorPathMap = buildTemplatePathMap(priorTemplate);
|
|
129
|
+
const newPathMap = buildTemplatePathMap(newTemplate);
|
|
130
|
+
const shouldRemap = needsPathRemapping(priorPathMap, newPathMap);
|
|
131
|
+
// Check if AI pushed new defaultValues that differ from the prior ones
|
|
132
|
+
let receivedNewDefaults = false;
|
|
133
|
+
let hasDirtyItems = false;
|
|
134
|
+
if (newNode.defaultValues !== undefined) {
|
|
135
|
+
if (!priorNode.defaultValues || JSON.stringify(priorNode.defaultValues) !== JSON.stringify(newNode.defaultValues)) {
|
|
136
|
+
receivedNewDefaults = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Determine if any prior item has dirty fields
|
|
140
|
+
if (receivedNewDefaults && priorValue !== undefined) {
|
|
141
|
+
const state = priorValue.value;
|
|
142
|
+
if (state && Array.isArray(state.items)) {
|
|
143
|
+
hasDirtyItems = state.items.some(item => item.values && Object.values(item.values).some(v => v.isDirty));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// If AI provided new defaultValues, populate those instead
|
|
147
|
+
if (receivedNewDefaults) {
|
|
148
|
+
const defaultCollection = resolveCollectionDefaultValues(newNode);
|
|
149
|
+
const constrained = applyItemConstraints(defaultCollection.value.items, newNode.minItems, newNode.maxItems, issues, newNode.id);
|
|
150
|
+
// If there were dirty items, return the new defaults as a `suggestion` for the whole collection
|
|
151
|
+
if (hasDirtyItems) {
|
|
152
|
+
const suggestedValue = {
|
|
153
|
+
value: normalized.value, // keep prior as current
|
|
154
|
+
suggestion: { items: constrained } // new items as suggestion
|
|
155
|
+
};
|
|
156
|
+
// Keep isDirty flag from prior NodeValue if it exists
|
|
157
|
+
if (normalized.isDirty) {
|
|
158
|
+
suggestedValue.isDirty = true;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
value: suggestedValue,
|
|
162
|
+
issues,
|
|
163
|
+
didMigrateItems: true, // Treat as migration to force UI update
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// If no dirty items, immediately overwrite
|
|
167
|
+
return {
|
|
168
|
+
value: { value: { items: constrained } },
|
|
169
|
+
issues,
|
|
170
|
+
didMigrateItems: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const migratedItems = normalized.value.items.map((item) => {
|
|
174
|
+
// Step 1: Remap value paths from old template structure to new
|
|
175
|
+
const values = shouldRemap
|
|
176
|
+
? remapCollectionItemPaths(item.values, priorPathMap, newPathMap)
|
|
177
|
+
: { ...item.values };
|
|
178
|
+
if (shouldRemap && values !== item.values) {
|
|
179
|
+
didMigrateItems = true;
|
|
180
|
+
}
|
|
181
|
+
// Step 2: Handle hash-based migration for the template root value
|
|
182
|
+
const priorTemplateValue = values[priorTemplate.id];
|
|
183
|
+
if (priorTemplateValue !== undefined && hasTemplateHashChanged(priorTemplate, newTemplate)) {
|
|
184
|
+
const migrationResult = attemptMigration(priorTemplate.id, priorTemplate, newTemplate, priorTemplateValue, options);
|
|
185
|
+
if (migrationResult.kind === 'migrated') {
|
|
186
|
+
didMigrateItems = true;
|
|
187
|
+
values[newTemplate.id] = migrationResult.value;
|
|
188
|
+
if (newTemplate.id !== priorTemplate.id) {
|
|
189
|
+
delete values[priorTemplate.id];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (migrationResult.kind === 'error') {
|
|
193
|
+
issues.push({
|
|
194
|
+
severity: ISSUE_SEVERITY.WARNING,
|
|
195
|
+
nodeId: newNode.id,
|
|
196
|
+
message: `Node ${newNode.id} migration failed: ${String(migrationResult.error)}`,
|
|
197
|
+
code: ISSUE_CODES.MIGRATION_FAILED,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
issues.push({
|
|
202
|
+
severity: ISSUE_SEVERITY.WARNING,
|
|
203
|
+
nodeId: newNode.id,
|
|
204
|
+
message: `Node ${newNode.id} view changed but no migration strategy available`,
|
|
205
|
+
code: ISSUE_CODES.MIGRATION_FAILED,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { values };
|
|
210
|
+
});
|
|
211
|
+
const constrained = applyItemConstraints(migratedItems, newNode.minItems, newNode.maxItems, issues, newNode.id);
|
|
212
|
+
return {
|
|
213
|
+
value: { value: { items: constrained } },
|
|
214
|
+
issues,
|
|
215
|
+
didMigrateItems,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function normalizeCollectionValue(node, value) {
|
|
219
|
+
if (!value || typeof value !== 'object' || !('value' in value)) {
|
|
220
|
+
return createInitialCollectionValue(node);
|
|
221
|
+
}
|
|
222
|
+
const state = value.value;
|
|
223
|
+
const items = Array.isArray(state?.items)
|
|
224
|
+
? state.items.map((item) => ({ values: (item?.values ?? {}) }))
|
|
225
|
+
: [];
|
|
226
|
+
return { value: { items } };
|
|
227
|
+
}
|
|
228
|
+
function applyItemConstraints(items, minItems, maxItems, issues, nodeId) {
|
|
229
|
+
let constrained = [...items];
|
|
230
|
+
const min = normalizeMinItems(minItems);
|
|
231
|
+
const max = normalizeMaxItems(maxItems);
|
|
232
|
+
if (max !== undefined && constrained.length > max) {
|
|
233
|
+
constrained = constrained.slice(0, max);
|
|
234
|
+
issues.push({
|
|
235
|
+
severity: ISSUE_SEVERITY.WARNING,
|
|
236
|
+
nodeId,
|
|
237
|
+
message: `Collection ${nodeId} exceeded maxItems`,
|
|
238
|
+
code: ISSUE_CODES.COLLECTION_CONSTRAINT_VIOLATED,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
while (constrained.length < min) {
|
|
242
|
+
constrained.push({ values: {} });
|
|
243
|
+
issues.push({
|
|
244
|
+
severity: ISSUE_SEVERITY.INFO,
|
|
245
|
+
nodeId,
|
|
246
|
+
message: `Collection ${nodeId} filled to minItems`,
|
|
247
|
+
code: ISSUE_CODES.COLLECTION_CONSTRAINT_VIOLATED,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return constrained;
|
|
251
|
+
}
|
|
252
|
+
function normalizeMinItems(value) {
|
|
253
|
+
if (value === undefined || value < 0) {
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
return Math.floor(value);
|
|
257
|
+
}
|
|
258
|
+
function normalizeMaxItems(value) {
|
|
259
|
+
if (value === undefined || value < 0) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return Math.floor(value);
|
|
263
|
+
}
|
|
264
|
+
function hasTemplateHashChanged(priorTemplate, newTemplate) {
|
|
265
|
+
return !!(priorTemplate.hash && newTemplate.hash && priorTemplate.hash !== newTemplate.hash);
|
|
266
|
+
}
|
|
267
|
+
function collectTemplateDefaults(node, parentPath = '') {
|
|
268
|
+
const nodeId = parentPath.length > 0 ? `${parentPath}/${node.id}` : node.id;
|
|
269
|
+
if (node.type === 'collection') {
|
|
270
|
+
return {
|
|
271
|
+
[nodeId]: createInitialCollectionValue(node),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const values = {};
|
|
275
|
+
if ('defaultValue' in node && node.defaultValue !== undefined) {
|
|
276
|
+
values[nodeId] = { value: node.defaultValue };
|
|
277
|
+
}
|
|
278
|
+
const children = getChildNodes(node);
|
|
279
|
+
for (const child of children) {
|
|
280
|
+
Object.assign(values, collectTemplateDefaults(child, nodeId));
|
|
281
|
+
}
|
|
282
|
+
return values;
|
|
283
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { OrphanedValue, StateSnapshot } from '@continuum/contract';
|
|
2
|
+
import type { ComponentResolutionAccumulator, ReconciliationIssue, ReconciliationOptions, StateDiff } from '../types.js';
|
|
3
|
+
import type { ReconciliationContext } from '../context.js';
|
|
4
|
+
export declare function resolveAllComponents(ctx: ReconciliationContext, priorValues: Map<string, unknown>, priorState: StateSnapshot, now: number, options: ReconciliationOptions): ComponentResolutionAccumulator;
|
|
5
|
+
export declare function detectRemovedComponents(ctx: ReconciliationContext, priorState: StateSnapshot, options: ReconciliationOptions, now: number): {
|
|
6
|
+
diffs: StateDiff[];
|
|
7
|
+
issues: ReconciliationIssue[];
|
|
8
|
+
orphanedValues: Record<string, OrphanedValue>;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=component-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-resolver.d.ts","sourceRoot":"","sources":["../../../../../packages/runtime/src/lib/reconciliation/component-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAuC,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE7G,OAAO,KAAK,EACV,8BAA8B,EAC9B,mBAAmB,EACnB,qBAAqB,EACrB,SAAS,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAqB3D,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,qBAAqB,EAC1B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,UAAU,EAAE,aAAa,EACzB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,qBAAqB,GAC7B,8BAA8B,CAoChC;AAoID,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,qBAAqB,EAC1B,UAAU,EAAE,aAAa,EACzB,OAAO,EAAE,qBAAqB,EAC9B,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CAAE,CAoCtG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StateDiff, ReconciliationResolution } from '../types.js';
|
|
2
|
+
export declare function addedDiff(nodeId: string): StateDiff;
|
|
3
|
+
export declare function removedDiff(nodeId: string, oldValue: unknown): StateDiff;
|
|
4
|
+
export declare function typeChangedDiff(nodeId: string, oldValue: unknown, priorType: string, newType: string): StateDiff;
|
|
5
|
+
export declare function migratedDiff(nodeId: string, oldValue: unknown, newValue: unknown): StateDiff;
|
|
6
|
+
export declare function restoredDiff(nodeId: string, newValue: unknown): StateDiff;
|
|
7
|
+
export declare function addedResolution(nodeId: string, newType: string): ReconciliationResolution;
|
|
8
|
+
export declare function carriedResolution(nodeId: string, priorId: string, matchedBy: 'id' | 'key', nodeType: string, priorValue: unknown, reconciledValue: unknown): ReconciliationResolution;
|
|
9
|
+
export declare function detachedResolution(nodeId: string, priorId: string, matchedBy: 'id' | 'key' | null, priorType: string, newType: string, priorValue: unknown): ReconciliationResolution;
|
|
10
|
+
export declare function migratedResolution(nodeId: string, priorId: string, matchedBy: 'id' | 'key' | null, priorType: string, newType: string, priorValue: unknown, reconciledValue: unknown): ReconciliationResolution;
|
|
11
|
+
export declare function restoredResolution(nodeId: string, priorType: string, reconciledValue: unknown): ReconciliationResolution;
|
|
12
|
+
//# sourceMappingURL=differ.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../../../../packages/runtime/src/lib/reconciliation/differ.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAEvE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAOnD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,SAAS,CAOxE;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,SAAS,CAOX;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,OAAO,GAChB,SAAS,CAQX;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,GAChB,SAAS,CAOX;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,wBAAwB,CAWzF;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,OAAO,EACnB,eAAe,EAAE,OAAO,GACvB,wBAAwB,CAW1B;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,EAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,OAAO,GAClB,wBAAwB,CAW1B;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,EAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,OAAO,EACnB,eAAe,EAAE,OAAO,GACvB,wBAAwB,CAW1B;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,OAAO,GACvB,wBAAwB,CAW1B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { VIEW_DIFFS, DATA_RESOLUTIONS } from '@continuum/contract';
|
|
2
|
+
export function addedDiff(nodeId) {
|
|
3
|
+
return {
|
|
4
|
+
nodeId,
|
|
5
|
+
type: VIEW_DIFFS.ADDED,
|
|
6
|
+
newValue: undefined,
|
|
7
|
+
reason: 'Node added to view',
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function removedDiff(nodeId, oldValue) {
|
|
11
|
+
return {
|
|
12
|
+
nodeId,
|
|
13
|
+
type: VIEW_DIFFS.REMOVED,
|
|
14
|
+
oldValue,
|
|
15
|
+
reason: 'Node removed from view',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function typeChangedDiff(nodeId, oldValue, priorType, newType) {
|
|
19
|
+
return {
|
|
20
|
+
nodeId,
|
|
21
|
+
type: VIEW_DIFFS.TYPE_CHANGED,
|
|
22
|
+
oldValue,
|
|
23
|
+
reason: `Type changed from ${priorType} to ${newType}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function migratedDiff(nodeId, oldValue, newValue) {
|
|
27
|
+
return {
|
|
28
|
+
nodeId,
|
|
29
|
+
type: VIEW_DIFFS.MIGRATED,
|
|
30
|
+
oldValue,
|
|
31
|
+
newValue,
|
|
32
|
+
reason: 'Node view changed, migration applied',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function restoredDiff(nodeId, newValue) {
|
|
36
|
+
return {
|
|
37
|
+
nodeId,
|
|
38
|
+
type: VIEW_DIFFS.RESTORED,
|
|
39
|
+
newValue,
|
|
40
|
+
reason: 'Node restored from detached values',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function addedResolution(nodeId, newType) {
|
|
44
|
+
return {
|
|
45
|
+
nodeId,
|
|
46
|
+
priorId: null,
|
|
47
|
+
matchedBy: null,
|
|
48
|
+
priorType: null,
|
|
49
|
+
newType,
|
|
50
|
+
resolution: DATA_RESOLUTIONS.ADDED,
|
|
51
|
+
priorValue: undefined,
|
|
52
|
+
reconciledValue: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function carriedResolution(nodeId, priorId, matchedBy, nodeType, priorValue, reconciledValue) {
|
|
56
|
+
return {
|
|
57
|
+
nodeId,
|
|
58
|
+
priorId,
|
|
59
|
+
matchedBy,
|
|
60
|
+
priorType: nodeType,
|
|
61
|
+
newType: nodeType,
|
|
62
|
+
resolution: DATA_RESOLUTIONS.CARRIED,
|
|
63
|
+
priorValue,
|
|
64
|
+
reconciledValue,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function detachedResolution(nodeId, priorId, matchedBy, priorType, newType, priorValue) {
|
|
68
|
+
return {
|
|
69
|
+
nodeId,
|
|
70
|
+
priorId,
|
|
71
|
+
matchedBy,
|
|
72
|
+
priorType,
|
|
73
|
+
newType,
|
|
74
|
+
resolution: DATA_RESOLUTIONS.DETACHED,
|
|
75
|
+
priorValue,
|
|
76
|
+
reconciledValue: undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function migratedResolution(nodeId, priorId, matchedBy, priorType, newType, priorValue, reconciledValue) {
|
|
80
|
+
return {
|
|
81
|
+
nodeId,
|
|
82
|
+
priorId,
|
|
83
|
+
matchedBy,
|
|
84
|
+
priorType,
|
|
85
|
+
newType,
|
|
86
|
+
resolution: DATA_RESOLUTIONS.MIGRATED,
|
|
87
|
+
priorValue,
|
|
88
|
+
reconciledValue,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function restoredResolution(nodeId, priorType, reconciledValue) {
|
|
92
|
+
return {
|
|
93
|
+
nodeId,
|
|
94
|
+
priorId: null,
|
|
95
|
+
matchedBy: null,
|
|
96
|
+
priorType,
|
|
97
|
+
newType: priorType,
|
|
98
|
+
resolution: DATA_RESOLUTIONS.RESTORED,
|
|
99
|
+
priorValue: undefined,
|
|
100
|
+
reconciledValue,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ViewNode } from '@continuum/contract';
|
|
2
|
+
import type { ReconciliationOptions } from '../types.js';
|
|
3
|
+
export type MigrationAttemptResult = {
|
|
4
|
+
kind: 'migrated';
|
|
5
|
+
value: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'none';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'error';
|
|
10
|
+
error: unknown;
|
|
11
|
+
};
|
|
12
|
+
export declare function attemptMigration(nodeId: string, priorNode: ViewNode, newNode: ViewNode, priorValue: unknown, options: ReconciliationOptions): MigrationAttemptResult;
|
|
13
|
+
//# sourceMappingURL=migrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../../../../../packages/runtime/src/lib/reconciliation/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAUtC,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,QAAQ,EACnB,OAAO,EAAE,QAAQ,EACjB,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,qBAAqB,GAC7B,sBAAsB,CA4ExB"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const MAX_CHAIN_DEPTH = 10;
|
|
2
|
+
export function attemptMigration(nodeId, priorNode, newNode, priorValue, options) {
|
|
3
|
+
if (options.migrationStrategies?.[nodeId]) {
|
|
4
|
+
try {
|
|
5
|
+
return {
|
|
6
|
+
kind: 'migrated',
|
|
7
|
+
value: options.migrationStrategies[nodeId](nodeId, priorNode, newNode, priorValue),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return { kind: 'error', error };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (newNode.migrations && priorNode.hash && newNode.hash) {
|
|
15
|
+
if (!options.strategyRegistry) {
|
|
16
|
+
return { kind: 'none' };
|
|
17
|
+
}
|
|
18
|
+
const directRule = newNode.migrations.find((m) => m.fromHash === priorNode.hash &&
|
|
19
|
+
m.toHash === newNode.hash &&
|
|
20
|
+
!!m.strategyId &&
|
|
21
|
+
!!options.strategyRegistry?.[m.strategyId]);
|
|
22
|
+
if (directRule?.strategyId) {
|
|
23
|
+
try {
|
|
24
|
+
return {
|
|
25
|
+
kind: 'migrated',
|
|
26
|
+
value: options.strategyRegistry[directRule.strategyId](nodeId, priorNode, newNode, priorValue),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return { kind: 'error', error };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const path = findMigrationPath(newNode.migrations, priorNode.hash, newNode.hash, options.strategyRegistry);
|
|
34
|
+
if (!path) {
|
|
35
|
+
return { kind: 'none' };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
let currentValue = priorValue;
|
|
39
|
+
let currentHash = priorNode.hash;
|
|
40
|
+
for (const step of path) {
|
|
41
|
+
const strategy = options.strategyRegistry[step.strategyId];
|
|
42
|
+
const stepPriorNode = { ...priorNode, hash: currentHash };
|
|
43
|
+
const stepNewNode = { ...newNode, hash: step.toHash };
|
|
44
|
+
currentValue = strategy(nodeId, stepPriorNode, stepNewNode, currentValue);
|
|
45
|
+
currentHash = step.toHash;
|
|
46
|
+
}
|
|
47
|
+
return { kind: 'migrated', value: currentValue };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { kind: 'error', error };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (priorNode.type === newNode.type) {
|
|
54
|
+
return { kind: 'migrated', value: priorValue };
|
|
55
|
+
}
|
|
56
|
+
return { kind: 'none' };
|
|
57
|
+
}
|
|
58
|
+
function findMigrationPath(rules, fromHash, toHash, strategyRegistry) {
|
|
59
|
+
const edges = rules
|
|
60
|
+
.filter((rule) => !!rule.strategyId && !!strategyRegistry[rule.strategyId])
|
|
61
|
+
.map((rule) => ({
|
|
62
|
+
fromHash: rule.fromHash,
|
|
63
|
+
toHash: rule.toHash,
|
|
64
|
+
strategyId: rule.strategyId,
|
|
65
|
+
}));
|
|
66
|
+
const byFrom = new Map();
|
|
67
|
+
for (const edge of edges) {
|
|
68
|
+
const existing = byFrom.get(edge.fromHash) ?? [];
|
|
69
|
+
existing.push(edge);
|
|
70
|
+
byFrom.set(edge.fromHash, existing);
|
|
71
|
+
}
|
|
72
|
+
const queue = [{ hash: fromHash, path: [] }];
|
|
73
|
+
const seen = new Set([fromHash]);
|
|
74
|
+
while (queue.length > 0) {
|
|
75
|
+
const current = queue.shift();
|
|
76
|
+
const nextEdges = byFrom.get(current.hash) ?? [];
|
|
77
|
+
for (const edge of nextEdges) {
|
|
78
|
+
const nextPath = [...current.path, edge];
|
|
79
|
+
if (nextPath.length > MAX_CHAIN_DEPTH) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (edge.toHash === toHash) {
|
|
83
|
+
return nextPath;
|
|
84
|
+
}
|
|
85
|
+
if (!seen.has(edge.toHash)) {
|
|
86
|
+
seen.add(edge.toHash);
|
|
87
|
+
queue.push({ hash: edge.toHash, path: nextPath });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataSnapshot, DetachedValue } from '@continuum/contract';
|
|
2
|
+
import type { NodeResolutionAccumulator, ReconciliationIssue, ReconciliationOptions, StateDiff } from '../types.js';
|
|
3
|
+
import type { ReconciliationContext } from '../context.js';
|
|
4
|
+
export declare function resolveAllNodes(ctx: ReconciliationContext, priorValues: Map<string, unknown>, priorData: DataSnapshot, now: number, options: ReconciliationOptions): NodeResolutionAccumulator;
|
|
5
|
+
export declare function detectRemovedNodes(ctx: ReconciliationContext, priorData: DataSnapshot, options: ReconciliationOptions, now: number): {
|
|
6
|
+
diffs: StateDiff[];
|
|
7
|
+
issues: ReconciliationIssue[];
|
|
8
|
+
detachedValues: Record<string, DetachedValue>;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=node-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-resolver.d.ts","sourceRoot":"","sources":["../../../../../packages/runtime/src/lib/reconciliation/node-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAuB,MAAM,qBAAqB,CAAC;AAE5F,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,qBAAqB,EACrB,SAAS,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAwB3D,wBAAgB,eAAe,CAC7B,GAAG,EAAE,qBAAqB,EAC1B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,SAAS,EAAE,YAAY,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,qBAAqB,GAC7B,yBAAyB,CAiF3B;AAgOD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,qBAAqB,EAC1B,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,qBAAqB,EAC9B,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CAAE,CAsCtG"}
|