cogsbox-state 0.5.435 → 0.5.437
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 +38 -25
- package/dist/CogsState.d.ts.map +1 -0
- package/dist/CogsState.jsx +1292 -1181
- package/dist/CogsState.jsx.map +1 -1
- package/dist/CogsStateClient.d.ts +1 -0
- package/dist/CogsStateClient.d.ts.map +1 -0
- package/dist/CogsStateClient.jsx.map +1 -1
- package/dist/Functions.d.ts +2 -5
- package/dist/Functions.d.ts.map +1 -0
- package/dist/Functions.jsx +17 -48
- package/dist/Functions.jsx.map +1 -1
- package/dist/TRPCValidationLink.d.ts +1 -0
- package/dist/TRPCValidationLink.d.ts.map +1 -0
- package/dist/TRPCValidationLink.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -20
- package/dist/index.js.map +1 -1
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +245 -185
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +1 -0
- package/dist/utility.d.ts.map +1 -0
- package/dist/utility.js.map +1 -1
- package/package.json +7 -7
- package/src/CogsState.tsx +500 -179
- package/src/Functions.tsx +8 -241
- package/src/TRPCValidationLink.ts +12 -12
- package/src/index.ts +5 -4
- package/src/store.ts +195 -38
- 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/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
1
|
+
export * from './CogsState.js';
|
|
2
|
+
export * from './CogsStateClient.js';
|
|
3
|
+
export * from './utility.js';
|
|
4
|
+
export * from './TRPCValidationLink.js';
|
|
5
|
+
export * from './store.js';
|
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,6 +305,85 @@ 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;
|
|
@@ -347,54 +456,72 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
347
456
|
},
|
|
348
457
|
|
|
349
458
|
notifyPathSubscribers: (updatedPath, newValue) => {
|
|
350
|
-
// <-- Now accepts newValue
|
|
351
459
|
const subscribers = get().pathSubscribers;
|
|
352
460
|
const subs = subscribers.get(updatedPath);
|
|
353
461
|
|
|
354
462
|
if (subs) {
|
|
355
|
-
// Pass the newValue to every callback
|
|
356
463
|
subs.forEach((callback) => callback(newValue));
|
|
357
464
|
}
|
|
358
465
|
},
|
|
359
|
-
initializeShadowState: (key: string, initialState: any) => {
|
|
360
|
-
const existingShadowStore = new Map(get().shadowStateStore);
|
|
361
466
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
375
483
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
380
|
-
} else if (isSimpleObject(value)) {
|
|
381
|
-
// Only create field mappings for simple objects
|
|
382
|
-
const fields = Object.fromEntries(
|
|
383
|
-
Object.keys(value).map((k) => [k, nodeKey + '.' + k])
|
|
384
|
-
);
|
|
385
|
-
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('.');
|
|
386
487
|
|
|
387
|
-
|
|
388
|
-
|
|
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,
|
|
389
519
|
});
|
|
390
|
-
} else {
|
|
391
|
-
// Treat everything else (including Uint8Array) as primitive values
|
|
392
|
-
existingShadowStore.set(nodeKey, { value });
|
|
393
520
|
}
|
|
394
|
-
};
|
|
395
521
|
|
|
396
|
-
|
|
397
|
-
|
|
522
|
+
// 6. Return the completely updated state
|
|
523
|
+
return { shadowStateStore: newShadowStore };
|
|
524
|
+
});
|
|
398
525
|
},
|
|
399
526
|
|
|
400
527
|
getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
|
|
@@ -443,11 +570,41 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
443
570
|
return get().shadowStateStore.get(fullKey);
|
|
444
571
|
},
|
|
445
572
|
|
|
446
|
-
setShadowMetadata: (key
|
|
573
|
+
setShadowMetadata: (key, path, metadata) => {
|
|
447
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
|
+
|
|
448
605
|
const newShadowStore = new Map(get().shadowStateStore);
|
|
449
|
-
const
|
|
450
|
-
newShadowStore.set(fullKey,
|
|
606
|
+
const finalMeta = { ...(existingMeta || {}), ...metadata };
|
|
607
|
+
newShadowStore.set(fullKey, finalMeta);
|
|
451
608
|
set({ shadowStateStore: newShadowStore });
|
|
452
609
|
},
|
|
453
610
|
setTransformCache: (
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ZodObject, ZodRawShape, ZodTypeAny } from 'zod';
|
|
2
|
-
|
|
3
|
-
export type ResultItem = {
|
|
4
|
-
status: "loading" | "success" | "failure" | "error";
|
|
5
|
-
message?: string;
|
|
6
|
-
};
|
|
7
|
-
type RequestType = Array<{
|
|
8
|
-
path: string[];
|
|
9
|
-
data: any;
|
|
10
|
-
key: string;
|
|
11
|
-
}>;
|
|
12
|
-
type ResultsState = {
|
|
13
|
-
results: Record<string, Record<string, ResultItem>>;
|
|
14
|
-
request: Record<string, RequestType>;
|
|
15
|
-
getResultsByKey: (key: string) => Record<string, ResultItem> | undefined;
|
|
16
|
-
setResults: (key: string) => (result: Record<string, ResultItem> | ((prevState: Record<string, ResultItem>) => Record<string, ResultItem>)) => void;
|
|
17
|
-
setRequest: (key: string) => (request: RequestType | ((prevState: RequestType) => RequestType)) => void;
|
|
18
|
-
getRequestsByKey: (key: string) => Array<{
|
|
19
|
-
path: string[];
|
|
20
|
-
data: any;
|
|
21
|
-
key: string;
|
|
22
|
-
}> | undefined;
|
|
23
|
-
};
|
|
24
|
-
export declare const useResultsStore: import('zustand').UseBoundStore<import('zustand').StoreApi<ResultsState>>;
|
|
25
|
-
export default function useValidateZodPath<T extends ZodRawShape>(validationKey: string, schema: ZodObject<T>, stateKey?: string): {
|
|
26
|
-
validateZodPath: (path: string[], data: any, results?: Record<string, ResultItem> | undefined) => "loading" | "success" | "error" | "failure";
|
|
27
|
-
getZodPathResults: (path: string[]) => string[] | ResultItem;
|
|
28
|
-
zodPathResults: Record<string, ResultItem> | undefined;
|
|
29
|
-
};
|
|
30
|
-
export declare function validateZodPathFunc<T extends ZodRawShape, U>(validationKey: string, schema: ZodTypeAny, path: string[], data: U): Promise<{
|
|
31
|
-
success: boolean;
|
|
32
|
-
message?: string;
|
|
33
|
-
}>;
|
|
34
|
-
export {};
|