fractostate 1.0.0
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/LICENSE +13 -0
- package/README.md +83 -0
- package/docs/api-reference.md +46 -0
- package/docs/architecture.md +27 -0
- package/docs/getting-started.md +94 -0
- package/package.json +37 -0
- package/src/index.ts +82 -0
- package/src/proxy.ts +313 -0
- package/src/store.ts +324 -0
- package/src/types.ts +126 -0
- package/test/README.md +73 -0
- package/test/eslint.config.js +23 -0
- package/test/index.html +13 -0
- package/test/package.json +47 -0
- package/test/postcss.config.mjs +7 -0
- package/test/public/vite.svg +1 -0
- package/test/src/App.css +42 -0
- package/test/src/App.tsx +44 -0
- package/test/src/assets/react.svg +1 -0
- package/test/src/components/CartDrawer.tsx +79 -0
- package/test/src/components/Navbar.tsx +48 -0
- package/test/src/components/Notifications.tsx +27 -0
- package/test/src/components/ProductList.tsx +56 -0
- package/test/src/flows.ts +7 -0
- package/test/src/index.css +33 -0
- package/test/src/layout/Layout.tsx +68 -0
- package/test/src/layout/ProtectedRoute.tsx +19 -0
- package/test/src/main.tsx +10 -0
- package/test/src/pages/LoginPage.tsx +86 -0
- package/test/src/pages/ProfilePage.tsx +48 -0
- package/test/src/pages/ShopPage.tsx +54 -0
- package/test/src/store/auth.ts +39 -0
- package/test/src/store/flows.ts +74 -0
- package/test/tsconfig.app.json +31 -0
- package/test/tsconfig.json +7 -0
- package/test/tsconfig.node.json +29 -0
- package/test/vite.config.ts +16 -0
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { FlowOptions, TypeAwareOps } from "./types";
|
|
2
|
+
import { store } from "./store";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a deep proxy that provides type-specific atomic operations for a flow.
|
|
6
|
+
* The proxy tracks its path within the state tree and maps access to specific update logic.
|
|
7
|
+
*/
|
|
8
|
+
export function createDeepProxy<T = any>(
|
|
9
|
+
key: string,
|
|
10
|
+
path: string[],
|
|
11
|
+
currentVal: any,
|
|
12
|
+
options: FlowOptions<any>,
|
|
13
|
+
): TypeAwareOps<T> {
|
|
14
|
+
return new Proxy(() => {}, {
|
|
15
|
+
get(_target: any, prop: any) {
|
|
16
|
+
if (typeof prop === "symbol") return undefined;
|
|
17
|
+
|
|
18
|
+
const newPath = [...path, prop];
|
|
19
|
+
|
|
20
|
+
// --- Meta-Operations ---
|
|
21
|
+
|
|
22
|
+
// Generic property replacement
|
|
23
|
+
if (prop === "set") {
|
|
24
|
+
return (val: any) => {
|
|
25
|
+
try {
|
|
26
|
+
const currentState = store.get(key, undefined);
|
|
27
|
+
const newState = setInPath(currentState, path, val);
|
|
28
|
+
store.set(key, newState, options);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(
|
|
31
|
+
`[FlowProxy] Error setting value at path ${path.join(".")}:`,
|
|
32
|
+
error,
|
|
33
|
+
);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Type-Specific Atomic Operations ---
|
|
40
|
+
|
|
41
|
+
// Number Operations
|
|
42
|
+
if (typeof currentVal === "number") {
|
|
43
|
+
if (prop === "increment")
|
|
44
|
+
return (amount = 1) => {
|
|
45
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
46
|
+
console.warn(`[FlowProxy] Invalid increment amount: ${amount}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
update(currentVal + amount);
|
|
50
|
+
};
|
|
51
|
+
if (prop === "decrement")
|
|
52
|
+
return (amount = 1) => {
|
|
53
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
54
|
+
console.warn(`[FlowProxy] Invalid decrement amount: ${amount}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
update(currentVal - amount);
|
|
58
|
+
};
|
|
59
|
+
if (prop === "add")
|
|
60
|
+
return (amount: number) => {
|
|
61
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
62
|
+
console.warn(`[FlowProxy] Invalid add amount: ${amount}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
update(currentVal + amount);
|
|
66
|
+
};
|
|
67
|
+
if (prop === "subtract")
|
|
68
|
+
return (amount: number) => {
|
|
69
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
70
|
+
console.warn(`[FlowProxy] Invalid subtract amount: ${amount}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
update(currentVal - amount);
|
|
74
|
+
};
|
|
75
|
+
if (prop === "multiply")
|
|
76
|
+
return (amount: number) => {
|
|
77
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
78
|
+
console.warn(`[FlowProxy] Invalid multiply amount: ${amount}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
update(currentVal * amount);
|
|
82
|
+
};
|
|
83
|
+
if (prop === "divide")
|
|
84
|
+
return (amount: number) => {
|
|
85
|
+
if (typeof amount !== "number" || !isFinite(amount)) {
|
|
86
|
+
console.warn(`[FlowProxy] Invalid divide amount: ${amount}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (amount === 0) {
|
|
90
|
+
console.error(`[FlowProxy] Cannot divide by zero`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
update(currentVal / amount);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Array Operations
|
|
98
|
+
if (Array.isArray(currentVal)) {
|
|
99
|
+
if (prop === "push")
|
|
100
|
+
return (item: any) => update([...currentVal, item]);
|
|
101
|
+
if (prop === "pop")
|
|
102
|
+
return () => {
|
|
103
|
+
if (currentVal.length === 0) {
|
|
104
|
+
console.warn(`[FlowProxy] Cannot pop from empty array`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
update(currentVal.slice(0, -1));
|
|
108
|
+
};
|
|
109
|
+
if (prop === "shift")
|
|
110
|
+
return () => {
|
|
111
|
+
if (currentVal.length === 0) {
|
|
112
|
+
console.warn(`[FlowProxy] Cannot shift from empty array`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
update(currentVal.slice(1));
|
|
116
|
+
};
|
|
117
|
+
if (prop === "unshift")
|
|
118
|
+
return (item: any) => update([item, ...currentVal]);
|
|
119
|
+
if (prop === "filter")
|
|
120
|
+
return (fn: any) => {
|
|
121
|
+
if (typeof fn !== "function") {
|
|
122
|
+
console.error(`[FlowProxy] Filter requires a function`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
update(currentVal.filter(fn));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`[FlowProxy] Filter function threw error:`, error);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
if (prop === "map")
|
|
132
|
+
return (fn: any) => {
|
|
133
|
+
if (typeof fn !== "function") {
|
|
134
|
+
console.error(`[FlowProxy] Map requires a function`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
update(currentVal.map(fn));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`[FlowProxy] Map function threw error:`, error);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
if (prop === "splice")
|
|
144
|
+
return (...args: any[]) => {
|
|
145
|
+
try {
|
|
146
|
+
const next = [...currentVal];
|
|
147
|
+
const start = args[0] ?? 0;
|
|
148
|
+
const deleteCount = args[1] ?? 0;
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
typeof start !== "number" ||
|
|
152
|
+
typeof deleteCount !== "number"
|
|
153
|
+
) {
|
|
154
|
+
console.error(`[FlowProxy] Splice requires numeric arguments`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
next.splice(start, deleteCount, ...args.slice(2));
|
|
159
|
+
update(next);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(`[FlowProxy] Splice error:`, error);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
if (prop === "removeAt")
|
|
165
|
+
return (index: number) => {
|
|
166
|
+
if (
|
|
167
|
+
typeof index !== "number" ||
|
|
168
|
+
index < 0 ||
|
|
169
|
+
index >= currentVal.length
|
|
170
|
+
) {
|
|
171
|
+
console.warn(`[FlowProxy] Invalid index: ${index}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
update(currentVal.filter((_, i) => i !== index));
|
|
175
|
+
};
|
|
176
|
+
if (prop === "insertAt")
|
|
177
|
+
return (index: number, item: any) => {
|
|
178
|
+
if (
|
|
179
|
+
typeof index !== "number" ||
|
|
180
|
+
index < 0 ||
|
|
181
|
+
index > currentVal.length
|
|
182
|
+
) {
|
|
183
|
+
console.warn(`[FlowProxy] Invalid index: ${index}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const next = [...currentVal];
|
|
187
|
+
next.splice(index, 0, item);
|
|
188
|
+
update(next);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// String Operations
|
|
193
|
+
if (typeof currentVal === "string") {
|
|
194
|
+
if (prop === "append")
|
|
195
|
+
return (str: string) => {
|
|
196
|
+
if (typeof str !== "string") {
|
|
197
|
+
console.warn(`[FlowProxy] Append requires a string`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
update(currentVal + str);
|
|
201
|
+
};
|
|
202
|
+
if (prop === "prepend")
|
|
203
|
+
return (str: string) => {
|
|
204
|
+
if (typeof str !== "string") {
|
|
205
|
+
console.warn(`[FlowProxy] Prepend requires a string`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
update(str + currentVal);
|
|
209
|
+
};
|
|
210
|
+
if (prop === "uppercase") return () => update(currentVal.toUpperCase());
|
|
211
|
+
if (prop === "lowercase") return () => update(currentVal.toLowerCase());
|
|
212
|
+
if (prop === "trim") return () => update(currentVal.trim());
|
|
213
|
+
if (prop === "replace")
|
|
214
|
+
return (search: string | RegExp, replace: string) => {
|
|
215
|
+
if (typeof replace !== "string") {
|
|
216
|
+
console.warn(`[FlowProxy] Replace requires a string replacement`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
update(currentVal.replace(search, replace));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(`[FlowProxy] Replace error:`, error);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Object Operations
|
|
228
|
+
if (
|
|
229
|
+
currentVal !== null &&
|
|
230
|
+
typeof currentVal === "object" &&
|
|
231
|
+
!Array.isArray(currentVal)
|
|
232
|
+
) {
|
|
233
|
+
if (prop === "merge")
|
|
234
|
+
return (obj: any) => {
|
|
235
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
236
|
+
console.warn(`[FlowProxy] Merge requires an object`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
update({ ...currentVal, ...obj });
|
|
240
|
+
};
|
|
241
|
+
if (prop === "delete")
|
|
242
|
+
return (keyToDel: string) => {
|
|
243
|
+
if (typeof keyToDel !== "string") {
|
|
244
|
+
console.warn(`[FlowProxy] Delete requires a string key`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!(keyToDel in currentVal)) {
|
|
248
|
+
console.warn(`[FlowProxy] Key "${keyToDel}" does not exist`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const next = { ...currentVal };
|
|
252
|
+
delete next[keyToDel];
|
|
253
|
+
update(next);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Internal helper to commit a value update to the store.
|
|
259
|
+
*/
|
|
260
|
+
function update(val: any) {
|
|
261
|
+
try {
|
|
262
|
+
const currentState = store.get(key, undefined);
|
|
263
|
+
const newState = setInPath(currentState, path, val);
|
|
264
|
+
store.set(key, newState, options);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(
|
|
267
|
+
`[FlowProxy] Error updating value at path ${path.join(".")}:`,
|
|
268
|
+
error,
|
|
269
|
+
);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Recursive step: create a new proxy for the child property
|
|
275
|
+
const nextVal = currentVal ? currentVal[prop] : undefined;
|
|
276
|
+
return createDeepProxy(key, newPath, nextVal, options);
|
|
277
|
+
},
|
|
278
|
+
}) as unknown as TypeAwareOps<T>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Immutable update utility that sets a value at a specific path within an object/array.
|
|
283
|
+
*/
|
|
284
|
+
export function setInPath(obj: any, path: string[], value: any): any {
|
|
285
|
+
if (path.length === 0) return value;
|
|
286
|
+
|
|
287
|
+
const [head, ...tail] = path;
|
|
288
|
+
|
|
289
|
+
if (Array.isArray(obj)) {
|
|
290
|
+
const index = parseInt(head, 10);
|
|
291
|
+
|
|
292
|
+
if (isNaN(index) || index < 0) {
|
|
293
|
+
throw new Error(`[FlowProxy] Invalid array index: ${head}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const newArr = [...obj];
|
|
297
|
+
|
|
298
|
+
if (index >= newArr.length) {
|
|
299
|
+
newArr.length = index + 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
newArr[index] = setInPath(newArr[index], tail, value);
|
|
303
|
+
return newArr;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle nested objects (including null/undefined recovery)
|
|
307
|
+
const currentObj = obj ?? {};
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...currentObj,
|
|
311
|
+
[head]: setInPath(currentObj[head], tail, value),
|
|
312
|
+
};
|
|
313
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { FlowOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fast & Secure Memory Vault
|
|
5
|
+
* Ultra-performant in-memory store with deep cloning and batching
|
|
6
|
+
* NO PERSISTENCE - Pure memory for maximum safety and speed
|
|
7
|
+
*/
|
|
8
|
+
class SecureVault {
|
|
9
|
+
private vault = new Map<string, any>();
|
|
10
|
+
private initialValues = new Map<string, any>();
|
|
11
|
+
private listeners = new Map<string, Set<() => void>>();
|
|
12
|
+
private histories = new Map<string, CircularBuffer<any>>();
|
|
13
|
+
private redoStacks = new Map<string, any[]>();
|
|
14
|
+
private debounceTimers = new Map<string, number>();
|
|
15
|
+
private batchQueue = new Set<string>();
|
|
16
|
+
private batchScheduled = false;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.setupSecureAccess();
|
|
20
|
+
this.setupAutoCleanup();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Protects the vault instance from global window inspection.
|
|
25
|
+
*/
|
|
26
|
+
private setupSecureAccess() {
|
|
27
|
+
if (typeof window !== "undefined") {
|
|
28
|
+
Object.defineProperty(window, "__FRACTO_VAULT__", {
|
|
29
|
+
get: () => "🔒 Access Denied: Secure Memory Instance",
|
|
30
|
+
configurable: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Periodically cleans up orphaned listeners.
|
|
37
|
+
*/
|
|
38
|
+
private setupAutoCleanup() {
|
|
39
|
+
if (typeof setInterval !== "undefined") {
|
|
40
|
+
setInterval(() => {
|
|
41
|
+
this.listeners.forEach((set, key) => {
|
|
42
|
+
if (set.size === 0 && !this.vault.has(key)) {
|
|
43
|
+
this.listeners.delete(key);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}, 300000); // 5 min
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lightweight but ultra-fast obfuscation for memory protection.
|
|
52
|
+
*/
|
|
53
|
+
private obfuscate(data: any): any {
|
|
54
|
+
if (data === undefined) return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const str = JSON.stringify(data);
|
|
57
|
+
// Simple scrambling to prevent plain-text memory inspection
|
|
58
|
+
return { _: btoa(encodeURIComponent(str)), t: Date.now() };
|
|
59
|
+
} catch {
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reverts obfuscation to retrieve original data structure.
|
|
66
|
+
*/
|
|
67
|
+
private deobfuscate(vaultItem: any): any {
|
|
68
|
+
if (!vaultItem || typeof vaultItem !== "object" || !vaultItem._)
|
|
69
|
+
return vaultItem;
|
|
70
|
+
try {
|
|
71
|
+
const decoded = decodeURIComponent(atob(vaultItem._));
|
|
72
|
+
return JSON.parse(decoded);
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Retrieves state from the vault. Initializes if a value is provided and the key is missing.
|
|
80
|
+
*/
|
|
81
|
+
get(key: string, initialValue?: any) {
|
|
82
|
+
if (!this.vault.has(key)) {
|
|
83
|
+
if (initialValue === undefined) return undefined;
|
|
84
|
+
|
|
85
|
+
const cloned = deepClone(initialValue);
|
|
86
|
+
this.vault.set(key, this.obfuscate(cloned));
|
|
87
|
+
this.initialValues.set(key, deepClone(cloned));
|
|
88
|
+
this.histories.set(key, new CircularBuffer(100, [cloned]));
|
|
89
|
+
this.redoStacks.set(key, []);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const val = this.deobfuscate(this.vault.get(key));
|
|
93
|
+
return val === undefined ? initialValue : val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Updates state in the vault with optional debouncing and middleware support.
|
|
98
|
+
*/
|
|
99
|
+
set(key: string, newValue: any, options: FlowOptions = {}) {
|
|
100
|
+
const prevState = this.deobfuscate(this.vault.get(key));
|
|
101
|
+
let stateToSet = deepClone(newValue);
|
|
102
|
+
|
|
103
|
+
// Apply synchronous middleware
|
|
104
|
+
if (options.middleware) {
|
|
105
|
+
for (const fn of options.middleware) {
|
|
106
|
+
stateToSet = fn(stateToSet);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Skip update if state has not changed (Deep equality check)
|
|
111
|
+
if (deepEqual(prevState, stateToSet)) return;
|
|
112
|
+
|
|
113
|
+
if (options.debounce) {
|
|
114
|
+
this.debouncedSet(key, stateToSet, options);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.applySet(key, stateToSet, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Executes a debounced state update.
|
|
123
|
+
*/
|
|
124
|
+
private debouncedSet(key: string, value: any, options: FlowOptions) {
|
|
125
|
+
const existing = this.debounceTimers.get(key);
|
|
126
|
+
if (existing) clearTimeout(existing);
|
|
127
|
+
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
this.applySet(key, value, options);
|
|
130
|
+
this.debounceTimers.delete(key);
|
|
131
|
+
}, options.debounce);
|
|
132
|
+
|
|
133
|
+
this.debounceTimers.set(key, timer as any);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Core logic for applying a state update and managing history.
|
|
138
|
+
*/
|
|
139
|
+
private applySet(key: string, stateToSet: any, options: FlowOptions) {
|
|
140
|
+
this.vault.set(key, this.obfuscate(stateToSet));
|
|
141
|
+
|
|
142
|
+
if (options.timeTravel) {
|
|
143
|
+
const history = this.histories.get(key);
|
|
144
|
+
if (history) history.push(deepClone(stateToSet));
|
|
145
|
+
const redo = this.redoStacks.get(key);
|
|
146
|
+
if (redo) redo.length = 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.scheduleBatchNotify(key);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Schedules a microtask to batch notifications and optimize React renders.
|
|
154
|
+
*/
|
|
155
|
+
private scheduleBatchNotify(key: string) {
|
|
156
|
+
this.batchQueue.add(key);
|
|
157
|
+
if (this.batchScheduled) return;
|
|
158
|
+
this.batchScheduled = true;
|
|
159
|
+
queueMicrotask(() => this.flushBatchNotify());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Flushes and notifies all queued listeners.
|
|
164
|
+
*/
|
|
165
|
+
private flushBatchNotify() {
|
|
166
|
+
const keys = Array.from(this.batchQueue);
|
|
167
|
+
this.batchQueue.clear();
|
|
168
|
+
this.batchScheduled = false;
|
|
169
|
+
keys.forEach((key) => this.notify(key));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Connects a listener to a specific flow key.
|
|
174
|
+
*/
|
|
175
|
+
subscribe(key: string, listener: () => void) {
|
|
176
|
+
if (!this.listeners.has(key)) this.listeners.set(key, new Set());
|
|
177
|
+
this.listeners.get(key)!.add(listener);
|
|
178
|
+
return () => {
|
|
179
|
+
this.listeners.get(key)?.delete(listener);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Directly notifies all listeners of a specific key.
|
|
185
|
+
*/
|
|
186
|
+
private notify(key: string) {
|
|
187
|
+
this.listeners.get(key)?.forEach((l) => l());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reverts the state to the previous version in history.
|
|
192
|
+
*/
|
|
193
|
+
undo(key: string) {
|
|
194
|
+
const history = this.histories.get(key);
|
|
195
|
+
const redo = this.redoStacks.get(key);
|
|
196
|
+
if (history && history.length() > 1) {
|
|
197
|
+
const current = history.pop();
|
|
198
|
+
if (current) redo?.push(current);
|
|
199
|
+
const prev = history.peek();
|
|
200
|
+
this.vault.set(key, this.obfuscate(prev));
|
|
201
|
+
this.notify(key);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Restores the state to the next version in the redo stack.
|
|
207
|
+
*/
|
|
208
|
+
redo(key: string) {
|
|
209
|
+
const history = this.histories.get(key);
|
|
210
|
+
const redo = this.redoStacks.get(key);
|
|
211
|
+
if (redo && redo.length > 0) {
|
|
212
|
+
const next = redo.pop();
|
|
213
|
+
history?.push(next);
|
|
214
|
+
this.vault.set(key, this.obfuscate(next));
|
|
215
|
+
this.notify(key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resets a flow to its initial value.
|
|
221
|
+
*/
|
|
222
|
+
reset(key: string) {
|
|
223
|
+
const initial = this.initialValues.get(key);
|
|
224
|
+
if (initial !== undefined) this.set(key, initial);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getHistory(key: string) {
|
|
228
|
+
return this.histories.get(key)?.toArray() || [];
|
|
229
|
+
}
|
|
230
|
+
getRedoStack(key: string) {
|
|
231
|
+
return this.redoStacks.get(key) || [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clears the entire store and all timers.
|
|
236
|
+
*/
|
|
237
|
+
clearAll() {
|
|
238
|
+
this.debounceTimers.forEach((t) => clearTimeout(t));
|
|
239
|
+
this.vault.clear();
|
|
240
|
+
this.initialValues.clear();
|
|
241
|
+
this.listeners.clear();
|
|
242
|
+
this.histories.clear();
|
|
243
|
+
this.redoStacks.clear();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Circular Buffer implementation for high-performance memory-efficient history.
|
|
249
|
+
*/
|
|
250
|
+
class CircularBuffer<T> {
|
|
251
|
+
private buffer: T[];
|
|
252
|
+
private head = 0;
|
|
253
|
+
private size = 0;
|
|
254
|
+
private maxSize: number;
|
|
255
|
+
|
|
256
|
+
constructor(maxSize: number, initial: T[] = []) {
|
|
257
|
+
this.maxSize = maxSize;
|
|
258
|
+
this.buffer = new Array(maxSize);
|
|
259
|
+
initial.forEach((i) => this.push(i));
|
|
260
|
+
}
|
|
261
|
+
push(item: T) {
|
|
262
|
+
this.buffer[this.head] = item;
|
|
263
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
264
|
+
if (this.size < this.maxSize) this.size++;
|
|
265
|
+
}
|
|
266
|
+
pop(): T | undefined {
|
|
267
|
+
if (this.size <= 0) return undefined;
|
|
268
|
+
this.head = (this.head - 1 + this.maxSize) % this.maxSize;
|
|
269
|
+
this.size--;
|
|
270
|
+
return this.buffer[this.head];
|
|
271
|
+
}
|
|
272
|
+
peek(): T | undefined {
|
|
273
|
+
return this.size > 0
|
|
274
|
+
? this.buffer[(this.head - 1 + this.maxSize) % this.maxSize]
|
|
275
|
+
: undefined;
|
|
276
|
+
}
|
|
277
|
+
length() {
|
|
278
|
+
return this.size;
|
|
279
|
+
}
|
|
280
|
+
toArray(): T[] {
|
|
281
|
+
const res = [];
|
|
282
|
+
for (let i = 0; i < this.size; i++) {
|
|
283
|
+
res.push(
|
|
284
|
+
this.buffer[(this.head - this.size + i + this.maxSize) % this.maxSize],
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return res;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* High-performance deep cloning utility.
|
|
293
|
+
*/
|
|
294
|
+
function deepClone<T>(obj: T): T {
|
|
295
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
296
|
+
if (Array.isArray(obj)) return obj.map(deepClone) as any;
|
|
297
|
+
const cloned: any = {};
|
|
298
|
+
for (const key in obj) {
|
|
299
|
+
if (Object.prototype.hasOwnProperty.call(obj, key))
|
|
300
|
+
cloned[key] = deepClone(obj[key]);
|
|
301
|
+
}
|
|
302
|
+
return cloned;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* High-performance deep equality utility.
|
|
307
|
+
*/
|
|
308
|
+
function deepEqual(a: any, b: any): boolean {
|
|
309
|
+
if (a === b) return true;
|
|
310
|
+
if (typeof a !== typeof b || a === null || b === null) return false;
|
|
311
|
+
if (typeof a === "object") {
|
|
312
|
+
const keysA = Object.keys(a);
|
|
313
|
+
const keysB = Object.keys(b);
|
|
314
|
+
if (keysA.length !== keysB.length) return false;
|
|
315
|
+
for (const key of keysA) {
|
|
316
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
317
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const store = new SecureVault();
|