cogsbox-state 0.5.471 → 0.5.473
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 +2 -5
- package/dist/CogsState.d.ts +105 -79
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1082 -987
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +293 -243
- 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 +128 -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 +43 -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 +1326 -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 +69 -26
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +436 -152
- 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 +18 -13
- package/src/CogsState.tsx +719 -458
- package/src/Components.tsx +304 -180
- package/src/PluginRunner.tsx +208 -0
- package/src/index.ts +2 -0
- package/src/pluginStore.ts +159 -0
- package/src/plugins.ts +548 -0
- package/src/store.ts +881 -189
- 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,9 +58,81 @@ export type ValidationState = {
|
|
|
99
58
|
lastValidated?: number;
|
|
100
59
|
validatedValue?: any;
|
|
101
60
|
};
|
|
102
|
-
|
|
61
|
+
export type SchemaTypeInfo = {
|
|
62
|
+
type:
|
|
63
|
+
| 'string'
|
|
64
|
+
| 'number'
|
|
65
|
+
| 'boolean'
|
|
66
|
+
| 'array'
|
|
67
|
+
| 'object'
|
|
68
|
+
| 'date'
|
|
69
|
+
| 'unknown';
|
|
70
|
+
schema: any; // Store the actual Zod schema object
|
|
71
|
+
source: 'sync' | 'zod4' | 'zod3' | 'runtime' | 'default';
|
|
72
|
+
default: any;
|
|
73
|
+
nullable?: boolean;
|
|
74
|
+
optional?: boolean;
|
|
75
|
+
};
|
|
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
|
+
};
|
|
131
|
+
// Update ShadowMetadata to include typeInfo
|
|
103
132
|
export type ShadowMetadata = {
|
|
133
|
+
value?: any;
|
|
104
134
|
id?: string;
|
|
135
|
+
typeInfo?: SchemaTypeInfo;
|
|
105
136
|
stateSource?: 'default' | 'server' | 'localStorage';
|
|
106
137
|
lastServerSync?: number;
|
|
107
138
|
isDirty?: boolean;
|
|
@@ -115,26 +146,14 @@ export type ShadowMetadata = {
|
|
|
115
146
|
syncInfo?: { status: string };
|
|
116
147
|
validation?: ValidationState;
|
|
117
148
|
features?: {
|
|
118
|
-
syncEnabled: boolean;
|
|
119
|
-
validationEnabled: boolean;
|
|
120
149
|
localStorageEnabled: boolean;
|
|
121
150
|
};
|
|
122
|
-
lastUpdated?: number;
|
|
123
151
|
signals?: Array<{
|
|
124
152
|
instanceId: string;
|
|
125
153
|
parentId: string;
|
|
126
154
|
position: number;
|
|
127
155
|
effect?: string;
|
|
128
156
|
}>;
|
|
129
|
-
mapWrappers?: Array<{
|
|
130
|
-
instanceId: string;
|
|
131
|
-
path: string[];
|
|
132
|
-
componentId: string;
|
|
133
|
-
meta?: any;
|
|
134
|
-
mapFn: (setter: any, index: number, arraySetter: any) => ReactNode;
|
|
135
|
-
containerRef: HTMLDivElement | null;
|
|
136
|
-
rebuildStateShape: any;
|
|
137
|
-
}>;
|
|
138
157
|
transformCaches?: Map<
|
|
139
158
|
string,
|
|
140
159
|
{
|
|
@@ -144,6 +163,7 @@ export type ShadowMetadata = {
|
|
|
144
163
|
}
|
|
145
164
|
>;
|
|
146
165
|
pathComponents?: Set<string>;
|
|
166
|
+
|
|
147
167
|
streams?: Map<
|
|
148
168
|
string,
|
|
149
169
|
{
|
|
@@ -151,21 +171,40 @@ export type ShadowMetadata = {
|
|
|
151
171
|
flushTimer: NodeJS.Timeout | null;
|
|
152
172
|
}
|
|
153
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;
|
|
154
178
|
} & ComponentsType;
|
|
155
179
|
|
|
156
180
|
type ShadowNode = {
|
|
157
|
-
value?: any;
|
|
158
181
|
_meta?: ShadowMetadata;
|
|
159
182
|
[key: string]: any;
|
|
160
183
|
};
|
|
161
184
|
|
|
162
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;
|
|
163
201
|
setTransformCache: (
|
|
164
202
|
key: string,
|
|
165
203
|
path: string[],
|
|
166
204
|
cacheKey: string,
|
|
167
205
|
cacheData: any
|
|
168
206
|
) => void;
|
|
207
|
+
initializeAndMergeShadowState: (key: string, initialState: any) => void;
|
|
169
208
|
initializeShadowState: (key: string, initialState: any) => void;
|
|
170
209
|
getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
|
|
171
210
|
getShadowMetadata: (
|
|
@@ -190,15 +229,15 @@ export type CogsGlobalState = {
|
|
|
190
229
|
addItemsToArrayNode: (
|
|
191
230
|
key: string,
|
|
192
231
|
arrayPath: string[],
|
|
193
|
-
newItems: any
|
|
194
|
-
newKeys: string[]
|
|
232
|
+
newItems: any
|
|
195
233
|
) => void;
|
|
196
234
|
insertShadowArrayElement: (
|
|
197
235
|
key: string,
|
|
198
236
|
arrayPath: string[],
|
|
199
237
|
newItem: any,
|
|
200
|
-
index?: number
|
|
201
|
-
|
|
238
|
+
index?: number,
|
|
239
|
+
itemId?: string
|
|
240
|
+
) => string;
|
|
202
241
|
removeShadowArrayElement: (key: string, itemPath: string[]) => void;
|
|
203
242
|
registerComponent: (
|
|
204
243
|
stateKey: string,
|
|
@@ -255,51 +294,452 @@ export type CogsGlobalState = {
|
|
|
255
294
|
setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
|
|
256
295
|
getSyncInfo: (key: string) => SyncInfo | null;
|
|
257
296
|
};
|
|
297
|
+
function getTypeFromZodSchema(
|
|
298
|
+
schema: any,
|
|
299
|
+
source: 'zod4' | 'zod3' | 'sync' = 'zod4'
|
|
300
|
+
): SchemaTypeInfo | null {
|
|
301
|
+
if (!schema) return null;
|
|
302
|
+
|
|
303
|
+
let current = schema;
|
|
304
|
+
let isNullable = false;
|
|
305
|
+
let isOptional = false;
|
|
306
|
+
let defaultValue: any = undefined;
|
|
307
|
+
let hasDefault = false;
|
|
308
|
+
|
|
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
|
|
322
|
+
} else {
|
|
323
|
+
break; // Union with no options, cannot determine type
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// --- END: THE CRITICAL FIX ---
|
|
327
|
+
|
|
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;
|
|
350
|
+
}
|
|
258
351
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
352
|
+
const nextSchema = def.innerType || def.schema || current._inner;
|
|
353
|
+
if (!nextSchema || nextSchema === current) {
|
|
354
|
+
break; // Reached the end or a recursive schema
|
|
355
|
+
}
|
|
356
|
+
current = nextSchema;
|
|
262
357
|
}
|
|
263
358
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
359
|
+
const baseSchema = current;
|
|
360
|
+
const baseDef = baseSchema?.def || baseSchema?._def;
|
|
361
|
+
const baseType = baseDef?.typeName || baseDef?.type || baseSchema?._type;
|
|
362
|
+
|
|
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
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Helper to get type info from runtime value
|
|
428
|
+
function getTypeFromValue(value: any): SchemaTypeInfo {
|
|
429
|
+
if (value === null) {
|
|
430
|
+
return {
|
|
431
|
+
type: 'unknown',
|
|
432
|
+
schema: null,
|
|
433
|
+
source: 'default',
|
|
434
|
+
default: null,
|
|
435
|
+
nullable: true,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (value === undefined) {
|
|
440
|
+
return {
|
|
441
|
+
type: 'unknown',
|
|
442
|
+
schema: null,
|
|
443
|
+
source: 'default',
|
|
444
|
+
default: undefined,
|
|
445
|
+
optional: true,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const valueType = typeof value;
|
|
450
|
+
|
|
451
|
+
if (valueType === 'number') {
|
|
452
|
+
return { type: 'number', schema: null, source: 'runtime', default: value };
|
|
453
|
+
} else if (valueType === 'string') {
|
|
454
|
+
return { type: 'string', schema: null, source: 'runtime', default: value };
|
|
455
|
+
} else if (valueType === 'boolean') {
|
|
456
|
+
return { type: 'boolean', schema: null, source: 'runtime', default: value };
|
|
457
|
+
} else if (Array.isArray(value)) {
|
|
458
|
+
return { type: 'array', schema: null, source: 'runtime', default: [] };
|
|
459
|
+
} else if (value instanceof Date) {
|
|
460
|
+
return { type: 'date', schema: null, source: 'runtime', default: value };
|
|
461
|
+
} else if (valueType === 'object') {
|
|
462
|
+
return { type: 'object', schema: null, source: 'runtime', default: {} };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return { type: 'unknown', schema: null, source: 'runtime', default: value };
|
|
466
|
+
}
|
|
467
|
+
type BuildContext = {
|
|
468
|
+
stateKey: string;
|
|
469
|
+
path: string[];
|
|
470
|
+
schemas: {
|
|
471
|
+
sync?: any;
|
|
472
|
+
zodV4?: any;
|
|
473
|
+
zodV3?: any;
|
|
474
|
+
};
|
|
475
|
+
};
|
|
267
476
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
477
|
+
export function buildShadowNode(
|
|
478
|
+
stateKey: string,
|
|
479
|
+
value: any,
|
|
480
|
+
context?: BuildContext
|
|
481
|
+
): ShadowNode {
|
|
482
|
+
// Handle null/undefined/primitives (This part is already correct)
|
|
483
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
484
|
+
const node: ShadowNode = { _meta: { value } };
|
|
485
|
+
node._meta!.typeInfo = getTypeInfoForPath(value, context);
|
|
486
|
+
return node;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Handle arrays
|
|
490
|
+
if (Array.isArray(value)) {
|
|
491
|
+
// 1. Create the node for the array.
|
|
492
|
+
const node: ShadowNode = { _meta: { arrayKeys: [] } };
|
|
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.
|
|
498
|
+
value.forEach((item, index) => {
|
|
499
|
+
const itemId = generateId(stateKey);
|
|
500
|
+
const itemContext = context
|
|
501
|
+
? {
|
|
502
|
+
...context,
|
|
503
|
+
path: [...context.path, index.toString()],
|
|
504
|
+
}
|
|
505
|
+
: undefined;
|
|
506
|
+
|
|
507
|
+
node[itemId] = buildShadowNode(stateKey, item, itemContext);
|
|
508
|
+
node._meta!.arrayKeys!.push(itemId);
|
|
272
509
|
});
|
|
273
510
|
|
|
274
|
-
|
|
275
|
-
return arrayNode;
|
|
511
|
+
return node;
|
|
276
512
|
}
|
|
277
513
|
|
|
514
|
+
// Handle objects
|
|
278
515
|
if (value.constructor === Object) {
|
|
279
|
-
|
|
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.
|
|
280
523
|
for (const key in value) {
|
|
281
524
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
282
|
-
|
|
525
|
+
const propContext = context
|
|
526
|
+
? {
|
|
527
|
+
...context,
|
|
528
|
+
path: [...context.path, key],
|
|
529
|
+
}
|
|
530
|
+
: undefined;
|
|
531
|
+
|
|
532
|
+
node[key] = buildShadowNode(stateKey, value[key], propContext);
|
|
283
533
|
}
|
|
284
534
|
}
|
|
285
|
-
|
|
535
|
+
|
|
536
|
+
return node;
|
|
286
537
|
}
|
|
287
538
|
|
|
288
|
-
|
|
539
|
+
// Fallback for other object types (Date, class instances, etc.)
|
|
540
|
+
return {
|
|
541
|
+
_meta: {
|
|
542
|
+
value: value,
|
|
543
|
+
typeInfo: getTypeFromValue(value),
|
|
544
|
+
},
|
|
545
|
+
};
|
|
289
546
|
}
|
|
290
|
-
// store.ts - Replace the shadow store methods with mutable versions
|
|
291
|
-
// store.ts - Replace the shadow store methods with mutable versions
|
|
292
547
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
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');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
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
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!typeInfo && context.schemas.sync?.[context.stateKey]) {
|
|
578
|
+
typeInfo = getTypeFromValue(value);
|
|
579
|
+
typeInfo.source = 'sync';
|
|
580
|
+
}
|
|
296
581
|
|
|
297
|
-
|
|
298
|
-
|
|
582
|
+
if (typeInfo) return typeInfo;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return getTypeFromValue(value);
|
|
586
|
+
}
|
|
587
|
+
|
|
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;
|
|
644
|
+
|
|
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;
|
|
299
663
|
}
|
|
300
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>();
|
|
704
|
+
let globalCounter = 0;
|
|
705
|
+
const instanceId = Date.now().toString(36);
|
|
706
|
+
|
|
707
|
+
export function generateId(stateKey: string): string {
|
|
708
|
+
const prefix = 'local';
|
|
709
|
+
|
|
710
|
+
return `id:${prefix}_${instanceId}_${(globalCounter++).toString(36)}`;
|
|
711
|
+
}
|
|
301
712
|
export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
302
|
-
|
|
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
|
+
},
|
|
303
743
|
|
|
304
744
|
setTransformCache: (
|
|
305
745
|
key: string,
|
|
@@ -316,7 +756,151 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
316
756
|
transformCaches: metadata.transformCaches,
|
|
317
757
|
});
|
|
318
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
|
+
}
|
|
319
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
|
+
}
|
|
896
|
+
|
|
897
|
+
// Cleanup logic is restored
|
|
898
|
+
if (storageKey === key) {
|
|
899
|
+
shadowStateStore.delete(`[${key}`);
|
|
900
|
+
} else {
|
|
901
|
+
shadowStateStore.delete(key);
|
|
902
|
+
}
|
|
903
|
+
},
|
|
320
904
|
initializeShadowState: (key: string, initialState: any) => {
|
|
321
905
|
const existingRoot =
|
|
322
906
|
shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
|
|
@@ -340,14 +924,29 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
340
924
|
shadowStateStore.delete(key);
|
|
341
925
|
shadowStateStore.delete(`[${key}`);
|
|
342
926
|
|
|
343
|
-
|
|
927
|
+
// Get all available schemas for this state
|
|
928
|
+
const options = get().getInitialOptions(key);
|
|
929
|
+
const syncSchemas = get().getInitialOptions('__syncSchemas');
|
|
930
|
+
|
|
931
|
+
const context: BuildContext = {
|
|
932
|
+
stateKey: key,
|
|
933
|
+
path: [],
|
|
934
|
+
schemas: {
|
|
935
|
+
sync: syncSchemas,
|
|
936
|
+
zodV4: options?.validation?.zodSchemaV4,
|
|
937
|
+
zodV3: options?.validation?.zodSchemaV3,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
// Build with context so type info is stored
|
|
942
|
+
const newRoot = buildShadowNode(key, initialState, context);
|
|
943
|
+
|
|
344
944
|
if (!newRoot._meta) newRoot._meta = {};
|
|
345
945
|
Object.assign(newRoot._meta, preservedMetadata);
|
|
346
946
|
|
|
347
947
|
const storageKey = Array.isArray(initialState) ? `[${key}` : key;
|
|
348
948
|
shadowStateStore.set(storageKey, newRoot);
|
|
349
949
|
},
|
|
350
|
-
|
|
351
950
|
getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
|
|
352
951
|
let current: any =
|
|
353
952
|
shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
|
|
@@ -398,59 +997,75 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
398
997
|
if (!current._meta) {
|
|
399
998
|
current._meta = {};
|
|
400
999
|
}
|
|
1000
|
+
|
|
401
1001
|
Object.assign(current._meta, newMetadata);
|
|
402
1002
|
},
|
|
1003
|
+
getShadowValue: (key: string, path: string[], validArrayIds?: string[]) => {
|
|
1004
|
+
const startNode = get().getShadowNode(key, path);
|
|
403
1005
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
log?: boolean
|
|
409
|
-
) => {
|
|
410
|
-
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
|
+
}
|
|
411
1010
|
|
|
412
|
-
|
|
1011
|
+
// --- High-Performance Iterative Materializer ---
|
|
413
1012
|
|
|
414
|
-
|
|
1013
|
+
// A single root object to hold the final, materialized result.
|
|
1014
|
+
const rootResult: any = {};
|
|
415
1015
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
];
|
|
419
1021
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
1022
|
+
while (stack.length > 0) {
|
|
1023
|
+
const [currentNode, parentResult, resultKey] = stack.pop()!;
|
|
423
1024
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
validArrayIds !== undefined && validArrayIds.length > 0
|
|
430
|
-
? validArrayIds
|
|
431
|
-
: node._meta!.arrayKeys!;
|
|
1025
|
+
// 1. Handle primitive values
|
|
1026
|
+
if (currentNode._meta?.hasOwnProperty('value')) {
|
|
1027
|
+
parentResult[resultKey] = currentNode._meta.value;
|
|
1028
|
+
continue; // Done with this branch
|
|
1029
|
+
}
|
|
432
1030
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
437
1051
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|
|
442
1059
|
}
|
|
443
1060
|
}
|
|
444
|
-
return result;
|
|
445
|
-
},
|
|
446
1061
|
|
|
1062
|
+
return rootResult.final;
|
|
1063
|
+
},
|
|
447
1064
|
updateShadowAtPath: (key, path, newValue) => {
|
|
448
|
-
// NO MORE set() wrapper - direct mutation!
|
|
449
1065
|
const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
|
|
450
1066
|
let root = shadowStateStore.get(rootKey);
|
|
451
1067
|
if (!root) return;
|
|
452
1068
|
|
|
453
|
-
// Navigate to parent without cloning
|
|
454
1069
|
let parentNode = root;
|
|
455
1070
|
for (let i = 0; i < path.length - 1; i++) {
|
|
456
1071
|
if (!parentNode[path[i]!]) {
|
|
@@ -458,72 +1073,141 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
458
1073
|
}
|
|
459
1074
|
parentNode = parentNode[path[i]!];
|
|
460
1075
|
}
|
|
461
|
-
|
|
462
1076
|
const targetNode =
|
|
463
1077
|
path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
|
|
464
1078
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
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)
|
|
475
1086
|
if (
|
|
476
1087
|
typeof plainValue !== 'object' ||
|
|
477
1088
|
plainValue === null ||
|
|
478
|
-
|
|
1089
|
+
plainValue instanceof Date
|
|
479
1090
|
) {
|
|
480
|
-
const oldMeta = nodeToUpdate._meta;
|
|
481
|
-
// Clear
|
|
482
|
-
for (const
|
|
483
|
-
if (
|
|
1091
|
+
const oldMeta = nodeToUpdate._meta || {};
|
|
1092
|
+
// Clear all child properties
|
|
1093
|
+
for (const prop in nodeToUpdate) {
|
|
1094
|
+
if (prop !== '_meta') delete nodeToUpdate[prop];
|
|
484
1095
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
}
|
|
1143
|
+
}
|
|
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
|
+
});
|
|
489
1151
|
}
|
|
1152
|
+
|
|
1153
|
+
// Update the keys array to reflect the new state
|
|
1154
|
+
nodeToUpdate._meta.arrayKeys = updatedKeys;
|
|
490
1155
|
return;
|
|
491
1156
|
}
|
|
492
1157
|
|
|
1158
|
+
// 3. Handle Objects
|
|
493
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
|
+
}
|
|
494
1164
|
|
|
495
1165
|
for (const propKey of plainValueKeys) {
|
|
496
1166
|
const childValue = plainValue[propKey];
|
|
497
1167
|
if (nodeToUpdate[propKey]) {
|
|
498
|
-
intelligentMerge(nodeToUpdate[propKey], childValue
|
|
1168
|
+
intelligentMerge(nodeToUpdate[propKey], childValue, [
|
|
1169
|
+
...currentPath,
|
|
1170
|
+
propKey,
|
|
1171
|
+
]);
|
|
499
1172
|
} else {
|
|
500
|
-
|
|
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);
|
|
501
1183
|
}
|
|
502
1184
|
}
|
|
503
1185
|
|
|
1186
|
+
// Delete keys that no longer exist
|
|
504
1187
|
for (const nodeKey in nodeToUpdate) {
|
|
505
1188
|
if (
|
|
506
1189
|
nodeKey === '_meta' ||
|
|
507
1190
|
!Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
|
|
508
1191
|
)
|
|
509
1192
|
continue;
|
|
510
|
-
|
|
511
1193
|
if (!plainValueKeys.has(nodeKey)) {
|
|
512
1194
|
delete nodeToUpdate[nodeKey];
|
|
513
1195
|
}
|
|
514
1196
|
}
|
|
515
1197
|
}
|
|
516
1198
|
|
|
517
|
-
|
|
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
|
+
}
|
|
518
1204
|
|
|
519
1205
|
get().notifyPathSubscribers([key, ...path].join('.'), {
|
|
520
1206
|
type: 'UPDATE',
|
|
521
1207
|
newValue,
|
|
522
1208
|
});
|
|
523
1209
|
},
|
|
524
|
-
|
|
525
|
-
addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
|
|
526
|
-
// Direct mutation - no cloning!
|
|
1210
|
+
addItemsToArrayNode: (key, arrayPath, newItems) => {
|
|
527
1211
|
const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
|
|
528
1212
|
let root = shadowStateStore.get(rootKey);
|
|
529
1213
|
if (!root) {
|
|
@@ -531,7 +1215,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
531
1215
|
return;
|
|
532
1216
|
}
|
|
533
1217
|
|
|
534
|
-
// Navigate without cloning
|
|
535
1218
|
let current = root;
|
|
536
1219
|
for (const segment of arrayPath) {
|
|
537
1220
|
if (!current[segment]) {
|
|
@@ -540,23 +1223,20 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
540
1223
|
current = current[segment];
|
|
541
1224
|
}
|
|
542
1225
|
|
|
543
|
-
// Mutate directly
|
|
544
1226
|
Object.assign(current, newItems);
|
|
545
|
-
if (!current._meta) current._meta = {};
|
|
546
|
-
current._meta.arrayKeys = newKeys; // Direct assignment!
|
|
547
1227
|
},
|
|
548
|
-
|
|
549
|
-
insertShadowArrayElement: (key, arrayPath, newItem, index) => {
|
|
1228
|
+
insertShadowArrayElement: (key, arrayPath, newItem, index, itemId) => {
|
|
550
1229
|
const arrayNode = get().getShadowNode(key, arrayPath);
|
|
551
1230
|
if (!arrayNode?._meta?.arrayKeys) {
|
|
552
|
-
|
|
1231
|
+
throw new Error(
|
|
553
1232
|
`Array not found at path: ${[key, ...arrayPath].join('.')}`
|
|
554
1233
|
);
|
|
555
|
-
return;
|
|
556
1234
|
}
|
|
1235
|
+
console.log('OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO');
|
|
1236
|
+
const newItemId = itemId || `${generateId(key)}`;
|
|
557
1237
|
|
|
558
|
-
|
|
559
|
-
|
|
1238
|
+
// BUILD AND ADD the node directly - no need for addItemsToArrayNode
|
|
1239
|
+
arrayNode[newItemId] = buildShadowNode(key, newItem);
|
|
560
1240
|
|
|
561
1241
|
// Mutate the array directly
|
|
562
1242
|
const currentKeys = arrayNode._meta.arrayKeys;
|
|
@@ -566,13 +1246,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
566
1246
|
: currentKeys.length;
|
|
567
1247
|
|
|
568
1248
|
if (insertionPoint >= currentKeys.length) {
|
|
569
|
-
currentKeys.push(newItemId);
|
|
1249
|
+
currentKeys.push(newItemId);
|
|
570
1250
|
} else {
|
|
571
|
-
currentKeys.splice(insertionPoint, 0, newItemId);
|
|
1251
|
+
currentKeys.splice(insertionPoint, 0, newItemId);
|
|
572
1252
|
}
|
|
573
1253
|
|
|
574
|
-
//
|
|
575
|
-
get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
|
|
1254
|
+
// Skip addItemsToArrayNode entirely - we already did everything it does!
|
|
576
1255
|
|
|
577
1256
|
const arrayKey = [key, ...arrayPath].join('.');
|
|
578
1257
|
get().notifyPathSubscribers(arrayKey, {
|
|
@@ -581,6 +1260,8 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
581
1260
|
itemKey: `${arrayKey}.${newItemId}`,
|
|
582
1261
|
index: insertionPoint,
|
|
583
1262
|
});
|
|
1263
|
+
|
|
1264
|
+
return newItemId;
|
|
584
1265
|
},
|
|
585
1266
|
|
|
586
1267
|
insertManyShadowArrayElements: (key, arrayPath, newItems, index) => {
|
|
@@ -596,16 +1277,16 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
596
1277
|
return;
|
|
597
1278
|
}
|
|
598
1279
|
|
|
599
|
-
const itemsToAdd: Record<string, any> = {};
|
|
600
1280
|
const newIds: string[] = [];
|
|
601
1281
|
|
|
1282
|
+
// Build and add items directly
|
|
602
1283
|
newItems.forEach((item) => {
|
|
603
|
-
const newItemId =
|
|
1284
|
+
const newItemId = `${generateId(key)}`;
|
|
604
1285
|
newIds.push(newItemId);
|
|
605
|
-
|
|
1286
|
+
arrayNode[newItemId] = buildShadowNode(key, item); // ADD DIRECTLY!
|
|
606
1287
|
});
|
|
607
1288
|
|
|
608
|
-
// Mutate
|
|
1289
|
+
// Mutate the keys array
|
|
609
1290
|
const currentKeys = arrayNode._meta.arrayKeys;
|
|
610
1291
|
const insertionPoint =
|
|
611
1292
|
index !== undefined && index >= 0 && index <= currentKeys.length
|
|
@@ -613,12 +1294,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
613
1294
|
: currentKeys.length;
|
|
614
1295
|
|
|
615
1296
|
if (insertionPoint >= currentKeys.length) {
|
|
616
|
-
currentKeys.push(...newIds);
|
|
1297
|
+
currentKeys.push(...newIds);
|
|
617
1298
|
} else {
|
|
618
|
-
currentKeys.splice(insertionPoint, 0, ...newIds);
|
|
1299
|
+
currentKeys.splice(insertionPoint, 0, ...newIds);
|
|
619
1300
|
}
|
|
620
1301
|
|
|
621
|
-
|
|
1302
|
+
// NO addItemsToArrayNode call needed!
|
|
622
1303
|
|
|
623
1304
|
const arrayKey = [key, ...arrayPath].join('.');
|
|
624
1305
|
get().notifyPathSubscribers(arrayKey, {
|
|
@@ -709,25 +1390,36 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
709
1390
|
},
|
|
710
1391
|
|
|
711
1392
|
markAsDirty: (key, path, options = { bubble: true }) => {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
get().setShadowMetadata(key, pathToMark, { isDirty: true });
|
|
718
|
-
return false;
|
|
719
|
-
};
|
|
1393
|
+
// Start at the root node once.
|
|
1394
|
+
let rootNode = get().getShadowNode(key, []);
|
|
1395
|
+
if (!rootNode) return;
|
|
720
1396
|
|
|
721
|
-
|
|
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
|
+
}
|
|
722
1403
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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;
|
|
730
1419
|
}
|
|
1420
|
+
if (!parentNode._meta) parentNode._meta = {};
|
|
1421
|
+
parentNode._meta.isDirty = true;
|
|
1422
|
+
parentNode = parentNode[path[i]!];
|
|
731
1423
|
}
|
|
732
1424
|
},
|
|
733
1425
|
|