cogsbox-state 0.5.472 → 0.5.474
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/README.md +48 -18
- package/dist/CogsState.d.ts +98 -82
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1030 -960
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +299 -219
- package/dist/Components.jsx.map +1 -1
- package/dist/PluginRunner.d.ts +10 -0
- package/dist/PluginRunner.d.ts.map +1 -0
- package/dist/PluginRunner.jsx +122 -0
- package/dist/PluginRunner.jsx.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -26
- package/dist/index.js.map +1 -1
- package/dist/pluginStore.d.ts +81 -0
- package/dist/pluginStore.d.ts.map +1 -0
- package/dist/pluginStore.js +52 -0
- package/dist/pluginStore.js.map +1 -0
- package/dist/plugins.d.ts +1323 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +76 -0
- package/dist/plugins.js.map +1 -0
- package/dist/store.d.ts +50 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +509 -470
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +1 -1
- package/dist/utility.d.ts.map +1 -1
- package/dist/utility.js +12 -12
- package/dist/utility.js.map +1 -1
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +39 -0
- package/dist/validation.js.map +1 -0
- package/package.json +13 -3
- package/src/CogsState.tsx +657 -457
- package/src/Components.tsx +291 -194
- package/src/PluginRunner.tsx +203 -0
- package/src/index.ts +2 -0
- package/src/pluginStore.ts +176 -0
- package/src/plugins.ts +544 -0
- package/src/store.ts +748 -493
- package/src/utility.ts +31 -31
- package/src/validation.ts +84 -0
package/src/store.ts
CHANGED
|
@@ -7,8 +7,6 @@ import type {
|
|
|
7
7
|
UpdateTypeDetail,
|
|
8
8
|
} from './CogsState.js';
|
|
9
9
|
|
|
10
|
-
import { type ReactNode } from 'react';
|
|
11
|
-
|
|
12
10
|
export type FreshValuesObject = {
|
|
13
11
|
pathsToValues?: string[];
|
|
14
12
|
prevValue?: any;
|
|
@@ -17,55 +15,16 @@ export type FreshValuesObject = {
|
|
|
17
15
|
};
|
|
18
16
|
|
|
19
17
|
type StateValue = any;
|
|
20
|
-
|
|
18
|
+
export type FormEventType = {
|
|
19
|
+
type: 'focus' | 'blur' | 'input';
|
|
20
|
+
value?: any;
|
|
21
|
+
path: string[];
|
|
22
|
+
};
|
|
21
23
|
export type TrieNode = {
|
|
22
24
|
subscribers: Set<string>;
|
|
23
25
|
children: Map<string, TrieNode>;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
export type FormRefStoreState = {
|
|
27
|
-
formRefs: Map<string, React.RefObject<any>>;
|
|
28
|
-
registerFormRef: (id: string, ref: React.RefObject<any>) => void;
|
|
29
|
-
getFormRef: (id: string) => React.RefObject<any> | undefined;
|
|
30
|
-
removeFormRef: (id: string) => void;
|
|
31
|
-
getFormRefsByStateKey: (
|
|
32
|
-
stateKey: string
|
|
33
|
-
) => Map<string, React.RefObject<any>>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const formRefStore = create<FormRefStoreState>((set, get) => ({
|
|
37
|
-
formRefs: new Map(),
|
|
38
|
-
|
|
39
|
-
registerFormRef: (id, ref) =>
|
|
40
|
-
set((state) => {
|
|
41
|
-
const newRefs = new Map(state.formRefs);
|
|
42
|
-
newRefs.set(id, ref);
|
|
43
|
-
return { formRefs: newRefs };
|
|
44
|
-
}),
|
|
45
|
-
|
|
46
|
-
getFormRef: (id) => get().formRefs.get(id),
|
|
47
|
-
|
|
48
|
-
removeFormRef: (id) =>
|
|
49
|
-
set((state) => {
|
|
50
|
-
const newRefs = new Map(state.formRefs);
|
|
51
|
-
newRefs.delete(id);
|
|
52
|
-
return { formRefs: newRefs };
|
|
53
|
-
}),
|
|
54
|
-
|
|
55
|
-
getFormRefsByStateKey: (stateKey) => {
|
|
56
|
-
const allRefs = get().formRefs;
|
|
57
|
-
const stateKeyPrefix = stateKey + '.';
|
|
58
|
-
const filteredRefs = new Map();
|
|
59
|
-
|
|
60
|
-
allRefs.forEach((ref, id) => {
|
|
61
|
-
if (id.startsWith(stateKeyPrefix) || id === stateKey) {
|
|
62
|
-
filteredRefs.set(id, ref);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return filteredRefs;
|
|
67
|
-
},
|
|
68
|
-
}));
|
|
69
28
|
export type ComponentsType = {
|
|
70
29
|
components?: Map<
|
|
71
30
|
string,
|
|
@@ -99,7 +58,7 @@ export type ValidationState = {
|
|
|
99
58
|
lastValidated?: number;
|
|
100
59
|
validatedValue?: any;
|
|
101
60
|
};
|
|
102
|
-
export type
|
|
61
|
+
export type SchemaTypeInfo = {
|
|
103
62
|
type:
|
|
104
63
|
| 'string'
|
|
105
64
|
| 'number'
|
|
@@ -114,13 +73,66 @@ export type TypeInfo = {
|
|
|
114
73
|
nullable?: boolean;
|
|
115
74
|
optional?: boolean;
|
|
116
75
|
};
|
|
117
|
-
|
|
76
|
+
export type ClientActivityState = {
|
|
77
|
+
// ALL elements currently mounted for this path
|
|
78
|
+
elements: Map<
|
|
79
|
+
string,
|
|
80
|
+
{
|
|
81
|
+
// componentId -> element info
|
|
82
|
+
domRef: React.RefObject<HTMLElement>;
|
|
83
|
+
elementType:
|
|
84
|
+
| 'input'
|
|
85
|
+
| 'textarea'
|
|
86
|
+
| 'select'
|
|
87
|
+
| 'checkbox'
|
|
88
|
+
| 'radio'
|
|
89
|
+
| 'range'
|
|
90
|
+
| 'file'
|
|
91
|
+
| 'custom';
|
|
92
|
+
inputType?: string; // For input elements: 'text', 'number', 'date', etc.
|
|
93
|
+
mountedAt: number;
|
|
94
|
+
|
|
95
|
+
// Current activity for THIS specific element
|
|
96
|
+
currentActivity?: {
|
|
97
|
+
type:
|
|
98
|
+
| 'focus'
|
|
99
|
+
| 'blur'
|
|
100
|
+
| 'input'
|
|
101
|
+
| 'select'
|
|
102
|
+
| 'hover'
|
|
103
|
+
| 'scroll'
|
|
104
|
+
| 'cursor';
|
|
105
|
+
startTime: number;
|
|
106
|
+
details?: {
|
|
107
|
+
value?: any;
|
|
108
|
+
previousValue?: any;
|
|
109
|
+
inputLength?: number;
|
|
110
|
+
changeType?: 'keyboard' | 'paste' | 'drop' | 'select' | 'clear';
|
|
111
|
+
selectionStart?: number;
|
|
112
|
+
selectionEnd?: number;
|
|
113
|
+
selectedText?: string;
|
|
114
|
+
cursorPosition?: number;
|
|
115
|
+
scrollTop?: number;
|
|
116
|
+
scrollLeft?: number;
|
|
117
|
+
isComposing?: boolean;
|
|
118
|
+
keystrokeCount?: number;
|
|
119
|
+
key?: string;
|
|
120
|
+
optionCount?: number;
|
|
121
|
+
selectedIndex?: number;
|
|
122
|
+
checked?: boolean;
|
|
123
|
+
arrayOperation?: 'insert' | 'remove' | 'reorder';
|
|
124
|
+
arrayIndex?: number;
|
|
125
|
+
arrayLength?: number;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
>;
|
|
130
|
+
};
|
|
118
131
|
// Update ShadowMetadata to include typeInfo
|
|
119
132
|
export type ShadowMetadata = {
|
|
120
133
|
value?: any;
|
|
121
|
-
syncArrayIdPrefix?: string;
|
|
122
134
|
id?: string;
|
|
123
|
-
typeInfo?:
|
|
135
|
+
typeInfo?: SchemaTypeInfo;
|
|
124
136
|
stateSource?: 'default' | 'server' | 'localStorage';
|
|
125
137
|
lastServerSync?: number;
|
|
126
138
|
isDirty?: boolean;
|
|
@@ -134,8 +146,6 @@ export type ShadowMetadata = {
|
|
|
134
146
|
syncInfo?: { status: string };
|
|
135
147
|
validation?: ValidationState;
|
|
136
148
|
features?: {
|
|
137
|
-
syncEnabled: boolean;
|
|
138
|
-
validationEnabled: boolean;
|
|
139
149
|
localStorageEnabled: boolean;
|
|
140
150
|
};
|
|
141
151
|
signals?: Array<{
|
|
@@ -144,7 +154,6 @@ export type ShadowMetadata = {
|
|
|
144
154
|
position: number;
|
|
145
155
|
effect?: string;
|
|
146
156
|
}>;
|
|
147
|
-
|
|
148
157
|
transformCaches?: Map<
|
|
149
158
|
string,
|
|
150
159
|
{
|
|
@@ -154,6 +163,7 @@ export type ShadowMetadata = {
|
|
|
154
163
|
}
|
|
155
164
|
>;
|
|
156
165
|
pathComponents?: Set<string>;
|
|
166
|
+
|
|
157
167
|
streams?: Map<
|
|
158
168
|
string,
|
|
159
169
|
{
|
|
@@ -161,6 +171,10 @@ export type ShadowMetadata = {
|
|
|
161
171
|
flushTimer: NodeJS.Timeout | null;
|
|
162
172
|
}
|
|
163
173
|
>;
|
|
174
|
+
pluginMetaData?: Map<string, Record<string, any>>;
|
|
175
|
+
// formRef?: React.RefObject<any>;
|
|
176
|
+
// focusedElement?: { path: string[]; ref: React.RefObject<any> } | null;
|
|
177
|
+
clientActivityState?: ClientActivityState;
|
|
164
178
|
} & ComponentsType;
|
|
165
179
|
|
|
166
180
|
type ShadowNode = {
|
|
@@ -169,12 +183,28 @@ type ShadowNode = {
|
|
|
169
183
|
};
|
|
170
184
|
|
|
171
185
|
export type CogsGlobalState = {
|
|
186
|
+
getPluginMetaDataMap: (
|
|
187
|
+
key: string,
|
|
188
|
+
path: string[]
|
|
189
|
+
) => Map<string, Record<string, any>> | undefined;
|
|
190
|
+
setPluginMetaData: (
|
|
191
|
+
key: string,
|
|
192
|
+
path: string[],
|
|
193
|
+
pluginName: string,
|
|
194
|
+
data: Record<string, any>
|
|
195
|
+
) => void;
|
|
196
|
+
removePluginMetaData: (
|
|
197
|
+
key: string,
|
|
198
|
+
path: string[],
|
|
199
|
+
pluginName: string
|
|
200
|
+
) => void;
|
|
172
201
|
setTransformCache: (
|
|
173
202
|
key: string,
|
|
174
203
|
path: string[],
|
|
175
204
|
cacheKey: string,
|
|
176
205
|
cacheData: any
|
|
177
206
|
) => void;
|
|
207
|
+
initializeAndMergeShadowState: (key: string, initialState: any) => void;
|
|
178
208
|
initializeShadowState: (key: string, initialState: any) => void;
|
|
179
209
|
getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
|
|
180
210
|
getShadowMetadata: (
|
|
@@ -199,14 +229,14 @@ export type CogsGlobalState = {
|
|
|
199
229
|
addItemsToArrayNode: (
|
|
200
230
|
key: string,
|
|
201
231
|
arrayPath: string[],
|
|
202
|
-
newItems: any
|
|
203
|
-
newKeys: string[]
|
|
232
|
+
newItems: any
|
|
204
233
|
) => void;
|
|
205
234
|
insertShadowArrayElement: (
|
|
206
235
|
key: string,
|
|
207
236
|
arrayPath: string[],
|
|
208
237
|
newItem: any,
|
|
209
|
-
index?: number
|
|
238
|
+
index?: number,
|
|
239
|
+
itemId?: string
|
|
210
240
|
) => string;
|
|
211
241
|
removeShadowArrayElement: (key: string, itemPath: string[]) => void;
|
|
212
242
|
registerComponent: (
|
|
@@ -264,194 +294,138 @@ export type CogsGlobalState = {
|
|
|
264
294
|
setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
|
|
265
295
|
getSyncInfo: (key: string) => SyncInfo | null;
|
|
266
296
|
};
|
|
267
|
-
|
|
268
297
|
function getTypeFromZodSchema(
|
|
269
298
|
schema: any,
|
|
270
299
|
source: 'zod4' | 'zod3' | 'sync' = 'zod4'
|
|
271
|
-
):
|
|
300
|
+
): SchemaTypeInfo | null {
|
|
272
301
|
if (!schema) return null;
|
|
273
302
|
|
|
274
|
-
let
|
|
303
|
+
let current = schema;
|
|
275
304
|
let isNullable = false;
|
|
276
305
|
let isOptional = false;
|
|
277
306
|
let defaultValue: any = undefined;
|
|
278
307
|
let hasDefault = false;
|
|
279
308
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
current = current._def.innerType || current.unwrap();
|
|
294
|
-
} else if (typeName === 'ZodDefault') {
|
|
295
|
-
hasDefault = true;
|
|
296
|
-
defaultValue = current._def.defaultValue();
|
|
297
|
-
current = current._def.innerType;
|
|
298
|
-
} else if (typeName === 'ZodEffects') {
|
|
299
|
-
// Handle .refine(), .transform() etc
|
|
300
|
-
current = current._def.schema;
|
|
309
|
+
// This loop will now correctly navigate through any wrappers AND unions.
|
|
310
|
+
for (let i = 0; i < 20; i++) {
|
|
311
|
+
// Added a safety break for complex schemas
|
|
312
|
+
const def = current?.def || current?._def;
|
|
313
|
+
if (!def) break;
|
|
314
|
+
|
|
315
|
+
const typeIdentifier = def.typeName || def.type || current._type;
|
|
316
|
+
|
|
317
|
+
// --- START: THE CRITICAL FIX FOR ZodUnion ---
|
|
318
|
+
if (typeIdentifier === 'ZodUnion' || typeIdentifier === 'union') {
|
|
319
|
+
if (def.options && def.options.length > 0) {
|
|
320
|
+
current = def.options[0]; // Proceed by analyzing the FIRST option of the union
|
|
321
|
+
continue; // Restart the loop with the new schema
|
|
301
322
|
} else {
|
|
302
|
-
//
|
|
303
|
-
break;
|
|
323
|
+
break; // Union with no options, cannot determine type
|
|
304
324
|
}
|
|
305
325
|
}
|
|
326
|
+
// --- END: THE CRITICAL FIX ---
|
|
306
327
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
type: 'boolean',
|
|
331
|
-
schema: schema,
|
|
332
|
-
source,
|
|
333
|
-
default: hasDefault ? defaultValue : false,
|
|
334
|
-
nullable: isNullable,
|
|
335
|
-
optional: isOptional,
|
|
336
|
-
};
|
|
337
|
-
} else if (typeName === 'ZodArray') {
|
|
338
|
-
return {
|
|
339
|
-
type: 'array',
|
|
340
|
-
schema: schema,
|
|
341
|
-
source,
|
|
342
|
-
default: hasDefault ? defaultValue : [],
|
|
343
|
-
nullable: isNullable,
|
|
344
|
-
optional: isOptional,
|
|
345
|
-
};
|
|
346
|
-
} else if (typeName === 'ZodObject') {
|
|
347
|
-
return {
|
|
348
|
-
type: 'object',
|
|
349
|
-
schema: schema,
|
|
350
|
-
source,
|
|
351
|
-
default: hasDefault ? defaultValue : {},
|
|
352
|
-
nullable: isNullable,
|
|
353
|
-
optional: isOptional,
|
|
354
|
-
};
|
|
355
|
-
} else if (typeName === 'ZodDate') {
|
|
356
|
-
return {
|
|
357
|
-
type: 'date',
|
|
358
|
-
schema: schema,
|
|
359
|
-
source,
|
|
360
|
-
default: hasDefault ? defaultValue : new Date(),
|
|
361
|
-
nullable: isNullable,
|
|
362
|
-
optional: isOptional,
|
|
363
|
-
};
|
|
328
|
+
if (typeIdentifier === 'ZodOptional' || typeIdentifier === 'optional') {
|
|
329
|
+
isOptional = true;
|
|
330
|
+
} else if (
|
|
331
|
+
typeIdentifier === 'ZodNullable' ||
|
|
332
|
+
typeIdentifier === 'nullable'
|
|
333
|
+
) {
|
|
334
|
+
isNullable = true;
|
|
335
|
+
} else if (
|
|
336
|
+
typeIdentifier === 'ZodDefault' ||
|
|
337
|
+
typeIdentifier === 'default'
|
|
338
|
+
) {
|
|
339
|
+
hasDefault = true;
|
|
340
|
+
defaultValue =
|
|
341
|
+
typeof def.defaultValue === 'function'
|
|
342
|
+
? def.defaultValue()
|
|
343
|
+
: def.defaultValue;
|
|
344
|
+
} else if (
|
|
345
|
+
typeIdentifier !== 'ZodEffects' &&
|
|
346
|
+
typeIdentifier !== 'effects'
|
|
347
|
+
) {
|
|
348
|
+
// This is not a wrapper we need to unwrap further, so we can exit the loop.
|
|
349
|
+
break;
|
|
364
350
|
}
|
|
365
|
-
}
|
|
366
351
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Check for wrappers in v3
|
|
372
|
-
while (current) {
|
|
373
|
-
if (current._type === 'optional') {
|
|
374
|
-
isOptional = true;
|
|
375
|
-
current = current._def?.innerType || current._inner;
|
|
376
|
-
} else if (current._type === 'nullable') {
|
|
377
|
-
isNullable = true;
|
|
378
|
-
current = current._def?.innerType || current._inner;
|
|
379
|
-
} else if (current._def?.defaultValue !== undefined) {
|
|
380
|
-
hasDefault = true;
|
|
381
|
-
defaultValue =
|
|
382
|
-
typeof current._def.defaultValue === 'function'
|
|
383
|
-
? current._def.defaultValue()
|
|
384
|
-
: current._def.defaultValue;
|
|
385
|
-
break;
|
|
386
|
-
} else {
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
352
|
+
const nextSchema = def.innerType || def.schema || current._inner;
|
|
353
|
+
if (!nextSchema || nextSchema === current) {
|
|
354
|
+
break; // Reached the end or a recursive schema
|
|
389
355
|
}
|
|
356
|
+
current = nextSchema;
|
|
357
|
+
}
|
|
390
358
|
|
|
391
|
-
|
|
359
|
+
const baseSchema = current;
|
|
360
|
+
const baseDef = baseSchema?.def || baseSchema?._def;
|
|
361
|
+
const baseType = baseDef?.typeName || baseDef?.type || baseSchema?._type;
|
|
392
362
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
363
|
+
if (baseType === 'ZodNumber' || baseType === 'number') {
|
|
364
|
+
return {
|
|
365
|
+
type: 'number',
|
|
366
|
+
schema: schema,
|
|
367
|
+
source,
|
|
368
|
+
default: hasDefault ? defaultValue : 0,
|
|
369
|
+
nullable: isNullable,
|
|
370
|
+
optional: isOptional,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (baseType === 'ZodString' || baseType === 'string') {
|
|
374
|
+
return {
|
|
375
|
+
type: 'string',
|
|
376
|
+
schema: schema,
|
|
377
|
+
source,
|
|
378
|
+
default: hasDefault ? defaultValue : '',
|
|
379
|
+
nullable: isNullable,
|
|
380
|
+
optional: isOptional,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (baseType === 'ZodBoolean' || baseType === 'boolean') {
|
|
384
|
+
return {
|
|
385
|
+
type: 'boolean',
|
|
386
|
+
schema: schema,
|
|
387
|
+
source,
|
|
388
|
+
default: hasDefault ? defaultValue : false,
|
|
389
|
+
nullable: isNullable,
|
|
390
|
+
optional: isOptional,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (baseType === 'ZodArray' || baseType === 'array') {
|
|
394
|
+
return {
|
|
395
|
+
type: 'array',
|
|
396
|
+
schema: schema,
|
|
397
|
+
source,
|
|
398
|
+
default: hasDefault ? defaultValue : [],
|
|
399
|
+
nullable: isNullable,
|
|
400
|
+
optional: isOptional,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (baseType === 'ZodObject' || baseType === 'object') {
|
|
404
|
+
return {
|
|
405
|
+
type: 'object',
|
|
406
|
+
schema: schema,
|
|
407
|
+
source,
|
|
408
|
+
default: hasDefault ? defaultValue : {},
|
|
409
|
+
nullable: isNullable,
|
|
410
|
+
optional: isOptional,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (baseType === 'ZodDate' || baseType === 'date') {
|
|
414
|
+
return {
|
|
415
|
+
type: 'date',
|
|
416
|
+
schema: schema,
|
|
417
|
+
source,
|
|
418
|
+
default: hasDefault ? defaultValue : new Date(),
|
|
419
|
+
nullable: isNullable,
|
|
420
|
+
optional: isOptional,
|
|
421
|
+
};
|
|
448
422
|
}
|
|
449
423
|
|
|
450
424
|
return null;
|
|
451
425
|
}
|
|
452
426
|
|
|
453
427
|
// Helper to get type info from runtime value
|
|
454
|
-
function getTypeFromValue(value: any):
|
|
428
|
+
function getTypeFromValue(value: any): SchemaTypeInfo {
|
|
455
429
|
if (value === null) {
|
|
456
430
|
return {
|
|
457
431
|
type: 'unknown',
|
|
@@ -499,227 +473,273 @@ type BuildContext = {
|
|
|
499
473
|
zodV3?: any;
|
|
500
474
|
};
|
|
501
475
|
};
|
|
502
|
-
|
|
476
|
+
|
|
503
477
|
export function buildShadowNode(
|
|
504
478
|
stateKey: string,
|
|
505
479
|
value: any,
|
|
506
480
|
context?: BuildContext
|
|
507
481
|
): ShadowNode {
|
|
508
|
-
//
|
|
482
|
+
// Handle null/undefined/primitives (This part is already correct)
|
|
509
483
|
if (value === null || value === undefined || typeof value !== 'object') {
|
|
510
|
-
const node: ShadowNode = { _meta: {} };
|
|
511
|
-
node._meta!.
|
|
512
|
-
if (context) {
|
|
513
|
-
let typeInfo: TypeInfo | null = null;
|
|
514
|
-
|
|
515
|
-
// 1. Try to get type from sync schema
|
|
516
|
-
if (context.schemas.sync && context.schemas.sync[context.stateKey]) {
|
|
517
|
-
const syncEntry = context.schemas.sync[context.stateKey];
|
|
518
|
-
if (syncEntry.schemas?.validation) {
|
|
519
|
-
// Navigate to the field in the validation schema
|
|
520
|
-
let fieldSchema = syncEntry.schemas.validation;
|
|
521
|
-
for (const segment of context.path) {
|
|
522
|
-
if (fieldSchema?.shape) {
|
|
523
|
-
fieldSchema = fieldSchema.shape[segment];
|
|
524
|
-
} else if (fieldSchema?._def?.shape) {
|
|
525
|
-
fieldSchema = fieldSchema._def.shape()[segment];
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (fieldSchema) {
|
|
530
|
-
typeInfo = getTypeFromZodSchema(fieldSchema, 'sync');
|
|
531
|
-
if (typeInfo) {
|
|
532
|
-
// Use the default from sync schema if available
|
|
533
|
-
if (syncEntry.schemas.defaults) {
|
|
534
|
-
let defaultValue = syncEntry.schemas.defaults;
|
|
535
|
-
for (const segment of context.path) {
|
|
536
|
-
if (defaultValue && typeof defaultValue === 'object') {
|
|
537
|
-
defaultValue = defaultValue[segment];
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
if (defaultValue !== undefined) {
|
|
541
|
-
typeInfo.default = defaultValue;
|
|
542
|
-
// If no value provided and not optional, use the default
|
|
543
|
-
if (
|
|
544
|
-
(value === undefined || value === null) &&
|
|
545
|
-
!typeInfo.optional
|
|
546
|
-
) {
|
|
547
|
-
node._meta!.value = defaultValue;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// 2. If no sync schema, try Zod v4
|
|
557
|
-
if (!typeInfo && context.schemas.zodV4) {
|
|
558
|
-
let fieldSchema = context.schemas.zodV4;
|
|
559
|
-
for (const segment of context.path) {
|
|
560
|
-
if (fieldSchema?.shape) {
|
|
561
|
-
fieldSchema = fieldSchema.shape[segment];
|
|
562
|
-
} else if (fieldSchema?._def?.shape) {
|
|
563
|
-
fieldSchema = fieldSchema._def.shape()[segment];
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (fieldSchema) {
|
|
568
|
-
typeInfo = getTypeFromZodSchema(fieldSchema, 'zod4');
|
|
569
|
-
if (typeInfo && (value === undefined || value === null)) {
|
|
570
|
-
// Only use default if the field is not optional/nullable
|
|
571
|
-
if (!typeInfo.optional && !typeInfo.nullable) {
|
|
572
|
-
node.value = typeInfo.default;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// 3. If no Zod v4, try Zod v3
|
|
579
|
-
if (!typeInfo && context.schemas.zodV3) {
|
|
580
|
-
let fieldSchema = context.schemas.zodV3;
|
|
581
|
-
for (const segment of context.path) {
|
|
582
|
-
if (fieldSchema?.shape) {
|
|
583
|
-
fieldSchema = fieldSchema.shape[segment];
|
|
584
|
-
} else if (fieldSchema?._shape) {
|
|
585
|
-
fieldSchema = fieldSchema._shape[segment];
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (fieldSchema) {
|
|
590
|
-
typeInfo = getTypeFromZodSchema(fieldSchema, 'zod3');
|
|
591
|
-
if (typeInfo && (value === undefined || value === null)) {
|
|
592
|
-
// Only use default if the field is not optional/nullable
|
|
593
|
-
if (!typeInfo.optional && !typeInfo.nullable) {
|
|
594
|
-
node.value = typeInfo.default;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// 4. Fall back to runtime type
|
|
601
|
-
if (!typeInfo) {
|
|
602
|
-
typeInfo = getTypeFromValue(node._meta!.value);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Store the type info
|
|
606
|
-
if (typeInfo) {
|
|
607
|
-
if (!node._meta) node._meta = {};
|
|
608
|
-
node._meta.typeInfo = typeInfo;
|
|
609
|
-
}
|
|
610
|
-
} else {
|
|
611
|
-
// No context, just use runtime type
|
|
612
|
-
const typeInfo = getTypeFromValue(value);
|
|
613
|
-
if (!node._meta) node._meta = {};
|
|
614
|
-
node._meta.typeInfo = typeInfo;
|
|
615
|
-
}
|
|
616
|
-
|
|
484
|
+
const node: ShadowNode = { _meta: { value } };
|
|
485
|
+
node._meta!.typeInfo = getTypeInfoForPath(value, context);
|
|
617
486
|
return node;
|
|
618
487
|
}
|
|
619
488
|
|
|
620
|
-
//
|
|
489
|
+
// Handle arrays
|
|
621
490
|
if (Array.isArray(value)) {
|
|
622
|
-
|
|
623
|
-
const
|
|
491
|
+
// 1. Create the node for the array.
|
|
492
|
+
const node: ShadowNode = { _meta: { arrayKeys: [] } };
|
|
624
493
|
|
|
494
|
+
// 2. Get the type info for the array itself ONCE, right at the start.
|
|
495
|
+
node._meta!.typeInfo = getTypeInfoForPath(value, context);
|
|
496
|
+
|
|
497
|
+
// 3. THEN, recursively process the children.
|
|
625
498
|
value.forEach((item, index) => {
|
|
626
|
-
const itemId =
|
|
627
|
-
// Pass context down for array items
|
|
499
|
+
const itemId = generateId(stateKey);
|
|
628
500
|
const itemContext = context
|
|
629
501
|
? {
|
|
630
502
|
...context,
|
|
631
503
|
path: [...context.path, index.toString()],
|
|
632
504
|
}
|
|
633
505
|
: undefined;
|
|
634
|
-
arrayNode[itemId] = buildShadowNode(stateKey, item, itemContext);
|
|
635
|
-
idKeys.push(itemId);
|
|
636
|
-
});
|
|
637
506
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
let arraySchema = null;
|
|
642
|
-
|
|
643
|
-
if (context.schemas.zodV4) {
|
|
644
|
-
let fieldSchema = context.schemas.zodV4;
|
|
645
|
-
for (const segment of context.path) {
|
|
646
|
-
if (fieldSchema?.shape) {
|
|
647
|
-
fieldSchema = fieldSchema.shape[segment];
|
|
648
|
-
} else if (fieldSchema?._def?.shape) {
|
|
649
|
-
fieldSchema = fieldSchema._def.shape()[segment];
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
arraySchema = fieldSchema;
|
|
653
|
-
}
|
|
507
|
+
node[itemId] = buildShadowNode(stateKey, item, itemContext);
|
|
508
|
+
node._meta!.arrayKeys!.push(itemId);
|
|
509
|
+
});
|
|
654
510
|
|
|
655
|
-
|
|
656
|
-
type: 'array',
|
|
657
|
-
schema: arraySchema,
|
|
658
|
-
source: arraySchema ? 'zod4' : 'runtime',
|
|
659
|
-
default: [],
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
return arrayNode;
|
|
511
|
+
return node;
|
|
663
512
|
}
|
|
664
513
|
|
|
665
|
-
//
|
|
514
|
+
// Handle objects
|
|
666
515
|
if (value.constructor === Object) {
|
|
667
|
-
|
|
516
|
+
// 1. Create the node for the object.
|
|
517
|
+
const node: ShadowNode = { _meta: {} };
|
|
518
|
+
|
|
519
|
+
// 2. Get the type info for the object itself ONCE, right at the start.
|
|
520
|
+
node._meta!.typeInfo = getTypeInfoForPath(value, context);
|
|
521
|
+
|
|
522
|
+
// 3. THEN, recursively process the children.
|
|
668
523
|
for (const key in value) {
|
|
669
524
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
670
|
-
// Pass context down for object properties
|
|
671
525
|
const propContext = context
|
|
672
526
|
? {
|
|
673
527
|
...context,
|
|
674
528
|
path: [...context.path, key],
|
|
675
529
|
}
|
|
676
530
|
: undefined;
|
|
677
|
-
|
|
531
|
+
|
|
532
|
+
node[key] = buildShadowNode(stateKey, value[key], propContext);
|
|
678
533
|
}
|
|
679
534
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
535
|
+
|
|
536
|
+
return node;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Fallback for other object types (Date, class instances, etc.)
|
|
540
|
+
return {
|
|
541
|
+
_meta: {
|
|
542
|
+
value: value,
|
|
543
|
+
typeInfo: getTypeFromValue(value),
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Helper function to get type info (extracted for clarity)
|
|
549
|
+
function getTypeInfoForPath(
|
|
550
|
+
value: any,
|
|
551
|
+
context?: BuildContext
|
|
552
|
+
): SchemaTypeInfo {
|
|
553
|
+
if (context) {
|
|
554
|
+
// Try to get schema-based type info
|
|
555
|
+
let typeInfo: SchemaTypeInfo | null = null;
|
|
556
|
+
|
|
557
|
+
if (context.schemas.zodV4) {
|
|
558
|
+
const schema =
|
|
559
|
+
context.path.length === 0
|
|
560
|
+
? context.schemas.zodV4
|
|
561
|
+
: getSchemaAtPath(context.schemas.zodV4, context.path);
|
|
562
|
+
if (schema) {
|
|
563
|
+
typeInfo = getTypeFromZodSchema(schema, 'zod4');
|
|
694
564
|
}
|
|
565
|
+
}
|
|
695
566
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
567
|
+
if (!typeInfo && context.schemas.zodV3) {
|
|
568
|
+
const schema =
|
|
569
|
+
context.path.length === 0
|
|
570
|
+
? context.schemas.zodV3
|
|
571
|
+
: getSchemaAtPath(context.schemas.zodV3, context.path);
|
|
572
|
+
if (schema) {
|
|
573
|
+
typeInfo = getTypeFromZodSchema(schema, 'zod3');
|
|
574
|
+
}
|
|
702
575
|
}
|
|
703
|
-
|
|
576
|
+
|
|
577
|
+
if (!typeInfo && context.schemas.sync?.[context.stateKey]) {
|
|
578
|
+
typeInfo = getTypeFromValue(value);
|
|
579
|
+
typeInfo.source = 'sync';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (typeInfo) return typeInfo;
|
|
704
583
|
}
|
|
705
584
|
|
|
706
|
-
return
|
|
585
|
+
return getTypeFromValue(value);
|
|
707
586
|
}
|
|
708
587
|
|
|
709
|
-
|
|
710
|
-
|
|
588
|
+
export function updateShadowTypeInfo(
|
|
589
|
+
stateKey: string,
|
|
590
|
+
rootSchema: any,
|
|
591
|
+
source: 'zod4' | 'zod3'
|
|
592
|
+
) {
|
|
593
|
+
const rootNode =
|
|
594
|
+
shadowStateStore.get(stateKey) || shadowStateStore.get(`[${stateKey}`);
|
|
595
|
+
if (!rootNode) return;
|
|
596
|
+
|
|
597
|
+
function updateNodeTypeInfo(node: any, path: string[]) {
|
|
598
|
+
if (!node || typeof node !== 'object') return;
|
|
599
|
+
const fieldSchema = getSchemaAtPath(rootSchema, path);
|
|
600
|
+
|
|
601
|
+
if (fieldSchema) {
|
|
602
|
+
const typeInfo = getTypeFromZodSchema(fieldSchema, source);
|
|
603
|
+
if (typeInfo) {
|
|
604
|
+
if (!node._meta) node._meta = {};
|
|
605
|
+
node._meta.typeInfo = {
|
|
606
|
+
...typeInfo,
|
|
607
|
+
schema: fieldSchema,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Recursively update children
|
|
613
|
+
if (node._meta?.arrayKeys) {
|
|
614
|
+
node._meta.arrayKeys.forEach((itemKey: string) => {
|
|
615
|
+
if (node[itemKey]) {
|
|
616
|
+
updateNodeTypeInfo(node[itemKey], [...path, '0']); // Use index 0 for array item schema
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
} else if (!node._meta?.hasOwnProperty('value')) {
|
|
620
|
+
// It's an object - update each property
|
|
621
|
+
Object.keys(node).forEach((key) => {
|
|
622
|
+
if (key !== '_meta') {
|
|
623
|
+
updateNodeTypeInfo(node[key], [...path, key]);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
updateNodeTypeInfo(rootNode, []);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Reliably unwraps a Zod schema to its core type, handling modifiers
|
|
634
|
+
* from both Zod v3 and modern Zod.
|
|
635
|
+
*/
|
|
636
|
+
function unwrapSchema(schema: any): any {
|
|
637
|
+
let current = schema;
|
|
638
|
+
while (current) {
|
|
639
|
+
// Version-agnostic way to get the definition object
|
|
640
|
+
const def = current.def || current._def;
|
|
641
|
+
|
|
642
|
+
// VITAL FIX: Check for `def.type` (like in your log), `def.typeName` (modern Zod), and `_type` (zod v3)
|
|
643
|
+
const typeIdentifier = def?.typeName || def?.type || current._type;
|
|
711
644
|
|
|
712
|
-
|
|
713
|
-
|
|
645
|
+
if (
|
|
646
|
+
typeIdentifier === 'ZodOptional' ||
|
|
647
|
+
typeIdentifier === 'optional' ||
|
|
648
|
+
typeIdentifier === 'ZodNullable' ||
|
|
649
|
+
typeIdentifier === 'nullable' ||
|
|
650
|
+
typeIdentifier === 'ZodDefault' ||
|
|
651
|
+
typeIdentifier === 'default' ||
|
|
652
|
+
typeIdentifier === 'ZodEffects' ||
|
|
653
|
+
typeIdentifier === 'effects'
|
|
654
|
+
) {
|
|
655
|
+
// Get the inner schema, supporting multiple internal structures
|
|
656
|
+
current =
|
|
657
|
+
def.innerType || def.schema || current._inner || current.unwrap?.();
|
|
658
|
+
} else {
|
|
659
|
+
break; // Reached the base schema
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return current;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Helper function to get a nested schema at a specific path,
|
|
667
|
+
* correctly handling both Zod v3 and modern Zod internals.
|
|
668
|
+
*/
|
|
669
|
+
function getSchemaAtPath(schema: any, path: string[]): any {
|
|
670
|
+
if (!schema) return null;
|
|
671
|
+
if (path.length === 0) return schema;
|
|
672
|
+
|
|
673
|
+
let currentSchema = schema;
|
|
674
|
+
|
|
675
|
+
for (const segment of path) {
|
|
676
|
+
const containerSchema = unwrapSchema(currentSchema);
|
|
677
|
+
if (!containerSchema) return null;
|
|
678
|
+
|
|
679
|
+
const def = containerSchema.def || containerSchema._def;
|
|
680
|
+
|
|
681
|
+
// VITAL FIX: Check for `def.type` as you discovered.
|
|
682
|
+
const typeIdentifier = def?.typeName || def?.type || containerSchema._type;
|
|
683
|
+
|
|
684
|
+
if (typeIdentifier === 'ZodObject' || typeIdentifier === 'object') {
|
|
685
|
+
// VITAL FIX: Check for `shape` inside `def` first, then on the schema itself.
|
|
686
|
+
const shape =
|
|
687
|
+
def?.shape || containerSchema.shape || containerSchema._shape;
|
|
688
|
+
currentSchema = shape?.[segment];
|
|
689
|
+
} else if (typeIdentifier === 'ZodArray' || typeIdentifier === 'array') {
|
|
690
|
+
// For arrays, the next schema is always the element's schema.
|
|
691
|
+
currentSchema = containerSchema.element || def?.type;
|
|
692
|
+
} else {
|
|
693
|
+
return null; // Not a container, cannot traverse deeper.
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!currentSchema) {
|
|
697
|
+
return null; // Path segment does not exist in the schema.
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return currentSchema;
|
|
702
|
+
}
|
|
703
|
+
export const shadowStateStore = new Map<string, ShadowNode>();
|
|
714
704
|
let globalCounter = 0;
|
|
705
|
+
const instanceId = Date.now().toString(36);
|
|
715
706
|
|
|
716
707
|
export function generateId(stateKey: string): string {
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
return `id:${prefix}_${(globalCounter++).toString(36)}`;
|
|
708
|
+
const prefix = 'local';
|
|
709
|
+
|
|
710
|
+
return `id:${prefix}_${instanceId}_${(globalCounter++).toString(36)}`;
|
|
720
711
|
}
|
|
721
712
|
export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
722
|
-
|
|
713
|
+
getPluginMetaDataMap: (
|
|
714
|
+
key: string,
|
|
715
|
+
path: string[]
|
|
716
|
+
): Map<string, Record<string, any>> | undefined => {
|
|
717
|
+
const metadata = get().getShadowMetadata(key, path);
|
|
718
|
+
return metadata?.pluginMetaData;
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
setPluginMetaData: (
|
|
722
|
+
key: string,
|
|
723
|
+
path: string[], // ADD THIS PARAMETER
|
|
724
|
+
pluginName: string,
|
|
725
|
+
data: Record<string, any>
|
|
726
|
+
) => {
|
|
727
|
+
const metadata = get().getShadowMetadata(key, path) || {}; // Use the path!
|
|
728
|
+
const pluginMetaData = new Map(metadata.pluginMetaData || []);
|
|
729
|
+
const existingData = pluginMetaData.get(pluginName) || {};
|
|
730
|
+
pluginMetaData.set(pluginName, { ...existingData, ...data });
|
|
731
|
+
get().setShadowMetadata(key, path, { ...metadata, pluginMetaData });
|
|
732
|
+
get().notifyPathSubscribers([key, ...path].join('.'), {
|
|
733
|
+
type: 'METADATA_UPDATE',
|
|
734
|
+
});
|
|
735
|
+
},
|
|
736
|
+
removePluginMetaData: (key: string, path: string[], pluginName: string) => {
|
|
737
|
+
const metadata = get().getShadowMetadata(key, path);
|
|
738
|
+
if (!metadata?.pluginMetaData) return;
|
|
739
|
+
const pluginMetaData = new Map(metadata.pluginMetaData);
|
|
740
|
+
pluginMetaData.delete(pluginName);
|
|
741
|
+
get().setShadowMetadata(key, path, { ...metadata, pluginMetaData });
|
|
742
|
+
},
|
|
723
743
|
|
|
724
744
|
setTransformCache: (
|
|
725
745
|
key: string,
|
|
@@ -736,7 +756,151 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
736
756
|
transformCaches: metadata.transformCaches,
|
|
737
757
|
});
|
|
738
758
|
},
|
|
759
|
+
// Replace your entire `initializeAndMergeShadowState` function with this one.
|
|
760
|
+
|
|
761
|
+
initializeAndMergeShadowState: (key: string, shadowState: any) => {
|
|
762
|
+
const isArrayState = shadowState?._meta?.arrayKeys !== undefined;
|
|
763
|
+
const storageKey = isArrayState ? `[${key}` : key;
|
|
764
|
+
|
|
765
|
+
const existingRoot =
|
|
766
|
+
shadowStateStore.get(storageKey) ||
|
|
767
|
+
shadowStateStore.get(key) ||
|
|
768
|
+
shadowStateStore.get(`[${key}`);
|
|
769
|
+
|
|
770
|
+
// --- THIS LOGIC IS RESTORED ---
|
|
771
|
+
// This is vital for preserving component registrations and other top-level
|
|
772
|
+
// metadata across a full merge/replace, which is why removing it was a mistake.
|
|
773
|
+
let preservedMetadata: Partial<ShadowMetadata> = {};
|
|
774
|
+
if (existingRoot?._meta) {
|
|
775
|
+
const {
|
|
776
|
+
components,
|
|
777
|
+
features,
|
|
778
|
+
lastServerSync,
|
|
779
|
+
stateSource,
|
|
780
|
+
baseServerState,
|
|
781
|
+
pathComponents,
|
|
782
|
+
signals,
|
|
783
|
+
validation,
|
|
784
|
+
} = existingRoot._meta;
|
|
785
|
+
|
|
786
|
+
if (components) preservedMetadata.components = components;
|
|
787
|
+
if (features) preservedMetadata.features = features;
|
|
788
|
+
if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
|
|
789
|
+
if (stateSource) preservedMetadata.stateSource = stateSource;
|
|
790
|
+
if (baseServerState) preservedMetadata.baseServerState = baseServerState;
|
|
791
|
+
if (pathComponents) preservedMetadata.pathComponents = pathComponents;
|
|
792
|
+
if (signals) preservedMetadata.signals = signals;
|
|
793
|
+
if (validation) preservedMetadata.validation = validation;
|
|
794
|
+
}
|
|
795
|
+
function deepMergeShadowNodes(target: ShadowNode, source: ShadowNode) {
|
|
796
|
+
// --- START: CORRECTED, MORE ROBUST METADATA MERGE ---
|
|
797
|
+
if (source._meta || target._meta) {
|
|
798
|
+
const existingMeta = target._meta || {};
|
|
799
|
+
const sourceMeta = source._meta || {};
|
|
800
|
+
|
|
801
|
+
// Combine metadata, letting the source overwrite simple, top-level properties.
|
|
802
|
+
const newMeta = { ...existingMeta, ...sourceMeta };
|
|
803
|
+
|
|
804
|
+
// CRITICAL FIX: Now, explicitly check and preserve the complex, valuable
|
|
805
|
+
// objects from the existing state if the incoming source state doesn't have
|
|
806
|
+
// an equally good or better version.
|
|
807
|
+
|
|
808
|
+
// 1. Preserve rich TypeInfo (with a schema) over a simple runtime one.
|
|
809
|
+
if (existingMeta.typeInfo?.schema && !sourceMeta.typeInfo?.schema) {
|
|
810
|
+
newMeta.typeInfo = existingMeta.typeInfo;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 2. Preserve the existing validation state, which is computed and stored on the target.
|
|
814
|
+
// A source built from a plain object will never have this.
|
|
815
|
+
if (existingMeta.validation && !sourceMeta.validation) {
|
|
816
|
+
newMeta.validation = existingMeta.validation;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 3. Preserve component registrations, which only exist on the live target state.
|
|
820
|
+
if (existingMeta.components) {
|
|
821
|
+
newMeta.components = existingMeta.components;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
target._meta = newMeta;
|
|
825
|
+
}
|
|
826
|
+
// --- END: CORRECTED METADATA MERGE ---
|
|
827
|
+
|
|
828
|
+
// 2. Handle the node's data (primitive, array, or object).
|
|
829
|
+
if (source._meta?.hasOwnProperty('value')) {
|
|
830
|
+
// Source is a primitive. Clear any old child properties from target.
|
|
831
|
+
for (const key in target) {
|
|
832
|
+
if (key !== '_meta') delete target[key];
|
|
833
|
+
}
|
|
834
|
+
return; // Done with this branch
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Synchronize the data structure based on the source.
|
|
838
|
+
const sourceKeys = new Set(
|
|
839
|
+
Object.keys(source).filter((k) => k !== '_meta')
|
|
840
|
+
);
|
|
841
|
+
const targetKeys = new Set(
|
|
842
|
+
Object.keys(target).filter((k) => k !== '_meta')
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// Delete keys that are in the target but no longer in the source.
|
|
846
|
+
for (const key of targetKeys) {
|
|
847
|
+
if (!sourceKeys.has(key)) {
|
|
848
|
+
delete target[key];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Recursively merge or add keys from the source.
|
|
853
|
+
for (const key of sourceKeys) {
|
|
854
|
+
const sourceValue = source[key];
|
|
855
|
+
const targetValue = target[key];
|
|
856
|
+
if (
|
|
857
|
+
targetValue &&
|
|
858
|
+
typeof targetValue === 'object' &&
|
|
859
|
+
sourceValue &&
|
|
860
|
+
typeof sourceValue === 'object'
|
|
861
|
+
) {
|
|
862
|
+
deepMergeShadowNodes(targetValue, sourceValue); // Recurse for objects
|
|
863
|
+
} else {
|
|
864
|
+
target[key] = sourceValue; // Add new or replace primitive/node
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// --- THIS IS YOUR ORIGINAL, CORRECT MAIN LOGIC ---
|
|
869
|
+
if (existingRoot) {
|
|
870
|
+
// Merge the new shadow state into the existing one
|
|
871
|
+
deepMergeShadowNodes(existingRoot, shadowState);
|
|
872
|
+
// Restore preserved metadata
|
|
873
|
+
if (!existingRoot._meta) existingRoot._meta = {};
|
|
874
|
+
Object.assign(existingRoot._meta, preservedMetadata);
|
|
875
|
+
shadowStateStore.set(storageKey, existingRoot);
|
|
876
|
+
} else {
|
|
877
|
+
// The logic for when no state exists yet
|
|
878
|
+
if (preservedMetadata && Object.keys(preservedMetadata).length > 0) {
|
|
879
|
+
if (!shadowState._meta) shadowState._meta = {};
|
|
880
|
+
Object.assign(shadowState._meta, preservedMetadata);
|
|
881
|
+
}
|
|
882
|
+
shadowStateStore.set(storageKey, shadowState);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// As your logs show, this part works. It runs AFTER the merge to apply schemas.
|
|
886
|
+
const options = get().getInitialOptions(key);
|
|
887
|
+
const hasSchema =
|
|
888
|
+
options?.validation?.zodSchemaV4 || options?.validation?.zodSchemaV3;
|
|
889
|
+
if (hasSchema) {
|
|
890
|
+
if (options.validation?.zodSchemaV4) {
|
|
891
|
+
updateShadowTypeInfo(key, options.validation.zodSchemaV4, 'zod4');
|
|
892
|
+
} else if (options.validation?.zodSchemaV3) {
|
|
893
|
+
updateShadowTypeInfo(key, options.validation.zodSchemaV3, 'zod3');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
739
896
|
|
|
897
|
+
// Cleanup logic is restored
|
|
898
|
+
if (storageKey === key) {
|
|
899
|
+
shadowStateStore.delete(`[${key}`);
|
|
900
|
+
} else {
|
|
901
|
+
shadowStateStore.delete(key);
|
|
902
|
+
}
|
|
903
|
+
},
|
|
740
904
|
initializeShadowState: (key: string, initialState: any) => {
|
|
741
905
|
const existingRoot =
|
|
742
906
|
shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
|
|
@@ -833,60 +997,75 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
833
997
|
if (!current._meta) {
|
|
834
998
|
current._meta = {};
|
|
835
999
|
}
|
|
1000
|
+
|
|
836
1001
|
Object.assign(current._meta, newMetadata);
|
|
837
1002
|
},
|
|
1003
|
+
getShadowValue: (key: string, path: string[], validArrayIds?: string[]) => {
|
|
1004
|
+
const startNode = get().getShadowNode(key, path);
|
|
838
1005
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
log?: boolean
|
|
844
|
-
) => {
|
|
845
|
-
const node = get().getShadowNode(key, path);
|
|
1006
|
+
// If the path is invalid or leads nowhere, return undefined immediately.
|
|
1007
|
+
if (!startNode) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
846
1010
|
|
|
847
|
-
|
|
1011
|
+
// --- High-Performance Iterative Materializer ---
|
|
848
1012
|
|
|
849
|
-
|
|
1013
|
+
// A single root object to hold the final, materialized result.
|
|
1014
|
+
const rootResult: any = {};
|
|
850
1015
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
) {
|
|
857
|
-
return node._meta.value;
|
|
858
|
-
}
|
|
1016
|
+
// Stack to manage the traversal without recursion.
|
|
1017
|
+
// Each item is [shadowNode, parentObjectInResult, keyToSetOnParent]
|
|
1018
|
+
const stack: [ShadowNode, any, string | number][] = [
|
|
1019
|
+
[startNode, rootResult, 'final'],
|
|
1020
|
+
];
|
|
859
1021
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1022
|
+
while (stack.length > 0) {
|
|
1023
|
+
const [currentNode, parentResult, resultKey] = stack.pop()!;
|
|
1024
|
+
|
|
1025
|
+
// 1. Handle primitive values
|
|
1026
|
+
if (currentNode._meta?.hasOwnProperty('value')) {
|
|
1027
|
+
parentResult[resultKey] = currentNode._meta.value;
|
|
1028
|
+
continue; // Done with this branch
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// 2. Handle arrays
|
|
1032
|
+
if (currentNode._meta?.arrayKeys) {
|
|
1033
|
+
const keysToIterate = validArrayIds || currentNode._meta.arrayKeys;
|
|
1034
|
+
const newArray: any[] = [];
|
|
1035
|
+
parentResult[resultKey] = newArray;
|
|
1036
|
+
|
|
1037
|
+
// Push children onto the stack in reverse order to process them from 0 to N
|
|
1038
|
+
for (let i = keysToIterate.length - 1; i >= 0; i--) {
|
|
1039
|
+
const itemKey = keysToIterate[i]!;
|
|
1040
|
+
if (currentNode[itemKey]) {
|
|
1041
|
+
// The child's result will be placed at index `i` in `newArray`
|
|
1042
|
+
stack.push([currentNode[itemKey], newArray, i]);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
continue; // Done with this branch
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// 3. Handle objects
|
|
1049
|
+
const newObject: any = {};
|
|
1050
|
+
parentResult[resultKey] = newObject;
|
|
873
1051
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1052
|
+
const objectKeys = Object.keys(currentNode);
|
|
1053
|
+
// Push children onto the stack (order doesn't matter for objects)
|
|
1054
|
+
for (const propKey of objectKeys) {
|
|
1055
|
+
if (propKey !== '_meta') {
|
|
1056
|
+
// The child's result will be set as a property on `newObject`
|
|
1057
|
+
stack.push([currentNode[propKey], newObject, propKey]);
|
|
1058
|
+
}
|
|
878
1059
|
}
|
|
879
1060
|
}
|
|
880
|
-
return result;
|
|
881
|
-
},
|
|
882
1061
|
|
|
1062
|
+
return rootResult.final;
|
|
1063
|
+
},
|
|
883
1064
|
updateShadowAtPath: (key, path, newValue) => {
|
|
884
|
-
// NO MORE set() wrapper - direct mutation!
|
|
885
1065
|
const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
|
|
886
1066
|
let root = shadowStateStore.get(rootKey);
|
|
887
1067
|
if (!root) return;
|
|
888
1068
|
|
|
889
|
-
// Navigate to parent without cloning
|
|
890
1069
|
let parentNode = root;
|
|
891
1070
|
for (let i = 0; i < path.length - 1; i++) {
|
|
892
1071
|
if (!parentNode[path[i]!]) {
|
|
@@ -894,72 +1073,141 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
894
1073
|
}
|
|
895
1074
|
parentNode = parentNode[path[i]!];
|
|
896
1075
|
}
|
|
897
|
-
|
|
898
1076
|
const targetNode =
|
|
899
1077
|
path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
|
|
900
1078
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function intelligentMerge(nodeToUpdate: any, plainValue: any) {
|
|
1079
|
+
// This function is now defined inside to close over 'key' and 'path' for context
|
|
1080
|
+
function intelligentMerge(
|
|
1081
|
+
nodeToUpdate: any,
|
|
1082
|
+
plainValue: any,
|
|
1083
|
+
currentPath: string[]
|
|
1084
|
+
) {
|
|
1085
|
+
// 1. Handle primitives (but NOT arrays)
|
|
911
1086
|
if (
|
|
912
1087
|
typeof plainValue !== 'object' ||
|
|
913
1088
|
plainValue === null ||
|
|
914
|
-
|
|
1089
|
+
plainValue instanceof Date
|
|
915
1090
|
) {
|
|
916
|
-
const oldMeta = nodeToUpdate._meta;
|
|
917
|
-
// Clear
|
|
918
|
-
for (const
|
|
919
|
-
if (
|
|
1091
|
+
const oldMeta = nodeToUpdate._meta || {};
|
|
1092
|
+
// Clear all child properties
|
|
1093
|
+
for (const prop in nodeToUpdate) {
|
|
1094
|
+
if (prop !== '_meta') delete nodeToUpdate[prop];
|
|
1095
|
+
}
|
|
1096
|
+
// Set the new primitive value, preserving metadata
|
|
1097
|
+
nodeToUpdate._meta = { ...oldMeta, value: plainValue };
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// 2. Handle Arrays INTELLIGENTLY
|
|
1102
|
+
if (Array.isArray(plainValue)) {
|
|
1103
|
+
// Ensure the target is a shadow array node
|
|
1104
|
+
if (!nodeToUpdate._meta) nodeToUpdate._meta = {};
|
|
1105
|
+
if (!nodeToUpdate._meta.arrayKeys) nodeToUpdate._meta.arrayKeys = [];
|
|
1106
|
+
|
|
1107
|
+
const existingKeys = nodeToUpdate._meta.arrayKeys;
|
|
1108
|
+
const newValues = plainValue;
|
|
1109
|
+
|
|
1110
|
+
const updatedKeys: string[] = [];
|
|
1111
|
+
|
|
1112
|
+
// Merge existing items and add new items
|
|
1113
|
+
for (let i = 0; i < newValues.length; i++) {
|
|
1114
|
+
const newItemValue = newValues[i]!;
|
|
1115
|
+
if (i < existingKeys.length) {
|
|
1116
|
+
// Merge into existing item, preserving its key and metadata
|
|
1117
|
+
const existingKey = existingKeys[i]!;
|
|
1118
|
+
intelligentMerge(nodeToUpdate[existingKey], newItemValue, [
|
|
1119
|
+
...currentPath,
|
|
1120
|
+
existingKey,
|
|
1121
|
+
]);
|
|
1122
|
+
updatedKeys.push(existingKey);
|
|
1123
|
+
} else {
|
|
1124
|
+
// Add a new item
|
|
1125
|
+
const newItemId = generateId(key);
|
|
1126
|
+
const options = get().getInitialOptions(key);
|
|
1127
|
+
// Build the new node WITH proper context to get schema info
|
|
1128
|
+
const itemContext: BuildContext = {
|
|
1129
|
+
stateKey: key,
|
|
1130
|
+
path: [...currentPath, '0'], // Use '0' for array element schema lookup
|
|
1131
|
+
schemas: {
|
|
1132
|
+
zodV4: options?.validation?.zodSchemaV4,
|
|
1133
|
+
zodV3: options?.validation?.zodSchemaV3,
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
nodeToUpdate[newItemId] = buildShadowNode(
|
|
1137
|
+
key,
|
|
1138
|
+
newItemValue,
|
|
1139
|
+
itemContext
|
|
1140
|
+
);
|
|
1141
|
+
updatedKeys.push(newItemId);
|
|
1142
|
+
}
|
|
920
1143
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (
|
|
924
|
-
|
|
1144
|
+
|
|
1145
|
+
// Remove deleted items
|
|
1146
|
+
if (existingKeys.length > newValues.length) {
|
|
1147
|
+
const keysToDelete = existingKeys.slice(newValues.length);
|
|
1148
|
+
keysToDelete.forEach((keyToDelete: string) => {
|
|
1149
|
+
delete nodeToUpdate[keyToDelete];
|
|
1150
|
+
});
|
|
925
1151
|
}
|
|
1152
|
+
|
|
1153
|
+
// Update the keys array to reflect the new state
|
|
1154
|
+
nodeToUpdate._meta.arrayKeys = updatedKeys;
|
|
926
1155
|
return;
|
|
927
1156
|
}
|
|
928
1157
|
|
|
1158
|
+
// 3. Handle Objects
|
|
929
1159
|
const plainValueKeys = new Set(Object.keys(plainValue));
|
|
1160
|
+
if (nodeToUpdate._meta?.hasOwnProperty('value')) {
|
|
1161
|
+
// transitioning from primitive to object, clear the value
|
|
1162
|
+
delete nodeToUpdate._meta.value;
|
|
1163
|
+
}
|
|
930
1164
|
|
|
931
1165
|
for (const propKey of plainValueKeys) {
|
|
932
1166
|
const childValue = plainValue[propKey];
|
|
933
1167
|
if (nodeToUpdate[propKey]) {
|
|
934
|
-
intelligentMerge(nodeToUpdate[propKey], childValue
|
|
1168
|
+
intelligentMerge(nodeToUpdate[propKey], childValue, [
|
|
1169
|
+
...currentPath,
|
|
1170
|
+
propKey,
|
|
1171
|
+
]);
|
|
935
1172
|
} else {
|
|
936
|
-
|
|
1173
|
+
const options = get().getInitialOptions(key);
|
|
1174
|
+
const itemContext: BuildContext = {
|
|
1175
|
+
stateKey: key,
|
|
1176
|
+
path: [...currentPath, propKey],
|
|
1177
|
+
schemas: {
|
|
1178
|
+
zodV4: options?.validation?.zodSchemaV4,
|
|
1179
|
+
zodV3: options?.validation?.zodSchemaV3,
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
nodeToUpdate[propKey] = buildShadowNode(key, childValue, itemContext);
|
|
937
1183
|
}
|
|
938
1184
|
}
|
|
939
1185
|
|
|
1186
|
+
// Delete keys that no longer exist
|
|
940
1187
|
for (const nodeKey in nodeToUpdate) {
|
|
941
1188
|
if (
|
|
942
1189
|
nodeKey === '_meta' ||
|
|
943
1190
|
!Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
|
|
944
1191
|
)
|
|
945
1192
|
continue;
|
|
946
|
-
|
|
947
1193
|
if (!plainValueKeys.has(nodeKey)) {
|
|
948
1194
|
delete nodeToUpdate[nodeKey];
|
|
949
1195
|
}
|
|
950
1196
|
}
|
|
951
1197
|
}
|
|
952
1198
|
|
|
953
|
-
|
|
1199
|
+
if (!targetNode) {
|
|
1200
|
+
parentNode[path[path.length - 1]!] = buildShadowNode(key, newValue); // Build fresh if no target
|
|
1201
|
+
} else {
|
|
1202
|
+
intelligentMerge(targetNode, newValue, path); // Use the new intelligent merge
|
|
1203
|
+
}
|
|
954
1204
|
|
|
955
1205
|
get().notifyPathSubscribers([key, ...path].join('.'), {
|
|
956
1206
|
type: 'UPDATE',
|
|
957
1207
|
newValue,
|
|
958
1208
|
});
|
|
959
1209
|
},
|
|
960
|
-
|
|
961
|
-
addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
|
|
962
|
-
// Direct mutation - no cloning!
|
|
1210
|
+
addItemsToArrayNode: (key, arrayPath, newItems) => {
|
|
963
1211
|
const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
|
|
964
1212
|
let root = shadowStateStore.get(rootKey);
|
|
965
1213
|
if (!root) {
|
|
@@ -967,7 +1215,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
967
1215
|
return;
|
|
968
1216
|
}
|
|
969
1217
|
|
|
970
|
-
// Navigate without cloning
|
|
971
1218
|
let current = root;
|
|
972
1219
|
for (const segment of arrayPath) {
|
|
973
1220
|
if (!current[segment]) {
|
|
@@ -976,22 +1223,20 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
976
1223
|
current = current[segment];
|
|
977
1224
|
}
|
|
978
1225
|
|
|
979
|
-
// Mutate directly
|
|
980
1226
|
Object.assign(current, newItems);
|
|
981
|
-
if (!current._meta) current._meta = {};
|
|
982
|
-
current._meta.arrayKeys = newKeys; // Direct assignment!
|
|
983
1227
|
},
|
|
984
|
-
|
|
985
|
-
insertShadowArrayElement: (key, arrayPath, newItem, index) => {
|
|
1228
|
+
insertShadowArrayElement: (key, arrayPath, newItem, index, itemId) => {
|
|
986
1229
|
const arrayNode = get().getShadowNode(key, arrayPath);
|
|
987
1230
|
if (!arrayNode?._meta?.arrayKeys) {
|
|
988
1231
|
throw new Error(
|
|
989
1232
|
`Array not found at path: ${[key, ...arrayPath].join('.')}`
|
|
990
1233
|
);
|
|
991
1234
|
}
|
|
1235
|
+
console.log('OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO');
|
|
1236
|
+
const newItemId = itemId || `${generateId(key)}`;
|
|
992
1237
|
|
|
993
|
-
|
|
994
|
-
|
|
1238
|
+
// BUILD AND ADD the node directly - no need for addItemsToArrayNode
|
|
1239
|
+
arrayNode[newItemId] = buildShadowNode(key, newItem);
|
|
995
1240
|
|
|
996
1241
|
// Mutate the array directly
|
|
997
1242
|
const currentKeys = arrayNode._meta.arrayKeys;
|
|
@@ -1001,13 +1246,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
1001
1246
|
: currentKeys.length;
|
|
1002
1247
|
|
|
1003
1248
|
if (insertionPoint >= currentKeys.length) {
|
|
1004
|
-
currentKeys.push(newItemId);
|
|
1249
|
+
currentKeys.push(newItemId);
|
|
1005
1250
|
} else {
|
|
1006
|
-
currentKeys.splice(insertionPoint, 0, newItemId);
|
|
1251
|
+
currentKeys.splice(insertionPoint, 0, newItemId);
|
|
1007
1252
|
}
|
|
1008
1253
|
|
|
1009
|
-
//
|
|
1010
|
-
get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
|
|
1254
|
+
// Skip addItemsToArrayNode entirely - we already did everything it does!
|
|
1011
1255
|
|
|
1012
1256
|
const arrayKey = [key, ...arrayPath].join('.');
|
|
1013
1257
|
get().notifyPathSubscribers(arrayKey, {
|
|
@@ -1033,16 +1277,16 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
1033
1277
|
return;
|
|
1034
1278
|
}
|
|
1035
1279
|
|
|
1036
|
-
const itemsToAdd: Record<string, any> = {};
|
|
1037
1280
|
const newIds: string[] = [];
|
|
1038
1281
|
|
|
1282
|
+
// Build and add items directly
|
|
1039
1283
|
newItems.forEach((item) => {
|
|
1040
1284
|
const newItemId = `${generateId(key)}`;
|
|
1041
1285
|
newIds.push(newItemId);
|
|
1042
|
-
|
|
1286
|
+
arrayNode[newItemId] = buildShadowNode(key, item); // ADD DIRECTLY!
|
|
1043
1287
|
});
|
|
1044
1288
|
|
|
1045
|
-
// Mutate
|
|
1289
|
+
// Mutate the keys array
|
|
1046
1290
|
const currentKeys = arrayNode._meta.arrayKeys;
|
|
1047
1291
|
const insertionPoint =
|
|
1048
1292
|
index !== undefined && index >= 0 && index <= currentKeys.length
|
|
@@ -1050,12 +1294,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
1050
1294
|
: currentKeys.length;
|
|
1051
1295
|
|
|
1052
1296
|
if (insertionPoint >= currentKeys.length) {
|
|
1053
|
-
currentKeys.push(...newIds);
|
|
1297
|
+
currentKeys.push(...newIds);
|
|
1054
1298
|
} else {
|
|
1055
|
-
currentKeys.splice(insertionPoint, 0, ...newIds);
|
|
1299
|
+
currentKeys.splice(insertionPoint, 0, ...newIds);
|
|
1056
1300
|
}
|
|
1057
1301
|
|
|
1058
|
-
|
|
1302
|
+
// NO addItemsToArrayNode call needed!
|
|
1059
1303
|
|
|
1060
1304
|
const arrayKey = [key, ...arrayPath].join('.');
|
|
1061
1305
|
get().notifyPathSubscribers(arrayKey, {
|
|
@@ -1146,25 +1390,36 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
1146
1390
|
},
|
|
1147
1391
|
|
|
1148
1392
|
markAsDirty: (key, path, options = { bubble: true }) => {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
return true;
|
|
1153
|
-
}
|
|
1154
|
-
get().setShadowMetadata(key, pathToMark, { isDirty: true });
|
|
1155
|
-
return false;
|
|
1156
|
-
};
|
|
1393
|
+
// Start at the root node once.
|
|
1394
|
+
let rootNode = get().getShadowNode(key, []);
|
|
1395
|
+
if (!rootNode) return;
|
|
1157
1396
|
|
|
1158
|
-
|
|
1397
|
+
// Navigate to the target node once.
|
|
1398
|
+
let currentNode = rootNode;
|
|
1399
|
+
for (const segment of path) {
|
|
1400
|
+
currentNode = currentNode[segment];
|
|
1401
|
+
if (!currentNode) return; // Path doesn't exist, nothing to mark.
|
|
1402
|
+
}
|
|
1159
1403
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1404
|
+
// Mark the target node as dirty.
|
|
1405
|
+
if (!currentNode._meta) currentNode._meta = {};
|
|
1406
|
+
currentNode._meta.isDirty = true;
|
|
1407
|
+
|
|
1408
|
+
// If bubbling is disabled, we are done.
|
|
1409
|
+
if (!options.bubble) return;
|
|
1410
|
+
|
|
1411
|
+
// Efficiently bubble up using the path segments.
|
|
1412
|
+
let parentNode = rootNode;
|
|
1413
|
+
for (let i = 0; i < path.length; i++) {
|
|
1414
|
+
// The current node in the loop is the parent of the next one.
|
|
1415
|
+
if (parentNode._meta?.isDirty) {
|
|
1416
|
+
// Optimization: If a parent is already dirty, all of its ancestors are too.
|
|
1417
|
+
// We can stop bubbling immediately.
|
|
1418
|
+
return;
|
|
1167
1419
|
}
|
|
1420
|
+
if (!parentNode._meta) parentNode._meta = {};
|
|
1421
|
+
parentNode._meta.isDirty = true;
|
|
1422
|
+
parentNode = parentNode[path[i]!];
|
|
1168
1423
|
}
|
|
1169
1424
|
},
|
|
1170
1425
|
|