cogsbox-state 0.5.434 → 0.5.436
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 +40 -42
- package/dist/CogsState.jsx +1358 -1155
- package/dist/CogsState.jsx.map +1 -1
- package/dist/CogsStateClient.jsx.map +1 -1
- package/dist/Functions.d.ts +1 -5
- package/dist/Functions.jsx +17 -49
- package/dist/Functions.jsx.map +1 -1
- package/dist/TRPCValidationLink.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.js +247 -185
- package/dist/store.js.map +1 -1
- package/dist/utility.js.map +1 -1
- package/package.json +6 -5
- package/src/CogsState.tsx +816 -211
- package/src/Functions.tsx +8 -241
- package/src/TRPCValidationLink.ts +12 -12
- package/src/store.ts +209 -43
- package/dist/useValidateZodPath.d.ts +0 -34
- package/src/useValidateZodPath.ts +0 -231
package/src/Functions.tsx
CHANGED
|
@@ -1,260 +1,29 @@
|
|
|
1
1
|
import { type FormOptsType } from './CogsState';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import { getGlobalStore } from './store';
|
|
6
5
|
|
|
7
|
-
export const useStoreSubscription = <T,>(
|
|
8
|
-
fullPath: string,
|
|
9
|
-
selector: (
|
|
10
|
-
store: ReturnType<typeof getGlobalStore.getState>,
|
|
11
|
-
path: string
|
|
12
|
-
) => T,
|
|
13
|
-
compare: (a: T, b: T) => boolean = (a, b) =>
|
|
14
|
-
JSON.stringify(a) === JSON.stringify(b)
|
|
15
|
-
) => {
|
|
16
|
-
const [value, setValue] = useState<T>(() =>
|
|
17
|
-
selector(getGlobalStore.getState(), fullPath)
|
|
18
|
-
);
|
|
19
|
-
const previousValueRef = useRef<T>(value);
|
|
20
|
-
const fullPathRef = useRef(fullPath);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
fullPathRef.current = fullPath; // Ensure latest fullPath is always used
|
|
23
|
-
|
|
24
|
-
setValue(selector(getGlobalStore.getState(), fullPath));
|
|
25
|
-
|
|
26
|
-
const callback = (store: any) => {
|
|
27
|
-
const newValue = selector(store, fullPathRef.current);
|
|
28
|
-
|
|
29
|
-
if (!compare(previousValueRef.current, newValue)) {
|
|
30
|
-
previousValueRef.current = newValue;
|
|
31
|
-
setValue(newValue);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
const unsubscribe = getGlobalStore.subscribe(callback);
|
|
35
|
-
return () => {
|
|
36
|
-
unsubscribe();
|
|
37
|
-
};
|
|
38
|
-
}, [fullPath]);
|
|
39
|
-
return value;
|
|
40
|
-
};
|
|
41
|
-
export const useGetValidationErrors = (
|
|
42
|
-
validationKey: string,
|
|
43
|
-
path: string[],
|
|
44
|
-
validIndices?: number[]
|
|
45
|
-
) => {
|
|
46
|
-
const fullPath =
|
|
47
|
-
validationKey +
|
|
48
|
-
'.' +
|
|
49
|
-
(path.length > 0 ? [path.join('.')] : []) +
|
|
50
|
-
(validIndices && validIndices.length > 0 ? '.' + validIndices : '');
|
|
51
|
-
|
|
52
|
-
const returnresult = useStoreSubscription(
|
|
53
|
-
fullPath,
|
|
54
|
-
(store, path) => store.getValidationErrors(path) || []
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
return returnresult;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Find FormControlComponent in your Functions.ts or equivalent file
|
|
61
|
-
|
|
62
|
-
// export const FormControlComponent = <TStateObject,>({
|
|
63
|
-
// setState, // This is the real effectiveSetState from the hook
|
|
64
|
-
// path,
|
|
65
|
-
// child,
|
|
66
|
-
// formOpts,
|
|
67
|
-
// stateKey,
|
|
68
|
-
// rebuildStateShape,
|
|
69
|
-
// }: FormControlComponentProps<TStateObject>) => {
|
|
70
|
-
// const { registerFormRef, getFormRef } = formRefStore.getState();
|
|
71
|
-
// const {
|
|
72
|
-
// getValidationErrors,
|
|
73
|
-
// addValidationError,
|
|
74
|
-
// getInitialOptions,
|
|
75
|
-
// removeValidationError,
|
|
76
|
-
// } = getGlobalStore.getState();
|
|
77
|
-
// const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
78
|
-
// const [, forceUpdate] = useState<any>();
|
|
79
|
-
// getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
|
|
80
|
-
// forceUpdate({});
|
|
81
|
-
// });
|
|
82
|
-
|
|
83
|
-
// const refKey = stateKey + '.' + path.join('.');
|
|
84
|
-
// const localFormRef = useRef<HTMLInputElement>(null);
|
|
85
|
-
// const existingRef = getFormRef(refKey);
|
|
86
|
-
// if (!existingRef) {
|
|
87
|
-
// registerFormRef(refKey, localFormRef);
|
|
88
|
-
// }
|
|
89
|
-
// const formRef = existingRef || localFormRef;
|
|
90
|
-
|
|
91
|
-
// // --- START CHANGES ---
|
|
92
|
-
|
|
93
|
-
// const globalStateValue = getGlobalStore
|
|
94
|
-
// .getState()
|
|
95
|
-
// .getShadowValue(stateKeyPathKey);
|
|
96
|
-
// const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
97
|
-
// const isCurrentlyDebouncing = useRef(false);
|
|
98
|
-
// const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
99
|
-
|
|
100
|
-
// // Effect to sync local state if global state changes externally
|
|
101
|
-
// useEffect(() => {
|
|
102
|
-
// // Only update local if not actively debouncing a local change
|
|
103
|
-
// if (!isCurrentlyDebouncing.current && globalStateValue !== localValue) {
|
|
104
|
-
// setLocalValue(globalStateValue);
|
|
105
|
-
// }
|
|
106
|
-
// }, [globalStateValue]); // Removed localValue dependency
|
|
107
|
-
|
|
108
|
-
// // Effect for cleanup
|
|
109
|
-
// useEffect(() => {
|
|
110
|
-
// return () => {
|
|
111
|
-
// if (debounceTimeoutRef.current) {
|
|
112
|
-
// clearTimeout(debounceTimeoutRef.current);
|
|
113
|
-
// debounceTimeoutRef.current = null; // Explicitly nullify
|
|
114
|
-
// isCurrentlyDebouncing.current = false;
|
|
115
|
-
// }
|
|
116
|
-
// };
|
|
117
|
-
// }, []);
|
|
118
|
-
|
|
119
|
-
// const debouncedUpdater = (payload: UpdateArg<TStateObject>) => {
|
|
120
|
-
// setLocalValue(payload); // Update local state immediately
|
|
121
|
-
// isCurrentlyDebouncing.current = true;
|
|
122
|
-
|
|
123
|
-
// if (payload === '') {
|
|
124
|
-
// if (debounceTimeoutRef.current) {
|
|
125
|
-
// clearTimeout(debounceTimeoutRef.current); // Clear pending timer
|
|
126
|
-
// debounceTimeoutRef.current = null;
|
|
127
|
-
// }
|
|
128
|
-
|
|
129
|
-
// setState(payload, path, { updateType: 'update' });
|
|
130
|
-
// isCurrentlyDebouncing.current = false; // No longer debouncing
|
|
131
|
-
// return; // Don't proceed to set another timeout
|
|
132
|
-
// }
|
|
133
|
-
|
|
134
|
-
// // If not empty, proceed with normal debouncing
|
|
135
|
-
// if (debounceTimeoutRef.current) {
|
|
136
|
-
// clearTimeout(debounceTimeoutRef.current);
|
|
137
|
-
// }
|
|
138
|
-
|
|
139
|
-
// debounceTimeoutRef.current = setTimeout(
|
|
140
|
-
// () => {
|
|
141
|
-
// isCurrentlyDebouncing.current = false;
|
|
142
|
-
// console.log('debouncedUpdater', payload);
|
|
143
|
-
// setState(payload, path, { updateType: 'update' });
|
|
144
|
-
// },
|
|
145
|
-
// formOpts?.debounceTime ??
|
|
146
|
-
// (typeof globalStateValue == 'boolean' ? 20 : 200)
|
|
147
|
-
// );
|
|
148
|
-
// };
|
|
149
|
-
|
|
150
|
-
// const initialOptions = getInitialOptions(stateKey);
|
|
151
|
-
|
|
152
|
-
// const validationKey = initialOptions?.validation?.key;
|
|
153
|
-
// const validateOnBlur = initialOptions?.validation?.onBlur === true;
|
|
154
|
-
|
|
155
|
-
// const handleBlur = async () => {
|
|
156
|
-
// // --- Ensure latest value is flushed if debouncing ---
|
|
157
|
-
// if (debounceTimeoutRef.current) {
|
|
158
|
-
// clearTimeout(debounceTimeoutRef.current); // Clear pending timer
|
|
159
|
-
// debounceTimeoutRef.current = null;
|
|
160
|
-
// isCurrentlyDebouncing.current = false;
|
|
161
|
-
// // Ensure the absolute latest local value is committed on blur
|
|
162
|
-
// setState(localValue, path, { updateType: 'update' });
|
|
163
|
-
// }
|
|
164
|
-
// // --- End modification ---
|
|
165
|
-
|
|
166
|
-
// if (!initialOptions?.validation?.zodSchema || !validateOnBlur) return;
|
|
167
|
-
// removeValidationError(validationKey + '.' + path.join('.'));
|
|
168
|
-
// try {
|
|
169
|
-
// // Use the potentially just flushed value
|
|
170
|
-
// if (!validationKey) return;
|
|
171
|
-
// const fieldValue = getGlobalStore
|
|
172
|
-
// .getState()
|
|
173
|
-
// .getShadowValue(stateKeyPathKey);
|
|
174
|
-
// await validateZodPathFunc(
|
|
175
|
-
// validationKey,
|
|
176
|
-
// initialOptions.validation.zodSchema,
|
|
177
|
-
// path,
|
|
178
|
-
// fieldValue
|
|
179
|
-
// );
|
|
180
|
-
// // forceUpdate might be needed if validation state update doesn't trigger render
|
|
181
|
-
// // Consider using useGetValidationErrors hook result directly for validation display
|
|
182
|
-
// } catch (error) {
|
|
183
|
-
// console.error('Validation error on blur:', error);
|
|
184
|
-
// }
|
|
185
|
-
// };
|
|
186
|
-
|
|
187
|
-
// const childElement = child({
|
|
188
|
-
// state: setter,
|
|
189
|
-
// // --- START CHANGES ---
|
|
190
|
-
// get: () => localValue, // Get should return the immediate local value
|
|
191
|
-
// set: debouncedUpdater, // Use the new debounced updater
|
|
192
|
-
// // --- END CHANGES ---
|
|
193
|
-
|
|
194
|
-
// path: path,
|
|
195
|
-
// validationErrors: () =>
|
|
196
|
-
// getValidationErrors(validationKey + '.' + path.join('.')),
|
|
197
|
-
// addValidationError: (message?: string) => {
|
|
198
|
-
// removeValidationError(validationKey + '.' + path.join('.'));
|
|
199
|
-
// addValidationError(validationKey + '.' + path.join('.'), message ?? '');
|
|
200
|
-
// },
|
|
201
|
-
// inputProps: {
|
|
202
|
-
// // --- START CHANGES ---
|
|
203
|
-
// value: localValue ?? '', // Input value is always the local state
|
|
204
|
-
// onChange: (e: any) => debouncedUpdater(e.target.value), // Use debounced updater
|
|
205
|
-
// // --- END CHANGES ---
|
|
206
|
-
// onBlur: handleBlur,
|
|
207
|
-
// ref: formRef,
|
|
208
|
-
// },
|
|
209
|
-
// });
|
|
210
|
-
|
|
211
|
-
// return (
|
|
212
|
-
// <>
|
|
213
|
-
// <ValidationWrapper {...{ formOpts, path, stateKey }}>
|
|
214
|
-
// {childElement}
|
|
215
|
-
// </ValidationWrapper>
|
|
216
|
-
// </>
|
|
217
|
-
// );
|
|
218
|
-
// };
|
|
219
6
|
export type ValidationWrapperProps = {
|
|
220
7
|
formOpts?: FormOptsType;
|
|
221
8
|
path: string[];
|
|
222
9
|
stateKey: string;
|
|
223
10
|
children: React.ReactNode;
|
|
224
|
-
validIndices?: number[];
|
|
225
11
|
};
|
|
226
12
|
export function ValidationWrapper({
|
|
227
13
|
formOpts,
|
|
228
14
|
path,
|
|
229
|
-
|
|
230
15
|
stateKey,
|
|
231
16
|
children,
|
|
232
|
-
validIndices,
|
|
233
17
|
}: ValidationWrapperProps) {
|
|
234
|
-
const { getInitialOptions } = getGlobalStore.getState();
|
|
18
|
+
const { getInitialOptions, getShadowMetadata } = getGlobalStore.getState();
|
|
235
19
|
const thisStateOpts = getInitialOptions(stateKey!);
|
|
236
|
-
const validationKey = thisStateOpts?.validation?.key ?? stateKey!;
|
|
237
|
-
const validationErrors = useGetValidationErrors(
|
|
238
|
-
validationKey,
|
|
239
|
-
path,
|
|
240
|
-
validIndices
|
|
241
|
-
);
|
|
242
|
-
// console.log(
|
|
243
|
-
// "validationErrors ValidationWrapper",
|
|
244
|
-
// stateKey,
|
|
245
|
-
// validationKey,
|
|
246
|
-
// path,
|
|
247
|
-
// validationErrors
|
|
248
|
-
// );
|
|
249
|
-
const thesMessages: string[] = [];
|
|
250
20
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
}
|
|
21
|
+
// GET VALIDATION FROM SHADOW METADATA
|
|
22
|
+
const shadowMeta = getShadowMetadata(stateKey!, path);
|
|
23
|
+
const validationState = shadowMeta?.validation;
|
|
24
|
+
const status = validationState?.status || 'PRISTINE';
|
|
257
25
|
|
|
26
|
+
const message = validationState?.message;
|
|
258
27
|
return (
|
|
259
28
|
<>
|
|
260
29
|
{thisStateOpts?.formElements?.validation &&
|
|
@@ -263,12 +32,10 @@ export function ValidationWrapper({
|
|
|
263
32
|
children: (
|
|
264
33
|
<React.Fragment key={path.toString()}>{children}</React.Fragment>
|
|
265
34
|
),
|
|
266
|
-
|
|
35
|
+
status, // Pass status instead of active
|
|
267
36
|
message: formOpts?.validation?.hideMessage
|
|
268
37
|
? ''
|
|
269
|
-
: formOpts?.validation?.message
|
|
270
|
-
? formOpts?.validation?.message
|
|
271
|
-
: thesMessages.map((m) => m).join(', '),
|
|
38
|
+
: formOpts?.validation?.message || message || '',
|
|
272
39
|
path: path,
|
|
273
40
|
})
|
|
274
41
|
) : (
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { observable } from
|
|
2
|
-
import type { AnyRouter } from
|
|
3
|
-
import type { TRPCLink } from
|
|
4
|
-
import type { Operation } from
|
|
5
|
-
import type { TRPCClientError } from
|
|
6
|
-
import { getGlobalStore } from
|
|
7
|
-
import type { Observer } from
|
|
1
|
+
import { observable } from '@trpc/server/observable';
|
|
2
|
+
import type { AnyRouter } from '@trpc/server';
|
|
3
|
+
import type { TRPCLink } from '@trpc/client';
|
|
4
|
+
import type { Operation } from '@trpc/client';
|
|
5
|
+
import type { TRPCClientError } from '@trpc/client';
|
|
6
|
+
import { getGlobalStore } from './store';
|
|
7
|
+
import type { Observer } from '@trpc/server/observable';
|
|
8
8
|
export const useCogsTrpcValidationLink = <
|
|
9
9
|
TRouter extends AnyRouter,
|
|
10
10
|
>(passedOpts?: {
|
|
@@ -25,27 +25,27 @@ export const useCogsTrpcValidationLink = <
|
|
|
25
25
|
try {
|
|
26
26
|
const errorObject = JSON.parse(err.message);
|
|
27
27
|
if (passedOpts?.log) {
|
|
28
|
-
console.log(
|
|
28
|
+
console.log('errorObject', errorObject);
|
|
29
29
|
}
|
|
30
30
|
if (Array.isArray(errorObject)) {
|
|
31
31
|
errorObject.forEach(
|
|
32
32
|
(error: { path: string[]; message: string }) => {
|
|
33
|
-
const fullpath = `${op.path}.${error.path.join(
|
|
33
|
+
const fullpath = `${op.path}.${error.path.join('.')}`;
|
|
34
34
|
// In your TRPC link
|
|
35
35
|
if (passedOpts?.log) {
|
|
36
|
-
console.log(
|
|
36
|
+
console.log('fullpath 1', fullpath);
|
|
37
37
|
}
|
|
38
38
|
addValidationError(fullpath, error.message);
|
|
39
39
|
}
|
|
40
40
|
);
|
|
41
41
|
} else if (
|
|
42
|
-
typeof errorObject ===
|
|
42
|
+
typeof errorObject === 'object' &&
|
|
43
43
|
errorObject !== null
|
|
44
44
|
) {
|
|
45
45
|
Object.entries(errorObject).forEach(([key, value]) => {
|
|
46
46
|
const fullpath = `${op.path}.${key}`;
|
|
47
47
|
if (passedOpts?.log) {
|
|
48
|
-
console.log(
|
|
48
|
+
console.log('fullpath 2', fullpath);
|
|
49
49
|
}
|
|
50
50
|
addValidationError(fullpath, value as string);
|
|
51
51
|
});
|
package/src/store.ts
CHANGED
|
@@ -105,6 +105,7 @@ export type ShadowMetadata = {
|
|
|
105
105
|
domRef?: HTMLElement | null;
|
|
106
106
|
};
|
|
107
107
|
syncInfo?: { status: string };
|
|
108
|
+
validation?: ValidationState;
|
|
108
109
|
lastUpdated?: number;
|
|
109
110
|
value?: any;
|
|
110
111
|
classSignals?: Array<{
|
|
@@ -151,6 +152,24 @@ export type ShadowMetadata = {
|
|
|
151
152
|
}
|
|
152
153
|
>;
|
|
153
154
|
} & ComponentsType;
|
|
155
|
+
|
|
156
|
+
export type ValidationStatus =
|
|
157
|
+
| 'PRISTINE' // Untouched, matches initial state.
|
|
158
|
+
| 'DIRTY' // Changed, but no validation run yet.
|
|
159
|
+
| 'VALID_LIVE' // Valid while typing.
|
|
160
|
+
| 'INVALID_LIVE' // Gentle error during typing.
|
|
161
|
+
| 'VALIDATION_FAILED' // Hard error on blur/submit.
|
|
162
|
+
| 'VALID_PENDING_SYNC' // Passed validation, ready for sync.
|
|
163
|
+
| 'SYNCING' // Actively being sent to the server.
|
|
164
|
+
| 'SYNCED' // Server confirmed success.
|
|
165
|
+
| 'SYNC_FAILED'; // Server rejected the data.
|
|
166
|
+
|
|
167
|
+
export type ValidationState = {
|
|
168
|
+
status: ValidationStatus;
|
|
169
|
+
message?: string;
|
|
170
|
+
lastValidated?: number;
|
|
171
|
+
validatedValue?: any;
|
|
172
|
+
};
|
|
154
173
|
export type CogsEvent =
|
|
155
174
|
| { type: 'INSERT'; path: string; itemKey: string; index: number }
|
|
156
175
|
| { type: 'REMOVE'; path: string; itemKey: string }
|
|
@@ -159,6 +178,17 @@ export type CogsEvent =
|
|
|
159
178
|
| { type: 'RELOAD'; path: string }; // For full re-initializations
|
|
160
179
|
export type CogsGlobalState = {
|
|
161
180
|
// --- Shadow State and Subscription System ---
|
|
181
|
+
registerComponent: (
|
|
182
|
+
stateKey: string,
|
|
183
|
+
componentId: string,
|
|
184
|
+
registration: any
|
|
185
|
+
) => void;
|
|
186
|
+
unregisterComponent: (stateKey: string, componentId: string) => void;
|
|
187
|
+
addPathComponent: (
|
|
188
|
+
stateKey: string,
|
|
189
|
+
dependencyPath: string[],
|
|
190
|
+
fullComponentId: string
|
|
191
|
+
) => void;
|
|
162
192
|
shadowStateStore: Map<string, ShadowMetadata>;
|
|
163
193
|
markAsDirty: (
|
|
164
194
|
key: string,
|
|
@@ -275,20 +305,103 @@ const isSimpleObject = (value: any): boolean => {
|
|
|
275
305
|
return Array.isArray(value) || value.constructor === Object;
|
|
276
306
|
};
|
|
277
307
|
export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
308
|
+
addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
|
|
309
|
+
set((state) => {
|
|
310
|
+
const newShadowStore = new Map(state.shadowStateStore);
|
|
311
|
+
const dependencyKey = [stateKey, ...dependencyPath].join('.');
|
|
312
|
+
|
|
313
|
+
// --- Part 1: Update the path's own metadata ---
|
|
314
|
+
const pathMeta = newShadowStore.get(dependencyKey) || {};
|
|
315
|
+
// Create a *new* Set to ensure immutability
|
|
316
|
+
const pathComponents = new Set(pathMeta.pathComponents);
|
|
317
|
+
pathComponents.add(fullComponentId);
|
|
318
|
+
// Update the metadata for the specific path
|
|
319
|
+
newShadowStore.set(dependencyKey, { ...pathMeta, pathComponents });
|
|
320
|
+
|
|
321
|
+
// --- Part 2: Update the component's own list of paths ---
|
|
322
|
+
const rootMeta = newShadowStore.get(stateKey) || {};
|
|
323
|
+
const component = rootMeta.components?.get(fullComponentId);
|
|
324
|
+
|
|
325
|
+
// If the component exists, update its `paths` set immutably
|
|
326
|
+
if (component) {
|
|
327
|
+
const newPaths = new Set(component.paths);
|
|
328
|
+
newPaths.add(dependencyKey);
|
|
329
|
+
|
|
330
|
+
const newComponentRegistration = { ...component, paths: newPaths };
|
|
331
|
+
const newComponentsMap = new Map(rootMeta.components);
|
|
332
|
+
newComponentsMap.set(fullComponentId, newComponentRegistration);
|
|
333
|
+
|
|
334
|
+
// Update the root metadata with the new components map
|
|
335
|
+
newShadowStore.set(stateKey, {
|
|
336
|
+
...rootMeta,
|
|
337
|
+
components: newComponentsMap,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Return the final, updated state
|
|
342
|
+
return { shadowStateStore: newShadowStore };
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
registerComponent: (stateKey, fullComponentId, registration) => {
|
|
346
|
+
set((state) => {
|
|
347
|
+
// Create a new Map to ensure Zustand detects the change
|
|
348
|
+
const newShadowStore = new Map(state.shadowStateStore);
|
|
349
|
+
|
|
350
|
+
// Get the metadata for the ROOT of the state (where the components map lives)
|
|
351
|
+
const rootMeta = newShadowStore.get(stateKey) || {};
|
|
352
|
+
|
|
353
|
+
// Also clone the components map to avoid direct mutation
|
|
354
|
+
const components = new Map(rootMeta.components);
|
|
355
|
+
components.set(fullComponentId, registration);
|
|
356
|
+
|
|
357
|
+
// Update the root metadata with the new components map
|
|
358
|
+
newShadowStore.set(stateKey, { ...rootMeta, components });
|
|
359
|
+
|
|
360
|
+
// Return the updated state
|
|
361
|
+
return { shadowStateStore: newShadowStore };
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
unregisterComponent: (stateKey, fullComponentId) => {
|
|
366
|
+
set((state) => {
|
|
367
|
+
const newShadowStore = new Map(state.shadowStateStore);
|
|
368
|
+
const rootMeta = newShadowStore.get(stateKey);
|
|
369
|
+
|
|
370
|
+
// If there's no metadata or no components map, do nothing
|
|
371
|
+
if (!rootMeta?.components) {
|
|
372
|
+
return state; // Return original state, no change needed
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const components = new Map(rootMeta.components);
|
|
376
|
+
const wasDeleted = components.delete(fullComponentId);
|
|
377
|
+
|
|
378
|
+
// Only update state if something was actually deleted
|
|
379
|
+
if (wasDeleted) {
|
|
380
|
+
newShadowStore.set(stateKey, { ...rootMeta, components });
|
|
381
|
+
return { shadowStateStore: newShadowStore };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return state; // Nothing changed
|
|
385
|
+
});
|
|
386
|
+
},
|
|
278
387
|
markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
|
|
279
388
|
const newShadowStore = new Map(get().shadowStateStore);
|
|
280
389
|
let changed = false;
|
|
281
390
|
|
|
282
|
-
// This function marks a single path as dirty if it was previously synced.
|
|
283
391
|
const setDirty = (currentPath: string[]) => {
|
|
284
392
|
const fullKey = [key, ...currentPath].join('.');
|
|
285
393
|
const meta = newShadowStore.get(fullKey);
|
|
286
394
|
|
|
287
|
-
// We
|
|
288
|
-
//
|
|
289
|
-
if (meta && meta.
|
|
395
|
+
// We mark something as dirty if it isn't already.
|
|
396
|
+
// The original data source doesn't matter.
|
|
397
|
+
if (meta && meta.isDirty !== true) {
|
|
290
398
|
newShadowStore.set(fullKey, { ...meta, isDirty: true });
|
|
291
399
|
changed = true;
|
|
400
|
+
} else if (!meta) {
|
|
401
|
+
// If there's no metadata, create it and mark it as dirty.
|
|
402
|
+
// This handles newly created fields within an object.
|
|
403
|
+
newShadowStore.set(fullKey, { isDirty: true });
|
|
404
|
+
changed = true;
|
|
292
405
|
}
|
|
293
406
|
};
|
|
294
407
|
|
|
@@ -304,7 +417,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
304
417
|
}
|
|
305
418
|
}
|
|
306
419
|
|
|
307
|
-
// Only update the global state if something actually changed.
|
|
308
420
|
if (changed) {
|
|
309
421
|
set({ shadowStateStore: newShadowStore });
|
|
310
422
|
}
|
|
@@ -344,54 +456,72 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
344
456
|
},
|
|
345
457
|
|
|
346
458
|
notifyPathSubscribers: (updatedPath, newValue) => {
|
|
347
|
-
// <-- Now accepts newValue
|
|
348
459
|
const subscribers = get().pathSubscribers;
|
|
349
460
|
const subs = subscribers.get(updatedPath);
|
|
350
461
|
|
|
351
462
|
if (subs) {
|
|
352
|
-
// Pass the newValue to every callback
|
|
353
463
|
subs.forEach((callback) => callback(newValue));
|
|
354
464
|
}
|
|
355
465
|
},
|
|
356
|
-
initializeShadowState: (key: string, initialState: any) => {
|
|
357
|
-
const existingShadowStore = new Map(get().shadowStateStore);
|
|
358
|
-
|
|
359
|
-
const processValue = (value: any, path: string[]) => {
|
|
360
|
-
const nodeKey = [key, ...path].join('.');
|
|
361
|
-
|
|
362
|
-
if (Array.isArray(value)) {
|
|
363
|
-
// Handle arrays as before
|
|
364
|
-
const childIds: string[] = [];
|
|
365
|
-
|
|
366
|
-
value.forEach((item) => {
|
|
367
|
-
const itemId = `id:${ulid()}`;
|
|
368
|
-
childIds.push(nodeKey + '.' + itemId);
|
|
369
|
-
});
|
|
370
466
|
|
|
371
|
-
|
|
467
|
+
initializeShadowState: (key: string, initialState: any) => {
|
|
468
|
+
set((state) => {
|
|
469
|
+
// 1. Make a copy of the current store to modify it
|
|
470
|
+
const newShadowStore = new Map(state.shadowStateStore);
|
|
471
|
+
|
|
472
|
+
// 2. PRESERVE the existing components map before doing anything else
|
|
473
|
+
const existingRootMeta = newShadowStore.get(key);
|
|
474
|
+
const preservedComponents = existingRootMeta?.components;
|
|
475
|
+
|
|
476
|
+
// 3. Wipe all old shadow entries for this state key
|
|
477
|
+
const prefixToDelete = key + '.';
|
|
478
|
+
for (const k of Array.from(newShadowStore.keys())) {
|
|
479
|
+
if (k === key || k.startsWith(prefixToDelete)) {
|
|
480
|
+
newShadowStore.delete(k);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
372
483
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
});
|
|
377
|
-
} else if (isSimpleObject(value)) {
|
|
378
|
-
// Only create field mappings for simple objects
|
|
379
|
-
const fields = Object.fromEntries(
|
|
380
|
-
Object.keys(value).map((k) => [k, nodeKey + '.' + k])
|
|
381
|
-
);
|
|
382
|
-
existingShadowStore.set(nodeKey, { fields });
|
|
484
|
+
// 4. Run your original logic to rebuild the state tree from scratch
|
|
485
|
+
const processValue = (value: any, path: string[]) => {
|
|
486
|
+
const nodeKey = [key, ...path].join('.');
|
|
383
487
|
|
|
384
|
-
|
|
385
|
-
|
|
488
|
+
if (Array.isArray(value)) {
|
|
489
|
+
const childIds: string[] = [];
|
|
490
|
+
value.forEach(() => {
|
|
491
|
+
const itemId = `id:${ulid()}`;
|
|
492
|
+
childIds.push(nodeKey + '.' + itemId);
|
|
493
|
+
});
|
|
494
|
+
newShadowStore.set(nodeKey, { arrayKeys: childIds });
|
|
495
|
+
value.forEach((item, index) => {
|
|
496
|
+
const itemId = childIds[index]!.split('.').pop();
|
|
497
|
+
processValue(item, [...path!, itemId!]);
|
|
498
|
+
});
|
|
499
|
+
} else if (isSimpleObject(value)) {
|
|
500
|
+
const fields = Object.fromEntries(
|
|
501
|
+
Object.keys(value).map((k) => [k, nodeKey + '.' + k])
|
|
502
|
+
);
|
|
503
|
+
newShadowStore.set(nodeKey, { fields });
|
|
504
|
+
Object.keys(value).forEach((k) => {
|
|
505
|
+
processValue(value[k], [...path, k]);
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
newShadowStore.set(nodeKey, { value });
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
processValue(initialState, []);
|
|
512
|
+
|
|
513
|
+
// 5. RESTORE the preserved components map onto the new root metadata
|
|
514
|
+
if (preservedComponents) {
|
|
515
|
+
const newRootMeta = newShadowStore.get(key) || {};
|
|
516
|
+
newShadowStore.set(key, {
|
|
517
|
+
...newRootMeta,
|
|
518
|
+
components: preservedComponents,
|
|
386
519
|
});
|
|
387
|
-
} else {
|
|
388
|
-
// Treat everything else (including Uint8Array) as primitive values
|
|
389
|
-
existingShadowStore.set(nodeKey, { value });
|
|
390
520
|
}
|
|
391
|
-
};
|
|
392
521
|
|
|
393
|
-
|
|
394
|
-
|
|
522
|
+
// 6. Return the completely updated state
|
|
523
|
+
return { shadowStateStore: newShadowStore };
|
|
524
|
+
});
|
|
395
525
|
},
|
|
396
526
|
|
|
397
527
|
getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
|
|
@@ -440,11 +570,41 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
440
570
|
return get().shadowStateStore.get(fullKey);
|
|
441
571
|
},
|
|
442
572
|
|
|
443
|
-
setShadowMetadata: (key
|
|
573
|
+
setShadowMetadata: (key, path, metadata) => {
|
|
444
574
|
const fullKey = [key, ...path].join('.');
|
|
575
|
+
const existingMeta = get().shadowStateStore.get(fullKey);
|
|
576
|
+
|
|
577
|
+
// --- THIS IS THE TRAP ---
|
|
578
|
+
// If the existing metadata HAS a components map, but the NEW metadata DOES NOT,
|
|
579
|
+
// it means we are about to wipe it out. This is the bug.
|
|
580
|
+
if (existingMeta?.components && !metadata.components) {
|
|
581
|
+
console.group(
|
|
582
|
+
'%c🚨 RACE CONDITION DETECTED! 🚨',
|
|
583
|
+
'color: red; font-size: 18px; font-weight: bold;'
|
|
584
|
+
);
|
|
585
|
+
console.error(
|
|
586
|
+
`An overwrite is about to happen on stateKey: "${key}" at path: [${path.join(', ')}]`
|
|
587
|
+
);
|
|
588
|
+
console.log(
|
|
589
|
+
'The EXISTING metadata had a components map:',
|
|
590
|
+
existingMeta.components
|
|
591
|
+
);
|
|
592
|
+
console.log(
|
|
593
|
+
'The NEW metadata is trying to save WITHOUT a components map:',
|
|
594
|
+
metadata
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
'%cStack trace to the function that caused this overwrite:',
|
|
598
|
+
'font-weight: bold;'
|
|
599
|
+
);
|
|
600
|
+
console.trace(); // This prints the call stack, leading you to the bad code.
|
|
601
|
+
console.groupEnd();
|
|
602
|
+
}
|
|
603
|
+
// --- END OF TRAP ---
|
|
604
|
+
|
|
445
605
|
const newShadowStore = new Map(get().shadowStateStore);
|
|
446
|
-
const
|
|
447
|
-
newShadowStore.set(fullKey,
|
|
606
|
+
const finalMeta = { ...(existingMeta || {}), ...metadata };
|
|
607
|
+
newShadowStore.set(fullKey, finalMeta);
|
|
448
608
|
set({ shadowStateStore: newShadowStore });
|
|
449
609
|
},
|
|
450
610
|
setTransformCache: (
|
|
@@ -648,6 +808,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
648
808
|
clearSelectedIndex: ({ arrayKey }: { arrayKey: string }): void => {
|
|
649
809
|
set((state) => {
|
|
650
810
|
const newMap = state.selectedIndicesMap;
|
|
811
|
+
const acutalKey = newMap.get(arrayKey);
|
|
812
|
+
if (acutalKey) {
|
|
813
|
+
get().notifyPathSubscribers(acutalKey, {
|
|
814
|
+
type: 'CLEAR_SELECTION',
|
|
815
|
+
});
|
|
816
|
+
}
|
|
651
817
|
|
|
652
818
|
newMap.delete(arrayKey);
|
|
653
819
|
get().notifyPathSubscribers(arrayKey, {
|