cogsbox-state 0.5.463 → 0.5.465
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CogsState.d.ts +10 -30
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1417 -1461
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Functions.d.ts.map +1 -1
- package/dist/Functions.jsx +23 -16
- package/dist/Functions.jsx.map +1 -1
- package/dist/index.js +26 -26
- package/dist/store.d.ts +39 -47
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +259 -295
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +0 -1
- package/dist/utility.d.ts.map +1 -1
- package/dist/utility.js +121 -158
- package/dist/utility.js.map +1 -1
- package/package.json +5 -4
- package/src/CogsState.tsx +1047 -1428
- package/src/Functions.tsx +27 -7
- package/src/store.ts +489 -593
- package/src/utility.ts +0 -65
package/src/store.ts
CHANGED
|
@@ -3,17 +3,12 @@ import { ulid } from 'ulid';
|
|
|
3
3
|
import type {
|
|
4
4
|
OptionsType,
|
|
5
5
|
ReactivityType,
|
|
6
|
-
StateKeys,
|
|
7
6
|
SyncInfo,
|
|
8
7
|
UpdateTypeDetail,
|
|
9
8
|
} from './CogsState.js';
|
|
10
9
|
|
|
11
10
|
import { startTransition, type ReactNode } from 'react';
|
|
12
11
|
|
|
13
|
-
type StateUpdater<StateValue> =
|
|
14
|
-
| StateValue
|
|
15
|
-
| ((prevValue: StateValue) => StateValue);
|
|
16
|
-
|
|
17
12
|
export type FreshValuesObject = {
|
|
18
13
|
pathsToValues?: string[];
|
|
19
14
|
prevValue?: any;
|
|
@@ -21,9 +16,6 @@ export type FreshValuesObject = {
|
|
|
21
16
|
timeStamp: number;
|
|
22
17
|
};
|
|
23
18
|
|
|
24
|
-
type SyncLogType = {
|
|
25
|
-
timeStamp: number;
|
|
26
|
-
};
|
|
27
19
|
type StateValue = any;
|
|
28
20
|
|
|
29
21
|
export type TrieNode = {
|
|
@@ -89,16 +81,35 @@ export type ComponentsType = {
|
|
|
89
81
|
}
|
|
90
82
|
>;
|
|
91
83
|
};
|
|
84
|
+
|
|
85
|
+
export type ValidationStatus =
|
|
86
|
+
| 'NOT_VALIDATED' // Never run
|
|
87
|
+
| 'VALIDATING' // Currently running
|
|
88
|
+
| 'VALID' // Passed
|
|
89
|
+
| 'INVALID'; // Failed
|
|
90
|
+
|
|
91
|
+
export type ValidationError = {
|
|
92
|
+
source: 'client' | 'sync_engine' | 'api';
|
|
93
|
+
message: string;
|
|
94
|
+
severity: 'warning' | 'error'; // warning = gentle, error = blocking
|
|
95
|
+
code?: string; // Optional error code
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type ValidationState = {
|
|
99
|
+
status: ValidationStatus;
|
|
100
|
+
errors: ValidationError[];
|
|
101
|
+
lastValidated?: number;
|
|
102
|
+
validatedValue?: any; // Value when last validated
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// This is the new definition for the metadata object
|
|
92
106
|
export type ShadowMetadata = {
|
|
93
107
|
id?: string;
|
|
94
|
-
|
|
95
108
|
stateSource?: 'default' | 'server' | 'localStorage';
|
|
96
109
|
lastServerSync?: number;
|
|
97
110
|
isDirty?: boolean;
|
|
98
111
|
baseServerState?: any;
|
|
99
|
-
|
|
100
112
|
arrayKeys?: string[];
|
|
101
|
-
|
|
102
113
|
fields?: Record<string, any>;
|
|
103
114
|
virtualizer?: {
|
|
104
115
|
itemHeight?: number;
|
|
@@ -106,14 +117,12 @@ export type ShadowMetadata = {
|
|
|
106
117
|
};
|
|
107
118
|
syncInfo?: { status: string };
|
|
108
119
|
validation?: ValidationState;
|
|
120
|
+
features?: {
|
|
121
|
+
syncEnabled: boolean;
|
|
122
|
+
validationEnabled: boolean;
|
|
123
|
+
localStorageEnabled: boolean;
|
|
124
|
+
};
|
|
109
125
|
lastUpdated?: number;
|
|
110
|
-
value?: any;
|
|
111
|
-
classSignals?: Array<{
|
|
112
|
-
id: string;
|
|
113
|
-
effect: string;
|
|
114
|
-
lastClasses: string;
|
|
115
|
-
deps: any[];
|
|
116
|
-
}>;
|
|
117
126
|
signals?: Array<{
|
|
118
127
|
instanceId: string;
|
|
119
128
|
parentId: string;
|
|
@@ -125,12 +134,7 @@ export type ShadowMetadata = {
|
|
|
125
134
|
path: string[];
|
|
126
135
|
componentId: string;
|
|
127
136
|
meta?: any;
|
|
128
|
-
mapFn: (
|
|
129
|
-
setter: any,
|
|
130
|
-
index: number,
|
|
131
|
-
|
|
132
|
-
arraySetter: any
|
|
133
|
-
) => ReactNode;
|
|
137
|
+
mapFn: (setter: any, index: number, arraySetter: any) => ReactNode;
|
|
134
138
|
containerRef: HTMLDivElement | null;
|
|
135
139
|
rebuildStateShape: any;
|
|
136
140
|
}>;
|
|
@@ -152,36 +156,48 @@ export type ShadowMetadata = {
|
|
|
152
156
|
>;
|
|
153
157
|
} & ComponentsType;
|
|
154
158
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
| 'VALIDATION_FAILED' // Hard error on blur/submit.
|
|
161
|
-
| 'VALID_PENDING_SYNC' // Passed validation, ready for sync.
|
|
162
|
-
| 'SYNCING' // Actively being sent to the server.
|
|
163
|
-
| 'SYNCED' // Server confirmed success.
|
|
164
|
-
| 'SYNC_FAILED'; // Server rejected the data.
|
|
165
|
-
|
|
166
|
-
export type ValidationState = {
|
|
167
|
-
status: ValidationStatus;
|
|
168
|
-
message?: string;
|
|
169
|
-
lastValidated?: number;
|
|
170
|
-
validatedValue?: any;
|
|
159
|
+
// The shadow node itself can have a value and the metadata object.
|
|
160
|
+
type ShadowNode = {
|
|
161
|
+
value?: any;
|
|
162
|
+
_meta?: ShadowMetadata;
|
|
163
|
+
[key: string]: any; // For nested data properties
|
|
171
164
|
};
|
|
172
|
-
|
|
173
|
-
| { type: 'INSERT'; path: string; itemKey: string; index: number }
|
|
174
|
-
| { type: 'REMOVE'; path: string; itemKey: string }
|
|
175
|
-
| { type: 'UPDATE'; path: string; newValue: any }
|
|
176
|
-
| { type: 'ITEMHEIGHT'; itemKey: string; height: number }
|
|
177
|
-
| { type: 'RELOAD'; path: string };
|
|
165
|
+
|
|
178
166
|
export type CogsGlobalState = {
|
|
179
|
-
|
|
180
|
-
|
|
167
|
+
// NEW shadow store
|
|
168
|
+
shadowStateStore: Map<string, ShadowNode>; // Changed ShadowMetadata to ShadowNode
|
|
169
|
+
setTransformCache: (
|
|
170
|
+
key: string,
|
|
171
|
+
path: string[],
|
|
172
|
+
cacheKey: string,
|
|
173
|
+
cacheData: any
|
|
174
|
+
) => void;
|
|
175
|
+
// NEW functions
|
|
176
|
+
initializeShadowState: (key: string, initialState: any) => void;
|
|
181
177
|
|
|
182
|
-
|
|
178
|
+
// REFACTORED: getShadowNode gets the whole object (data + _meta)
|
|
179
|
+
getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
|
|
180
|
+
// REFACTORED: getShadowMetadata now returns just the _meta field
|
|
181
|
+
getShadowMetadata: (
|
|
182
|
+
key: string,
|
|
183
|
+
path: string[]
|
|
184
|
+
) => ShadowMetadata | undefined;
|
|
183
185
|
|
|
184
|
-
|
|
186
|
+
setShadowMetadata: (key: string, path: string[], metadata: any) => void;
|
|
187
|
+
getShadowValue: (
|
|
188
|
+
key: string,
|
|
189
|
+
path: string[],
|
|
190
|
+
validArrayIds?: string[],
|
|
191
|
+
log?: boolean
|
|
192
|
+
) => any;
|
|
193
|
+
updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
|
|
194
|
+
insertShadowArrayElement: (
|
|
195
|
+
key: string,
|
|
196
|
+
arrayPath: string[],
|
|
197
|
+
newItem: any,
|
|
198
|
+
index?: number
|
|
199
|
+
) => void;
|
|
200
|
+
removeShadowArrayElement: (key: string, itemPath: string[]) => void;
|
|
185
201
|
registerComponent: (
|
|
186
202
|
stateKey: string,
|
|
187
203
|
componentId: string,
|
|
@@ -193,7 +209,6 @@ export type CogsGlobalState = {
|
|
|
193
209
|
dependencyPath: string[],
|
|
194
210
|
fullComponentId: string
|
|
195
211
|
) => void;
|
|
196
|
-
shadowStateStore: Map<string, ShadowMetadata>;
|
|
197
212
|
|
|
198
213
|
markAsDirty: (
|
|
199
214
|
key: string,
|
|
@@ -201,35 +216,6 @@ export type CogsGlobalState = {
|
|
|
201
216
|
options: { bubble: boolean }
|
|
202
217
|
) => void;
|
|
203
218
|
// These method signatures stay the same
|
|
204
|
-
initializeShadowState: (key: string, initialState: any) => void;
|
|
205
|
-
updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
|
|
206
|
-
insertShadowArrayElement: (
|
|
207
|
-
key: string,
|
|
208
|
-
arrayPath: string[],
|
|
209
|
-
newItem: any
|
|
210
|
-
) => void;
|
|
211
|
-
removeShadowArrayElement: (key: string, arrayPath: string[]) => void;
|
|
212
|
-
getShadowValue: (
|
|
213
|
-
key: string,
|
|
214
|
-
|
|
215
|
-
validArrayIds?: string[]
|
|
216
|
-
) => any;
|
|
217
|
-
|
|
218
|
-
getShadowMetadata: (
|
|
219
|
-
key: string,
|
|
220
|
-
path: string[]
|
|
221
|
-
) => ShadowMetadata | undefined;
|
|
222
|
-
setShadowMetadata: (
|
|
223
|
-
key: string,
|
|
224
|
-
path: string[],
|
|
225
|
-
metadata: Omit<ShadowMetadata, 'id'>
|
|
226
|
-
) => void;
|
|
227
|
-
setTransformCache: (
|
|
228
|
-
key: string,
|
|
229
|
-
path: string[],
|
|
230
|
-
cacheKey: string,
|
|
231
|
-
cacheData: any
|
|
232
|
-
) => void;
|
|
233
219
|
|
|
234
220
|
pathSubscribers: Map<string, Set<(newValue: any) => void>>;
|
|
235
221
|
subscribeToPath: (
|
|
@@ -268,655 +254,565 @@ export type CogsGlobalState = {
|
|
|
268
254
|
|
|
269
255
|
stateLog: Map<string, Map<string, UpdateTypeDetail>>;
|
|
270
256
|
syncInfoStore: Map<string, SyncInfo>;
|
|
271
|
-
addStateLog: (
|
|
257
|
+
addStateLog: (updates: UpdateTypeDetail[]) => void;
|
|
272
258
|
|
|
273
259
|
setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
|
|
274
260
|
getSyncInfo: (key: string) => SyncInfo | null;
|
|
275
261
|
};
|
|
276
|
-
const isSimpleObject = (value: any): boolean => {
|
|
277
|
-
// Most common cases first
|
|
278
|
-
if (value === null || typeof value !== 'object') return false;
|
|
279
262
|
|
|
280
|
-
|
|
281
|
-
|
|
263
|
+
// ✅ CHANGE 1: `METADATA_KEYS` now only contains `_meta` and `value`.
|
|
264
|
+
// The other keys are now properties of the `ShadowMetadata` type.
|
|
265
|
+
export const METADATA_KEYS = new Set(['_meta', 'value']);
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The single source of truth for converting a regular JS value/object
|
|
269
|
+
* into the shadow state tree format with the new `_meta` structure.
|
|
270
|
+
*/
|
|
271
|
+
// ✅ CHANGE 2: `buildShadowNode` now creates the `_meta` field.
|
|
272
|
+
export function buildShadowNode(value: any): ShadowNode {
|
|
273
|
+
// Primitives and null are wrapped.
|
|
274
|
+
if (value === null || typeof value !== 'object') {
|
|
275
|
+
return { value };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Arrays are converted to an object with id-keyed children and metadata in `_meta`.
|
|
279
|
+
if (Array.isArray(value)) {
|
|
280
|
+
const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } }; // Initialize with _meta and arrayKeys
|
|
281
|
+
const idKeys: string[] = [];
|
|
282
|
+
value.forEach((item) => {
|
|
283
|
+
const itemId = `id:${ulid()}`;
|
|
284
|
+
arrayNode[itemId] = buildShadowNode(item); // Recurse for each item
|
|
285
|
+
idKeys.push(itemId);
|
|
286
|
+
});
|
|
287
|
+
arrayNode._meta!.arrayKeys = idKeys; // Set the final ordered keys
|
|
288
|
+
return arrayNode;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Plain objects are recursively processed.
|
|
292
|
+
if (value.constructor === Object) {
|
|
293
|
+
const objectNode: ShadowNode = { _meta: {} }; // Initialize with an empty meta object
|
|
294
|
+
for (const key in value) {
|
|
295
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
296
|
+
objectNode[key] = buildShadowNode(value[key]); // Recurse for each property
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return objectNode;
|
|
300
|
+
}
|
|
282
301
|
|
|
283
|
-
//
|
|
284
|
-
|
|
302
|
+
// Fallback for other object types (Date, etc.) - treat them as primitives.
|
|
303
|
+
return { value };
|
|
304
|
+
}
|
|
285
305
|
|
|
286
|
-
// Everything else is not simple
|
|
287
|
-
return false;
|
|
288
|
-
};
|
|
289
306
|
export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
290
|
-
|
|
291
|
-
// A flag to ensure we only schedule the flush once per event-loop tick.
|
|
292
|
-
isFlushScheduled: false,
|
|
307
|
+
shadowStateStore: new Map<string, ShadowNode>(),
|
|
293
308
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
309
|
+
setTransformCache: (
|
|
310
|
+
key: string,
|
|
311
|
+
path: string[],
|
|
312
|
+
cacheKey: string,
|
|
313
|
+
cacheData: any
|
|
314
|
+
) => {
|
|
315
|
+
// This function now uses setShadowMetadata which correctly places the data.
|
|
316
|
+
const metadata = get().getShadowMetadata(key, path) || {};
|
|
317
|
+
if (!metadata.transformCaches) {
|
|
318
|
+
metadata.transformCaches = new Map();
|
|
302
319
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
320
|
+
metadata.transformCaches.set(cacheKey, cacheData);
|
|
321
|
+
get().setShadowMetadata(key, path, {
|
|
322
|
+
transformCaches: metadata.transformCaches,
|
|
323
|
+
});
|
|
306
324
|
},
|
|
307
|
-
|
|
325
|
+
|
|
326
|
+
initializeShadowState: (key: string, initialState: any) => {
|
|
308
327
|
set((state) => {
|
|
309
328
|
const newShadowStore = new Map(state.shadowStateStore);
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
329
|
+
const existingRoot =
|
|
330
|
+
newShadowStore.get(key) || newShadowStore.get(`[${key}`);
|
|
331
|
+
let preservedMetadata: Partial<ShadowMetadata> = {};
|
|
332
|
+
|
|
333
|
+
if (existingRoot?._meta) {
|
|
334
|
+
const {
|
|
335
|
+
components,
|
|
336
|
+
features,
|
|
337
|
+
lastServerSync,
|
|
338
|
+
stateSource,
|
|
339
|
+
baseServerState,
|
|
340
|
+
} = existingRoot._meta;
|
|
341
|
+
if (components) preservedMetadata.components = components;
|
|
342
|
+
if (features) preservedMetadata.features = features;
|
|
343
|
+
if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
|
|
344
|
+
if (stateSource) preservedMetadata.stateSource = stateSource;
|
|
345
|
+
if (baseServerState)
|
|
346
|
+
preservedMetadata.baseServerState = baseServerState;
|
|
347
|
+
}
|
|
319
348
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const component = rootMeta.components?.get(fullComponentId);
|
|
349
|
+
newShadowStore.delete(key);
|
|
350
|
+
newShadowStore.delete(`[${key}`);
|
|
323
351
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const newComponentRegistration = { ...component, paths: newPaths };
|
|
330
|
-
const newComponentsMap = new Map(rootMeta.components);
|
|
331
|
-
newComponentsMap.set(fullComponentId, newComponentRegistration);
|
|
352
|
+
const newRoot = buildShadowNode(initialState);
|
|
353
|
+
// Ensure _meta exists before assigning to it
|
|
354
|
+
if (!newRoot._meta) newRoot._meta = {};
|
|
355
|
+
Object.assign(newRoot._meta, preservedMetadata);
|
|
332
356
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
...rootMeta,
|
|
336
|
-
components: newComponentsMap,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
357
|
+
const storageKey = Array.isArray(initialState) ? `[${key}` : key;
|
|
358
|
+
newShadowStore.set(storageKey, newRoot);
|
|
339
359
|
|
|
340
|
-
// Return the final, updated state
|
|
341
|
-
return { shadowStateStore: newShadowStore };
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
registerComponent: (stateKey, fullComponentId, registration) => {
|
|
345
|
-
set((state) => {
|
|
346
|
-
const newShadowStore = new Map(state.shadowStateStore);
|
|
347
|
-
const rootMeta = newShadowStore.get(stateKey) || {};
|
|
348
|
-
const components = new Map(rootMeta.components);
|
|
349
|
-
components.set(fullComponentId, registration);
|
|
350
|
-
newShadowStore.set(stateKey, { ...rootMeta, components });
|
|
351
360
|
return { shadowStateStore: newShadowStore };
|
|
352
361
|
});
|
|
353
362
|
},
|
|
354
363
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (!rootMeta?.components) {
|
|
360
|
-
return state; // Return original state, no change needed
|
|
361
|
-
}
|
|
364
|
+
// ✅ NEW HELPER: Gets the entire node (data and metadata).
|
|
365
|
+
getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
|
|
366
|
+
const store = get().shadowStateStore;
|
|
367
|
+
let current: any = store.get(key) || store.get(`[${key}`);
|
|
362
368
|
|
|
363
|
-
|
|
364
|
-
|
|
369
|
+
if (!current) return undefined;
|
|
370
|
+
if (path.length === 0) return current;
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
for (const segment of path) {
|
|
373
|
+
if (typeof current !== 'object' || current === null) return undefined;
|
|
374
|
+
current = current[segment];
|
|
375
|
+
if (current === undefined) return undefined;
|
|
376
|
+
}
|
|
377
|
+
return current;
|
|
378
|
+
},
|
|
371
379
|
|
|
372
|
-
|
|
373
|
-
|
|
380
|
+
// ✅ REFACTORED: Returns only the `_meta` part of a node.
|
|
381
|
+
getShadowMetadata: (
|
|
382
|
+
key: string,
|
|
383
|
+
path: string[]
|
|
384
|
+
): ShadowMetadata | undefined => {
|
|
385
|
+
const node = get().getShadowNode(key, path);
|
|
386
|
+
return node?._meta;
|
|
374
387
|
},
|
|
375
|
-
markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
|
|
376
|
-
const { shadowStateStore } = get();
|
|
377
|
-
const updates = new Map<string, ShadowMetadata>();
|
|
378
388
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
389
|
+
// ✅ REFACTORED: Sets data within the `_meta` object.
|
|
390
|
+
setShadowMetadata: (
|
|
391
|
+
key: string,
|
|
392
|
+
path: string[],
|
|
393
|
+
newMetadata: Partial<ShadowMetadata>
|
|
394
|
+
) => {
|
|
395
|
+
set((state) => {
|
|
396
|
+
const newStore = new Map(state.shadowStateStore);
|
|
397
|
+
const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
|
|
398
|
+
let root = newStore.get(rootKey);
|
|
382
399
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
400
|
+
if (!root) {
|
|
401
|
+
root = {};
|
|
402
|
+
newStore.set(rootKey, root);
|
|
386
403
|
}
|
|
387
404
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
// Mark the target path
|
|
393
|
-
setDirty(path);
|
|
405
|
+
const clonedRoot: any = { ...root };
|
|
406
|
+
newStore.set(rootKey, clonedRoot);
|
|
394
407
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const wasDirty = setDirty(parentPath);
|
|
401
|
-
if (wasDirty) {
|
|
402
|
-
break; // Stop bubbling if parent was already dirty
|
|
403
|
-
}
|
|
408
|
+
let current = clonedRoot;
|
|
409
|
+
for (const segment of path) {
|
|
410
|
+
const nextNode = current[segment] || {};
|
|
411
|
+
current[segment] = { ...nextNode }; // Clone for immutability
|
|
412
|
+
current = current[segment];
|
|
404
413
|
}
|
|
405
|
-
}
|
|
406
414
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
set((state) => {
|
|
410
|
-
updates.forEach((meta, key) => {
|
|
411
|
-
state.shadowStateStore.set(key, meta);
|
|
412
|
-
});
|
|
413
|
-
return state;
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
serverStateUpdates: new Map(),
|
|
418
|
-
setServerStateUpdate: (key, serverState) => {
|
|
419
|
-
set((state) => {
|
|
420
|
-
const newMap = new Map(state.serverStateUpdates);
|
|
421
|
-
newMap.set(key, serverState);
|
|
422
|
-
return { serverStateUpdates: newMap };
|
|
423
|
-
});
|
|
415
|
+
// Ensure _meta object exists and merge the new metadata into it
|
|
416
|
+
current._meta = { ...(current._meta || {}), ...newMetadata };
|
|
424
417
|
|
|
425
|
-
|
|
426
|
-
get().notifyPathSubscribers(key, {
|
|
427
|
-
type: 'SERVER_STATE_UPDATE',
|
|
428
|
-
serverState,
|
|
418
|
+
return { shadowStateStore: newStore };
|
|
429
419
|
});
|
|
430
420
|
},
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
421
|
+
getShadowValue: (
|
|
422
|
+
key: string,
|
|
423
|
+
path: string[],
|
|
424
|
+
validArrayIds?: string[],
|
|
425
|
+
log?: boolean
|
|
426
|
+
) => {
|
|
427
|
+
const node = get().getShadowNode(key, path);
|
|
434
428
|
|
|
435
|
-
|
|
436
|
-
const subscribers = get().pathSubscribers;
|
|
437
|
-
const subsForPath = subscribers.get(path) || new Set();
|
|
438
|
-
subsForPath.add(callback);
|
|
439
|
-
subscribers.set(path, subsForPath);
|
|
429
|
+
if (node === null || node === undefined) return undefined;
|
|
440
430
|
|
|
441
|
-
|
|
442
|
-
const currentSubs = get().pathSubscribers.get(path);
|
|
443
|
-
if (currentSubs) {
|
|
444
|
-
currentSubs.delete(callback);
|
|
445
|
-
if (currentSubs.size === 0) {
|
|
446
|
-
get().pathSubscribers.delete(path);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
},
|
|
431
|
+
const nodeKeys = Object.keys(node);
|
|
451
432
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
433
|
+
// ✅ FIX: A node is a primitive wrapper ONLY if its keys are 'value' and/or '_meta'.
|
|
434
|
+
// This prevents objects in your data that happen to have a "value" property from being
|
|
435
|
+
// incorrectly treated as wrappers.
|
|
436
|
+
const isPrimitiveWrapper =
|
|
437
|
+
Object.prototype.hasOwnProperty.call(node, 'value') &&
|
|
438
|
+
nodeKeys.every((k) => k === 'value' || k === '_meta');
|
|
455
439
|
|
|
456
|
-
if (
|
|
457
|
-
|
|
440
|
+
if (isPrimitiveWrapper) {
|
|
441
|
+
return node.value;
|
|
458
442
|
}
|
|
459
|
-
},
|
|
460
443
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
}
|
|
444
|
+
// Array Check (This part is correct)
|
|
445
|
+
const isArrayNode =
|
|
446
|
+
node._meta &&
|
|
447
|
+
Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
|
|
448
|
+
if (isArrayNode) {
|
|
449
|
+
const keysToIterate =
|
|
450
|
+
validArrayIds !== undefined && validArrayIds.length > 0
|
|
451
|
+
? validArrayIds
|
|
452
|
+
: node._meta!.arrayKeys!;
|
|
453
|
+
|
|
454
|
+
return keysToIterate.map((itemKey: string) =>
|
|
455
|
+
get().getShadowValue(key, [...path, itemKey])
|
|
456
|
+
);
|
|
457
|
+
}
|
|
477
458
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
value.forEach(() => {
|
|
485
|
-
const itemId = `id:${ulid()}`;
|
|
486
|
-
childIds.push(nodeKey + '.' + itemId);
|
|
487
|
-
});
|
|
488
|
-
newShadowStore.set(nodeKey, { arrayKeys: childIds });
|
|
489
|
-
value.forEach((item, index) => {
|
|
490
|
-
const itemId = childIds[index]!.split('.').pop();
|
|
491
|
-
processValue(item, [...path!, itemId!]);
|
|
492
|
-
});
|
|
493
|
-
} else if (isSimpleObject(value)) {
|
|
494
|
-
const fields = Object.fromEntries(
|
|
495
|
-
Object.keys(value).map((k) => [k, nodeKey + '.' + k])
|
|
496
|
-
);
|
|
497
|
-
newShadowStore.set(nodeKey, { fields });
|
|
498
|
-
Object.keys(value).forEach((k) => {
|
|
499
|
-
processValue(value[k], [...path, k]);
|
|
500
|
-
});
|
|
501
|
-
} else {
|
|
502
|
-
newShadowStore.set(nodeKey, { value });
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
processValue(initialState, []);
|
|
506
|
-
|
|
507
|
-
// 5. RESTORE the preserved components map onto the new root metadata
|
|
508
|
-
if (preservedComponents) {
|
|
509
|
-
const newRootMeta = newShadowStore.get(key) || {};
|
|
510
|
-
newShadowStore.set(key, {
|
|
511
|
-
...newRootMeta,
|
|
512
|
-
components: preservedComponents,
|
|
513
|
-
});
|
|
459
|
+
// Object Reconstruction (This part is also correct)
|
|
460
|
+
const result: any = {};
|
|
461
|
+
for (const propKey of nodeKeys) {
|
|
462
|
+
// We correctly ignore metadata and array item keys here.
|
|
463
|
+
if (propKey !== '_meta' && !propKey.startsWith('id:')) {
|
|
464
|
+
result[propKey] = get().getShadowValue(key, [...path, propKey]);
|
|
514
465
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return { shadowStateStore: newShadowStore };
|
|
518
|
-
});
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
519
468
|
},
|
|
520
469
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
keys.forEach((itemKey) => {
|
|
544
|
-
result.push(reconstruct(itemKey));
|
|
545
|
-
});
|
|
546
|
-
} else if (shadowMeta.fields) {
|
|
547
|
-
result = {};
|
|
548
|
-
memo.set(keyToBuild, result);
|
|
549
|
-
Object.entries(shadowMeta.fields).forEach(([key, fieldPath]) => {
|
|
550
|
-
result[key] = reconstruct(fieldPath as string);
|
|
551
|
-
});
|
|
470
|
+
// ✅ REFACTORED: Correctly preserves `_meta` on updates.
|
|
471
|
+
updateShadowAtPath: (key, path, newValue) => {
|
|
472
|
+
set((state) => {
|
|
473
|
+
const newStore = new Map(state.shadowStateStore);
|
|
474
|
+
const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
|
|
475
|
+
let root = newStore.get(rootKey);
|
|
476
|
+
|
|
477
|
+
if (!root) return state;
|
|
478
|
+
|
|
479
|
+
const clonedRoot: any = { ...root };
|
|
480
|
+
newStore.set(rootKey, clonedRoot);
|
|
481
|
+
|
|
482
|
+
if (path.length === 0) {
|
|
483
|
+
const newRootStructure = buildShadowNode(newValue);
|
|
484
|
+
// Preserve the top-level metadata
|
|
485
|
+
if (clonedRoot._meta) {
|
|
486
|
+
newRootStructure._meta = {
|
|
487
|
+
...(newRootStructure._meta || {}),
|
|
488
|
+
...clonedRoot._meta,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
newStore.set(rootKey, newRootStructure);
|
|
552
492
|
} else {
|
|
553
|
-
|
|
554
|
-
|
|
493
|
+
let current = clonedRoot;
|
|
494
|
+
const parentPath = path.slice(0, -1);
|
|
495
|
+
for (const segment of parentPath) {
|
|
496
|
+
current[segment] = { ...current[segment] };
|
|
497
|
+
current = current[segment];
|
|
498
|
+
}
|
|
555
499
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
500
|
+
const lastSegment = path[path.length - 1]!;
|
|
501
|
+
const existingNode = current[lastSegment] || {};
|
|
502
|
+
const newNodeStructure = buildShadowNode(newValue);
|
|
559
503
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
504
|
+
// This merge is critical: it preserves existing metadata during an update.
|
|
505
|
+
if (existingNode._meta) {
|
|
506
|
+
newNodeStructure._meta = {
|
|
507
|
+
...(newNodeStructure._meta || {}),
|
|
508
|
+
...existingNode._meta,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
current[lastSegment] = newNodeStructure;
|
|
512
|
+
}
|
|
565
513
|
|
|
566
|
-
|
|
514
|
+
get().notifyPathSubscribers([key, ...path].join('.'), {
|
|
515
|
+
type: 'UPDATE',
|
|
516
|
+
newValue,
|
|
517
|
+
});
|
|
518
|
+
return { shadowStateStore: newStore };
|
|
519
|
+
});
|
|
567
520
|
},
|
|
568
521
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
// --- THIS IS THE TRAP ---
|
|
574
|
-
// If the existing metadata HAS a components map, but the NEW metadata DOES NOT,
|
|
575
|
-
// it means we are about to wipe it out. This is the bug.
|
|
576
|
-
if (existingMeta?.components && !metadata.components) {
|
|
577
|
-
console.group(
|
|
578
|
-
'%c🚨 RACE CONDITION DETECTED! 🚨',
|
|
579
|
-
'color: red; font-size: 18px; font-weight: bold;'
|
|
580
|
-
);
|
|
522
|
+
// ✅ REFACTORED: Works with `_meta.arrayKeys`.
|
|
523
|
+
insertShadowArrayElement: (key, arrayPath, newItem, index) => {
|
|
524
|
+
const arrayNode = get().getShadowNode(key, arrayPath);
|
|
525
|
+
if (!arrayNode?._meta?.arrayKeys) {
|
|
581
526
|
console.error(
|
|
582
|
-
`
|
|
583
|
-
);
|
|
584
|
-
console.log(
|
|
585
|
-
'The EXISTING metadata had a components map:',
|
|
586
|
-
existingMeta.components
|
|
587
|
-
);
|
|
588
|
-
console.log(
|
|
589
|
-
'The NEW metadata is trying to save WITHOUT a components map:',
|
|
590
|
-
metadata
|
|
527
|
+
`Array not found at path: ${[key, ...arrayPath].join('.')}`
|
|
591
528
|
);
|
|
592
|
-
|
|
593
|
-
'%cStack trace to the function that caused this overwrite:',
|
|
594
|
-
'font-weight: bold;'
|
|
595
|
-
);
|
|
596
|
-
console.trace(); // This prints the call stack, leading you to the bad code.
|
|
597
|
-
console.groupEnd();
|
|
529
|
+
return;
|
|
598
530
|
}
|
|
599
|
-
// --- END OF TRAP ---
|
|
600
531
|
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const fullKey = [key, ...path].join('.');
|
|
613
|
-
const newShadowStore = new Map(get().shadowStateStore);
|
|
614
|
-
const existing = newShadowStore.get(fullKey) || {};
|
|
532
|
+
const newItemId = `id:${ulid()}`;
|
|
533
|
+
const newItemNode = buildShadowNode(newItem);
|
|
534
|
+
|
|
535
|
+
// Update the `arrayKeys` in the metadata
|
|
536
|
+
const currentKeys = arrayNode._meta.arrayKeys;
|
|
537
|
+
const newKeys = [...currentKeys];
|
|
538
|
+
if (index !== undefined && index >= 0 && index <= newKeys.length) {
|
|
539
|
+
newKeys.splice(index, 0, newItemId);
|
|
540
|
+
} else {
|
|
541
|
+
newKeys.push(newItemId);
|
|
542
|
+
}
|
|
615
543
|
|
|
616
|
-
//
|
|
617
|
-
if (
|
|
618
|
-
|
|
544
|
+
// Update transform caches if they exist
|
|
545
|
+
if (arrayNode._meta.transformCaches) {
|
|
546
|
+
arrayNode._meta.transformCaches.forEach((cache) => {
|
|
547
|
+
if (cache.validIds && Array.isArray(cache.validIds)) {
|
|
548
|
+
const matchesFilters = cache.transforms.every((transform) =>
|
|
549
|
+
transform.type === 'filter' ? transform.fn(newItem) : true
|
|
550
|
+
);
|
|
551
|
+
if (matchesFilters) {
|
|
552
|
+
cache.validIds = [...cache.validIds];
|
|
553
|
+
if (index !== undefined) {
|
|
554
|
+
cache.validIds.splice(index, 0, newItemId);
|
|
555
|
+
} else {
|
|
556
|
+
cache.validIds.push(newItemId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
619
561
|
}
|
|
620
562
|
|
|
621
|
-
//
|
|
622
|
-
|
|
563
|
+
// Directly set the new item and updated metadata on the node before setting state
|
|
564
|
+
arrayNode[newItemId] = newItemNode;
|
|
565
|
+
arrayNode._meta.arrayKeys = newKeys;
|
|
623
566
|
|
|
624
|
-
|
|
625
|
-
newShadowStore.set(fullKey, existing);
|
|
626
|
-
set({ shadowStateStore: newShadowStore });
|
|
567
|
+
get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
|
|
627
568
|
|
|
628
|
-
//
|
|
629
|
-
},
|
|
630
|
-
insertShadowArrayElement: (
|
|
631
|
-
key: string,
|
|
632
|
-
arrayPath: string[],
|
|
633
|
-
newItem: any
|
|
634
|
-
) => {
|
|
635
|
-
const newShadowStore = new Map(get().shadowStateStore);
|
|
569
|
+
// Trigger notifications
|
|
636
570
|
const arrayKey = [key, ...arrayPath].join('.');
|
|
637
|
-
const parentMeta = newShadowStore.get(arrayKey);
|
|
638
|
-
|
|
639
|
-
if (!parentMeta || !parentMeta.arrayKeys) return;
|
|
640
|
-
|
|
641
|
-
const newItemId = `id:${ulid()}`;
|
|
642
|
-
const fullItemKey = arrayKey + '.' + newItemId;
|
|
643
|
-
|
|
644
|
-
// Just add to the end (or at a specific index if provided)
|
|
645
|
-
const newArrayKeys = [...parentMeta.arrayKeys];
|
|
646
|
-
newArrayKeys.push(fullItemKey); // Or use splice if you have an index
|
|
647
|
-
newShadowStore.set(arrayKey, { ...parentMeta, arrayKeys: newArrayKeys });
|
|
648
|
-
|
|
649
|
-
// Process the new item - but use the correct logic
|
|
650
|
-
const processNewItem = (value: any, path: string[]) => {
|
|
651
|
-
const nodeKey = [key, ...path].join('.');
|
|
652
|
-
|
|
653
|
-
if (Array.isArray(value)) {
|
|
654
|
-
// Handle arrays...
|
|
655
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
656
|
-
// Create fields mapping
|
|
657
|
-
const fields = Object.fromEntries(
|
|
658
|
-
Object.keys(value).map((k) => [k, nodeKey + '.' + k])
|
|
659
|
-
);
|
|
660
|
-
newShadowStore.set(nodeKey, { fields });
|
|
661
|
-
|
|
662
|
-
// Process each field
|
|
663
|
-
Object.entries(value).forEach(([k, v]) => {
|
|
664
|
-
processNewItem(v, [...path, k]);
|
|
665
|
-
});
|
|
666
|
-
} else {
|
|
667
|
-
// Primitive value
|
|
668
|
-
newShadowStore.set(nodeKey, { value });
|
|
669
|
-
}
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
processNewItem(newItem, [...arrayPath, newItemId]);
|
|
673
|
-
set({ shadowStateStore: newShadowStore });
|
|
674
|
-
|
|
675
571
|
get().notifyPathSubscribers(arrayKey, {
|
|
676
572
|
type: 'INSERT',
|
|
677
573
|
path: arrayKey,
|
|
678
|
-
itemKey:
|
|
574
|
+
itemKey: `${arrayKey}.${newItemId}`,
|
|
575
|
+
index: index ?? newKeys.length - 1,
|
|
679
576
|
});
|
|
680
577
|
},
|
|
681
|
-
removeShadowArrayElement: (key: string, itemPath: string[]) => {
|
|
682
|
-
const newShadowStore = new Map(get().shadowStateStore);
|
|
683
578
|
|
|
684
|
-
|
|
685
|
-
|
|
579
|
+
// ✅ REFACTORED: Works with `_meta.arrayKeys`.
|
|
580
|
+
removeShadowArrayElement: (key, itemPath) => {
|
|
581
|
+
if (itemPath.length === 0) return;
|
|
686
582
|
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
583
|
+
const arrayPath = itemPath.slice(0, -1);
|
|
584
|
+
const itemId = itemPath[itemPath.length - 1];
|
|
585
|
+
if (!itemId?.startsWith('id:')) return;
|
|
690
586
|
|
|
691
|
-
|
|
692
|
-
|
|
587
|
+
const arrayNode = get().getShadowNode(key, arrayPath);
|
|
588
|
+
if (!arrayNode?._meta?.arrayKeys) return;
|
|
693
589
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const indexToRemove = parentMeta.arrayKeys.findIndex(
|
|
697
|
-
(arrayItemKey) => arrayItemKey === itemKey
|
|
698
|
-
);
|
|
590
|
+
// Filter the item's ID from the `arrayKeys` metadata
|
|
591
|
+
const newKeys = arrayNode._meta.arrayKeys.filter((k) => k !== itemId);
|
|
699
592
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const newArrayKeys = parentMeta.arrayKeys.filter(
|
|
703
|
-
(arrayItemKey) => arrayItemKey !== itemKey
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
// Update parent with new array keys
|
|
707
|
-
newShadowStore.set(parentKey, {
|
|
708
|
-
...parentMeta,
|
|
709
|
-
arrayKeys: newArrayKeys,
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// Delete all data associated with the removed item
|
|
713
|
-
const prefixToDelete = itemKey + '.';
|
|
714
|
-
for (const k of Array.from(newShadowStore.keys())) {
|
|
715
|
-
if (k === itemKey || k.startsWith(prefixToDelete)) {
|
|
716
|
-
newShadowStore.delete(k);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
593
|
+
// Delete the item's data from the node
|
|
594
|
+
delete arrayNode[itemId];
|
|
721
595
|
|
|
722
|
-
|
|
596
|
+
// Persist the modified array node back to the store
|
|
597
|
+
get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
|
|
723
598
|
|
|
724
|
-
|
|
599
|
+
const arrayKey = [key, ...arrayPath].join('.');
|
|
600
|
+
get().notifyPathSubscribers(arrayKey, {
|
|
725
601
|
type: 'REMOVE',
|
|
726
|
-
path:
|
|
727
|
-
itemKey:
|
|
602
|
+
path: arrayKey,
|
|
603
|
+
itemKey: `${arrayKey}.${itemId}`,
|
|
728
604
|
});
|
|
729
605
|
},
|
|
730
606
|
|
|
731
|
-
|
|
732
|
-
|
|
607
|
+
// The rest of the functions are updated to use the new helpers (`getShadowMetadata`, `setShadowMetadata`)
|
|
608
|
+
// which abstracts away the `_meta` implementation detail.
|
|
733
609
|
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
610
|
+
addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
|
|
611
|
+
const metadata = get().getShadowMetadata(stateKey, dependencyPath) || {};
|
|
612
|
+
const newPathComponents = new Set(metadata.pathComponents);
|
|
613
|
+
newPathComponents.add(fullComponentId);
|
|
614
|
+
get().setShadowMetadata(stateKey, dependencyPath, {
|
|
615
|
+
pathComponents: newPathComponents,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const rootMeta = get().getShadowMetadata(stateKey, []);
|
|
619
|
+
if (rootMeta?.components) {
|
|
620
|
+
const component = rootMeta.components.get(fullComponentId);
|
|
621
|
+
if (component) {
|
|
622
|
+
const fullPathKey = [stateKey, ...dependencyPath].join('.');
|
|
623
|
+
const newPaths = new Set(component.paths);
|
|
624
|
+
newPaths.add(fullPathKey);
|
|
625
|
+
const newComponentRegistration = { ...component, paths: newPaths };
|
|
626
|
+
const newComponentsMap = new Map(rootMeta.components);
|
|
627
|
+
newComponentsMap.set(fullComponentId, newComponentRegistration);
|
|
628
|
+
get().setShadowMetadata(stateKey, [], { components: newComponentsMap });
|
|
629
|
+
}
|
|
738
630
|
}
|
|
631
|
+
},
|
|
739
632
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
633
|
+
registerComponent: (stateKey, fullComponentId, registration) => {
|
|
634
|
+
const rootMeta = get().getShadowMetadata(stateKey, []) || {};
|
|
635
|
+
const components = new Map(rootMeta.components);
|
|
636
|
+
components.set(fullComponentId, registration);
|
|
637
|
+
get().setShadowMetadata(stateKey, [], { components });
|
|
638
|
+
},
|
|
743
639
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (meta && meta.fields) {
|
|
754
|
-
for (const fieldKey in objectToSet) {
|
|
755
|
-
if (Object.prototype.hasOwnProperty.call(objectToSet, fieldKey)) {
|
|
756
|
-
const childValue = objectToSet[fieldKey];
|
|
757
|
-
const childFullPath = meta.fields[fieldKey];
|
|
758
|
-
|
|
759
|
-
if (childFullPath) {
|
|
760
|
-
if (isSimpleObject(childValue)) {
|
|
761
|
-
processObject(
|
|
762
|
-
childFullPath.split('.').slice(1),
|
|
763
|
-
childValue
|
|
764
|
-
);
|
|
765
|
-
} else {
|
|
766
|
-
const existingChildMeta = store.get(childFullPath) || {};
|
|
767
|
-
store.set(childFullPath, {
|
|
768
|
-
...existingChildMeta,
|
|
769
|
-
value: childValue,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
};
|
|
640
|
+
unregisterComponent: (stateKey, fullComponentId) => {
|
|
641
|
+
const rootMeta = get().getShadowMetadata(stateKey, []);
|
|
642
|
+
if (!rootMeta?.components) return;
|
|
643
|
+
const components = new Map(rootMeta.components);
|
|
644
|
+
if (components.delete(fullComponentId)) {
|
|
645
|
+
get().setShadowMetadata(stateKey, [], { components });
|
|
646
|
+
}
|
|
647
|
+
},
|
|
777
648
|
|
|
778
|
-
|
|
649
|
+
// ✅ REFACTORED: `markAsDirty` now correctly writes to `_meta.isDirty`.
|
|
650
|
+
markAsDirty: (key, path, options = { bubble: true }) => {
|
|
651
|
+
const setDirtyOnPath = (pathToMark: string[]) => {
|
|
652
|
+
const node = get().getShadowNode(key, pathToMark);
|
|
653
|
+
if (node?._meta?.isDirty) {
|
|
654
|
+
return true; // Already dirty, stop bubbling
|
|
779
655
|
}
|
|
656
|
+
get().setShadowMetadata(key, pathToMark, { isDirty: true });
|
|
657
|
+
return false; // Was not dirty before
|
|
658
|
+
};
|
|
780
659
|
|
|
781
|
-
|
|
782
|
-
get().notifyPathSubscribers(fullKey, { type: 'UPDATE', newValue });
|
|
660
|
+
setDirtyOnPath(path);
|
|
783
661
|
|
|
784
|
-
|
|
785
|
-
|
|
662
|
+
if (options.bubble) {
|
|
663
|
+
let parentPath = [...path];
|
|
664
|
+
while (parentPath.length > 0) {
|
|
665
|
+
parentPath.pop();
|
|
666
|
+
if (setDirtyOnPath(parentPath)) {
|
|
667
|
+
break; // Stop if parent was already dirty
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
serverStateUpdates: new Map(),
|
|
674
|
+
setServerStateUpdate: (key, serverState) => {
|
|
675
|
+
set((state) => ({
|
|
676
|
+
serverStateUpdates: new Map(state.serverStateUpdates).set(
|
|
677
|
+
key,
|
|
678
|
+
serverState
|
|
679
|
+
),
|
|
680
|
+
}));
|
|
681
|
+
get().notifyPathSubscribers(key, {
|
|
682
|
+
type: 'SERVER_STATE_UPDATE',
|
|
683
|
+
serverState,
|
|
786
684
|
});
|
|
787
685
|
},
|
|
686
|
+
|
|
687
|
+
pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
|
|
688
|
+
subscribeToPath: (path, callback) => {
|
|
689
|
+
const subscribers = get().pathSubscribers;
|
|
690
|
+
const subsForPath = subscribers.get(path) || new Set();
|
|
691
|
+
subsForPath.add(callback);
|
|
692
|
+
subscribers.set(path, subsForPath);
|
|
693
|
+
|
|
694
|
+
return () => {
|
|
695
|
+
const currentSubs = get().pathSubscribers.get(path);
|
|
696
|
+
if (currentSubs) {
|
|
697
|
+
currentSubs.delete(callback);
|
|
698
|
+
if (currentSubs.size === 0) {
|
|
699
|
+
get().pathSubscribers.delete(path);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
},
|
|
704
|
+
notifyPathSubscribers: (updatedPath, newValue) => {
|
|
705
|
+
const subscribers = get().pathSubscribers;
|
|
706
|
+
const subs = subscribers.get(updatedPath);
|
|
707
|
+
if (subs) {
|
|
708
|
+
subs.forEach((callback) => callback(newValue));
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
|
|
788
712
|
selectedIndicesMap: new Map<string, string>(),
|
|
789
|
-
getSelectedIndex: (arrayKey
|
|
713
|
+
getSelectedIndex: (arrayKey, validIds) => {
|
|
790
714
|
const itemKey = get().selectedIndicesMap.get(arrayKey);
|
|
791
|
-
|
|
792
715
|
if (!itemKey) return -1;
|
|
793
716
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (!arrayKeys) return -1;
|
|
717
|
+
const arrayMeta = get().getShadowMetadata(
|
|
718
|
+
arrayKey.split('.')[0]!,
|
|
719
|
+
arrayKey.split('.').slice(1)
|
|
720
|
+
);
|
|
721
|
+
const arrayKeys = validIds || arrayMeta?.arrayKeys;
|
|
800
722
|
|
|
801
|
-
return arrayKeys.indexOf(itemKey);
|
|
723
|
+
return arrayKeys ? arrayKeys.indexOf(itemKey) : -1;
|
|
802
724
|
},
|
|
803
725
|
|
|
804
|
-
setSelectedIndex: (arrayKey
|
|
726
|
+
setSelectedIndex: (arrayKey, itemKey) => {
|
|
805
727
|
set((state) => {
|
|
806
|
-
const newMap = state.selectedIndicesMap;
|
|
728
|
+
const newMap = new Map(state.selectedIndicesMap);
|
|
729
|
+
const oldSelection = newMap.get(arrayKey);
|
|
730
|
+
if (oldSelection) {
|
|
731
|
+
get().notifyPathSubscribers(oldSelection, { type: 'THIS_UNSELECTED' });
|
|
732
|
+
}
|
|
807
733
|
|
|
808
734
|
if (itemKey === undefined) {
|
|
809
735
|
newMap.delete(arrayKey);
|
|
810
736
|
} else {
|
|
811
|
-
if (newMap.has(arrayKey)) {
|
|
812
|
-
get().notifyPathSubscribers(newMap.get(arrayKey)!, {
|
|
813
|
-
type: 'THIS_UNSELECTED',
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
737
|
newMap.set(arrayKey, itemKey);
|
|
817
|
-
|
|
818
|
-
get().notifyPathSubscribers(itemKey, {
|
|
819
|
-
type: 'THIS_SELECTED',
|
|
820
|
-
});
|
|
738
|
+
get().notifyPathSubscribers(itemKey, { type: 'THIS_SELECTED' });
|
|
821
739
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
return {
|
|
826
|
-
...state,
|
|
827
|
-
selectedIndicesMap: newMap,
|
|
828
|
-
};
|
|
740
|
+
|
|
741
|
+
get().notifyPathSubscribers(arrayKey, { type: 'GET_SELECTED' });
|
|
742
|
+
return { selectedIndicesMap: newMap };
|
|
829
743
|
});
|
|
830
744
|
},
|
|
831
|
-
|
|
745
|
+
|
|
746
|
+
clearSelectedIndex: ({ arrayKey }) => {
|
|
832
747
|
set((state) => {
|
|
833
|
-
const newMap = state.selectedIndicesMap;
|
|
834
|
-
const
|
|
835
|
-
if (
|
|
836
|
-
get().notifyPathSubscribers(
|
|
837
|
-
type: 'CLEAR_SELECTION',
|
|
838
|
-
});
|
|
748
|
+
const newMap = new Map(state.selectedIndicesMap);
|
|
749
|
+
const actualKey = newMap.get(arrayKey);
|
|
750
|
+
if (actualKey) {
|
|
751
|
+
get().notifyPathSubscribers(actualKey, { type: 'CLEAR_SELECTION' });
|
|
839
752
|
}
|
|
840
|
-
|
|
841
753
|
newMap.delete(arrayKey);
|
|
842
|
-
get().notifyPathSubscribers(arrayKey, {
|
|
843
|
-
|
|
844
|
-
});
|
|
845
|
-
return {
|
|
846
|
-
...state,
|
|
847
|
-
selectedIndicesMap: newMap,
|
|
848
|
-
};
|
|
754
|
+
get().notifyPathSubscribers(arrayKey, { type: 'CLEAR_SELECTION' });
|
|
755
|
+
return { selectedIndicesMap: newMap };
|
|
849
756
|
});
|
|
850
757
|
},
|
|
851
|
-
|
|
758
|
+
|
|
759
|
+
clearSelectedIndexesForState: (stateKey) => {
|
|
852
760
|
set((state) => {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
761
|
+
const newMap = new Map(state.selectedIndicesMap);
|
|
762
|
+
let changed = false;
|
|
763
|
+
for (const key of newMap.keys()) {
|
|
764
|
+
if (key === stateKey || key.startsWith(stateKey + '.')) {
|
|
765
|
+
newMap.delete(key);
|
|
766
|
+
changed = true;
|
|
767
|
+
}
|
|
859
768
|
}
|
|
769
|
+
return changed ? { selectedIndicesMap: newMap } : {};
|
|
860
770
|
});
|
|
861
771
|
},
|
|
862
772
|
|
|
863
773
|
initialStateOptions: {},
|
|
864
|
-
|
|
865
|
-
stateTimeline: {},
|
|
866
|
-
cogsStateStore: {},
|
|
867
774
|
stateLog: new Map(),
|
|
868
|
-
|
|
869
775
|
initialStateGlobal: {},
|
|
870
776
|
|
|
871
|
-
|
|
872
|
-
|
|
777
|
+
addStateLog: (updates) => {
|
|
778
|
+
if (!updates || updates.length === 0) return;
|
|
873
779
|
set((state) => {
|
|
874
780
|
const newLog = new Map(state.stateLog);
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
781
|
+
const logsGroupedByKey = new Map<string, UpdateTypeDetail[]>();
|
|
782
|
+
for (const update of updates) {
|
|
783
|
+
const group = logsGroupedByKey.get(update.stateKey) || [];
|
|
784
|
+
group.push(update);
|
|
785
|
+
logsGroupedByKey.set(update.stateKey, group);
|
|
786
|
+
}
|
|
787
|
+
for (const [key, batchOfUpdates] of logsGroupedByKey.entries()) {
|
|
788
|
+
const newStateLogForKey = new Map(newLog.get(key));
|
|
789
|
+
for (const update of batchOfUpdates) {
|
|
790
|
+
newStateLogForKey.set(JSON.stringify(update.path), { ...update });
|
|
791
|
+
}
|
|
792
|
+
newLog.set(key, newStateLogForKey);
|
|
886
793
|
}
|
|
887
|
-
|
|
888
|
-
newLog.set(key, stateLogForKey);
|
|
889
794
|
return { stateLog: newLog };
|
|
890
795
|
});
|
|
891
796
|
},
|
|
892
797
|
|
|
893
|
-
getInitialOptions: (key) =>
|
|
894
|
-
return get().initialStateOptions[key];
|
|
895
|
-
},
|
|
896
|
-
|
|
798
|
+
getInitialOptions: (key) => get().initialStateOptions[key],
|
|
897
799
|
setInitialStateOptions: (key, value) => {
|
|
898
800
|
set((prev) => ({
|
|
899
|
-
initialStateOptions: {
|
|
900
|
-
...prev.initialStateOptions,
|
|
901
|
-
[key]: value,
|
|
902
|
-
},
|
|
801
|
+
initialStateOptions: { ...prev.initialStateOptions, [key]: value },
|
|
903
802
|
}));
|
|
904
803
|
},
|
|
905
804
|
updateInitialStateGlobal: (key, newState) => {
|
|
906
805
|
set((prev) => ({
|
|
907
|
-
initialStateGlobal: {
|
|
908
|
-
...prev.initialStateGlobal,
|
|
909
|
-
[key]: newState,
|
|
910
|
-
},
|
|
806
|
+
initialStateGlobal: { ...prev.initialStateGlobal, [key]: newState },
|
|
911
807
|
}));
|
|
912
808
|
},
|
|
913
809
|
|
|
914
810
|
syncInfoStore: new Map<string, SyncInfo>(),
|
|
915
|
-
setSyncInfo: (key
|
|
811
|
+
setSyncInfo: (key, syncInfo) =>
|
|
916
812
|
set((state) => {
|
|
917
813
|
const newMap = new Map(state.syncInfoStore);
|
|
918
814
|
newMap.set(key, syncInfo);
|
|
919
|
-
return {
|
|
815
|
+
return { syncInfoStore: newMap };
|
|
920
816
|
}),
|
|
921
|
-
getSyncInfo: (key
|
|
817
|
+
getSyncInfo: (key) => get().syncInfoStore.get(key) || null,
|
|
922
818
|
}));
|