fractostate 1.0.1 → 1.0.2
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/cjs/index.js +2 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/src/index.js +66 -0
- package/dist/cjs/src/index.js.map +1 -0
- package/dist/cjs/src/proxy.js +289 -0
- package/dist/cjs/src/proxy.js.map +1 -0
- package/dist/cjs/src/store.js +373 -0
- package/dist/cjs/src/store.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/src/index.js +63 -0
- package/dist/esm/src/index.js.map +1 -0
- package/dist/esm/src/proxy.js +286 -0
- package/dist/esm/src/proxy.js.map +1 -0
- package/dist/esm/src/store.js +371 -0
- package/dist/esm/src/store.js.map +1 -0
- package/dist/index.d.ts +124 -0
- package/package.json +48 -8
- package/docs/api-reference.md +0 -46
- package/docs/architecture.md +0 -27
- package/docs/getting-started.md +0 -94
- package/src/index.ts +0 -82
- package/src/proxy.ts +0 -313
- package/src/store.ts +0 -324
- package/src/types.ts +0 -126
- package/test/README.md +0 -73
- package/test/eslint.config.js +0 -23
- package/test/index.html +0 -13
- package/test/package.json +0 -47
- package/test/postcss.config.mjs +0 -7
- package/test/public/vite.svg +0 -1
- package/test/src/App.css +0 -42
- package/test/src/App.tsx +0 -44
- package/test/src/assets/react.svg +0 -1
- package/test/src/components/CartDrawer.tsx +0 -79
- package/test/src/components/Navbar.tsx +0 -48
- package/test/src/components/Notifications.tsx +0 -27
- package/test/src/components/ProductList.tsx +0 -56
- package/test/src/flows.ts +0 -7
- package/test/src/index.css +0 -33
- package/test/src/layout/Layout.tsx +0 -68
- package/test/src/layout/ProtectedRoute.tsx +0 -19
- package/test/src/main.tsx +0 -10
- package/test/src/pages/LoginPage.tsx +0 -86
- package/test/src/pages/ProfilePage.tsx +0 -48
- package/test/src/pages/ShopPage.tsx +0 -54
- package/test/src/store/auth.ts +0 -39
- package/test/src/store/flows.ts +0 -74
- package/test/tsconfig.app.json +0 -31
- package/test/tsconfig.json +0 -7
- package/test/tsconfig.node.json +0 -29
- package/test/vite.config.ts +0 -16
package/docs/architecture.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Architecture: Secure Memory Vault
|
|
2
|
-
|
|
3
|
-
FractoState operates on a paradigm shift in state management. Instead of relying on a hierarchical data flow injected through component trees, it utilizes a decentralized, side-car architecture.
|
|
4
|
-
|
|
5
|
-
## The Memory Vault
|
|
6
|
-
|
|
7
|
-
At the core of FractoState is the SecureVault. This is a pure JavaScript singleton that exists independently of the React lifecycle. It holds the application state in a protected memory space.
|
|
8
|
-
|
|
9
|
-
### Security and Isolation
|
|
10
|
-
|
|
11
|
-
Data within the Vault is stored in an obfuscated format. This prevents simple memory inspection and ensures that user data is not readily available in plain text within the browser's RAM. Access is strictly controlled through the FractoState internal engine, returning copies of the data to components to prevent accidental direct mutations.
|
|
12
|
-
|
|
13
|
-
## Proxy-Based Mutation Engine
|
|
14
|
-
|
|
15
|
-
FractoState leverages JavaScript Proxies to provide a surgical update mechanism. When you access `ops.self`, you are interacting with a recursive proxy that maps your state structure to specific atomic operations.
|
|
16
|
-
|
|
17
|
-
### Type-Aware Operations
|
|
18
|
-
|
|
19
|
-
The proxy identifies the data type at any given path (Number, String, Array, or Object) and presents relevant, type-safe methods. This allows for complex state updates—like pushing to a nested array or merging into a deep object—without the need for manual immutability boilerplate.
|
|
20
|
-
|
|
21
|
-
## Decoupled React Integration
|
|
22
|
-
|
|
23
|
-
The `useFlow` hook acts as a high-performance bridge. It subscribes components to specific segments of the Vault.
|
|
24
|
-
|
|
25
|
-
- **Selective Re-rendering**: Components are only notified when the specific key they are watching is updated.
|
|
26
|
-
- **Provider-less Execution**: Since the store is external, there is no need for context providers. This eliminates the "Wrapper Hell" and ensures that any component, at any depth, can access state with zero performance penalty.
|
|
27
|
-
- **Microtask Batching**: Multiple state changes within the same execution cycle are batched into a single microtask, ensuring optimal UI performance and preventing unnecessary render cycles.
|
package/docs/getting-started.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Getting Started with FractoState
|
|
2
|
-
|
|
3
|
-
This guide will walk you through the integration of FractoState into a React application.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
Ensure you have the library within your project's source directory (currently in development).
|
|
8
|
-
|
|
9
|
-
## Core Concepts
|
|
10
|
-
|
|
11
|
-
FractoState is built around "Flows". A Flow is a reactive stream of data identified by a unique key.
|
|
12
|
-
|
|
13
|
-
### 1. Defining a Flow
|
|
14
|
-
|
|
15
|
-
Centralize your state structure and initial values using `defineFlow`. This promotes reusability and type safety.
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
// store/flows.ts
|
|
19
|
-
import { defineFlow } from "fractostate";
|
|
20
|
-
|
|
21
|
-
export const UserFlow = defineFlow("user", {
|
|
22
|
-
name: "Default User",
|
|
23
|
-
settings: { theme: "dark" },
|
|
24
|
-
});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 2. Using a Flow in a Component
|
|
28
|
-
|
|
29
|
-
Use the `useFlow` hook to connect any component to the state.
|
|
30
|
-
|
|
31
|
-
```tsx
|
|
32
|
-
import { useFlow } from "fractostate";
|
|
33
|
-
import { UserFlow } from "./store/flows";
|
|
34
|
-
|
|
35
|
-
export default function Profile() {
|
|
36
|
-
const [state, { ops }] = useFlow(UserFlow);
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div>
|
|
40
|
-
<h1>User: {state.name}</h1>
|
|
41
|
-
<button onClick={() => ops.self.name.set("New Name")}>Update Name</button>
|
|
42
|
-
</div>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 3. Sharing State Across Components
|
|
48
|
-
|
|
49
|
-
To consume or modify the same state in another component, simply use the same `UserFlow` definition. No providers or context required.
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
import { useFlow } from "fractostate";
|
|
53
|
-
import { UserFlow } from "./store/flows";
|
|
54
|
-
|
|
55
|
-
export default function Sidebar() {
|
|
56
|
-
const [state] = useFlow(UserFlow); // Automatically synchronizes with the Profile component
|
|
57
|
-
return <aside>Active theme: {state.settings.theme}</aside>;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### 4. Advanced Configuration
|
|
62
|
-
|
|
63
|
-
You can enable features like Time Travel or Middleware directly in the definition or the hook.
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
// Enable Undo/Redo and middleware for logging
|
|
67
|
-
export const CartFlow = defineFlow(
|
|
68
|
-
"cart",
|
|
69
|
-
{ items: [] },
|
|
70
|
-
{
|
|
71
|
-
timeTravel: true,
|
|
72
|
-
middleware: [
|
|
73
|
-
(state) => {
|
|
74
|
-
console.log("Cart updated:", state);
|
|
75
|
-
return state;
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Using Time Travel in a component:
|
|
83
|
-
|
|
84
|
-
```tsx
|
|
85
|
-
function CartControls() {
|
|
86
|
-
const [cart, { undo, redo, canUndo }] = useFlow(CartFlow);
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<button onClick={undo} disabled={!canUndo}>
|
|
90
|
-
Undo Last Item
|
|
91
|
-
</button>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
```
|
package/src/index.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
-
import type { FlowOptions, FlowOperations, FlowDefinition } from "./types";
|
|
3
|
-
import { store } from "./store";
|
|
4
|
-
import { createDeepProxy } from "./proxy";
|
|
5
|
-
|
|
6
|
-
export * from "./types";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Defines a flow in a centralized manner.
|
|
10
|
-
* Allows defining the key, initial state, and options in a single reusable object.
|
|
11
|
-
*/
|
|
12
|
-
export function defineFlow<T>(
|
|
13
|
-
key: string,
|
|
14
|
-
initial: T,
|
|
15
|
-
options?: FlowOptions<T>,
|
|
16
|
-
): FlowDefinition<T> {
|
|
17
|
-
return { key, initial, options };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Unique "All-in-One" hook to create or consume a FractoState flow.
|
|
22
|
-
* - If called with a FlowDefinition or an initial value: initializes the flow if it doesn't exist.
|
|
23
|
-
* - If called with just a key: connects to the existing flow.
|
|
24
|
-
*
|
|
25
|
-
* Returns a 2-element array: [state, toolbox]
|
|
26
|
-
* where toolbox contains { ops, set, undo, redo, history, ... }
|
|
27
|
-
*/
|
|
28
|
-
export function useFlow<T>(
|
|
29
|
-
keyOrDef: string | FlowDefinition<T>,
|
|
30
|
-
maybeInitialValue?: T,
|
|
31
|
-
options: FlowOptions<T> = {},
|
|
32
|
-
): [T, FlowOperations<T>] {
|
|
33
|
-
// Argument analysis to unify initialization and consumption
|
|
34
|
-
const isDef = typeof keyOrDef !== "string";
|
|
35
|
-
const key = isDef ? keyOrDef.key : keyOrDef;
|
|
36
|
-
|
|
37
|
-
// Initial data comes either from the definition or the direct argument
|
|
38
|
-
const initialValue = isDef ? keyOrDef.initial : maybeInitialValue;
|
|
39
|
-
|
|
40
|
-
// Merge options
|
|
41
|
-
const opt = isDef ? { ...keyOrDef.options, ...options } : options;
|
|
42
|
-
|
|
43
|
-
// Store access: store.get handles initialization if initialValue is present
|
|
44
|
-
const [state, setState] = useState(() => store.get(key, initialValue));
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
// Subscribe to store changes
|
|
48
|
-
const unsub = store.subscribe(key, () => {
|
|
49
|
-
setState(store.get(key, initialValue));
|
|
50
|
-
});
|
|
51
|
-
return unsub;
|
|
52
|
-
}, [key, initialValue]);
|
|
53
|
-
|
|
54
|
-
const setManual = useCallback(
|
|
55
|
-
(val: T | ((prev: T) => T)) => {
|
|
56
|
-
const current = store.get(key, initialValue);
|
|
57
|
-
const next = typeof val === "function" ? (val as any)(current) : val;
|
|
58
|
-
store.set(key, next, opt);
|
|
59
|
-
},
|
|
60
|
-
[key, initialValue, opt],
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const toolbox = useMemo((): FlowOperations<T> => {
|
|
64
|
-
return {
|
|
65
|
-
ops: {
|
|
66
|
-
get self() {
|
|
67
|
-
return createDeepProxy<T>(key, [], store.get(key, initialValue), opt);
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
set: setManual,
|
|
71
|
-
undo: () => store.undo(key),
|
|
72
|
-
redo: () => store.redo(key),
|
|
73
|
-
history: store.getHistory(key),
|
|
74
|
-
canUndo: store.getHistory(key).length > 1,
|
|
75
|
-
canRedo: store.getRedoStack(key).length > 0,
|
|
76
|
-
reset: () => store.reset(key),
|
|
77
|
-
cf: opt,
|
|
78
|
-
};
|
|
79
|
-
}, [key, opt, state, initialValue, setManual]);
|
|
80
|
-
|
|
81
|
-
return [state, toolbox];
|
|
82
|
-
}
|
package/src/proxy.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
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
|
-
}
|