@ryupold/vode 1.2.0 → 1.3.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.
@@ -0,0 +1,228 @@
1
+ import { AnimatedPatch, DeepPartial, PatchableState, RenderPatch } from "./vode.js";
2
+
3
+ /**
4
+ * Generates dot-notation path strings for all nested properties in an object type.
5
+ *
6
+ * @example
7
+ * type User = { profile: { settings: { theme: string } } };
8
+ * type Paths = KeyPath<User>; // "profile" | "profile.settings" | "profile.settings.theme"
9
+ */
10
+ export type KeyPath<ObjectType extends object> =
11
+ { [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
12
+ ? `${Key}` | `${Key}.${KeyPath<ObjectType[Key]>}`
13
+ : `${Key}`
14
+ }[keyof ObjectType & (string | number)];
15
+
16
+ /**
17
+ * Extracts the value type at a given dot-notation path in an object type.
18
+ *
19
+ * @example
20
+ * type User = { profile: { settings: { theme: string } } };
21
+ * type Theme = PathValue<User, "profile.settings.theme">; // string
22
+ */
23
+ export type PathValue<T, P extends string> =
24
+ P extends `${infer Key}.${infer Rest}`
25
+ ? Key extends keyof T ? PathValue<T[Key], Rest> : never
26
+ : P extends keyof T ? T[P] : never;
27
+
28
+ /**
29
+ * Maps valid paths in an object type to paths that resolve to a specific substate type.
30
+ * Used for type-safe path constraints in StateContext.
31
+ * Ensures exact type matching (not just compatibility).
32
+ *
33
+ * @example
34
+ * type User = { profile: { name: string, settings: { theme: string } } };
35
+ * type SettingsPaths = KeyToSubState<User, { theme: string }>; // "profile.settings"
36
+ * type InvalidPath = KeyToSubState<User, { theme: string }, "profile">; // never (type mismatch)
37
+ */
38
+ export type KeyToSubState<S extends object, Sub, K = KeyPath<S>> =
39
+ K extends KeyPath<S> ? [PathValue<S, K>] extends [Sub] ? [Sub] extends [PathValue<S, K>] ? K
40
+ : never : never : never;
41
+
42
+ /**
43
+ * State context for type-safe access and manipulation of nested state paths
44
+ * while still be able to access the parent state.
45
+ */
46
+ export interface StateContext<S extends PatchableState, SubState> extends SubStateContext<SubState> {
47
+ /**
48
+ * parent state
49
+ * @see PatchableState<S>
50
+ */
51
+ get state(): S;
52
+ }
53
+
54
+ /**
55
+ * State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state.
56
+ */
57
+ export interface SubStateContext<SubState> {
58
+ /**
59
+ * Reads the current value of the substate if it exists.
60
+ *
61
+ * @returns The current value, or undefined if the path doesn't exist
62
+ */
63
+ get(): SubState | undefined;
64
+
65
+ /**
66
+ * Updates the nested sub-state value WITHOUT triggering a render.
67
+ * This performs a silent mutation of the parent state object.
68
+ *
69
+ * @param {DeepPartial<SubState>} value - The new value or partial update to apply
70
+ */
71
+ put(value: SubState | DeepPartial<SubState> | undefined | null): void;
72
+
73
+ /**
74
+ * Updates the nested sub-state value AND triggers a render.
75
+ * This is the recommended way to update nested state in most cases.
76
+ *
77
+ * @param value - The new value or partial update to apply
78
+ */
79
+ patch(value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null): void;
80
+ }
81
+
82
+ /**
83
+ * Provides type-safe access to deeply nested state with path-based operations.
84
+ *
85
+ * **When to use:**
86
+ * - State is deeply nested
87
+ * - Multiple components access the same nested path
88
+ * - You need type safety for nested updates
89
+ *
90
+ * **When to avoid:**
91
+ * - Shallow state structures for main state
92
+ * - State structure changes frequently
93
+ * - Learning vode for the first time (start simpler)
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const state = createState({
98
+ * user: {
99
+ * profile: {
100
+ * settings: { theme: 'dark', lang: 'en' }
101
+ * }
102
+ * }
103
+ * });
104
+ * app(element, state, (s) => [DIV]);
105
+ *
106
+ * // Create a context for the nested settings
107
+ * const settingsCtx = new StateContext(state, 'user.profile.settings');
108
+ *
109
+ * // Read current value
110
+ * const settings = settingsCtx.get(); // { theme: 'dark', lang: 'en' }
111
+ *
112
+ * // Update and trigger render
113
+ * settingsCtx.patch({ theme: 'light' });
114
+ *
115
+ * // Update without render (silent mutation)
116
+ * settingsCtx.put({ lang: 'de' });
117
+ * state.patch({}); // trigger render manually later
118
+ * ```
119
+ *
120
+ * @template S - The root state type (must extend PatchableState)
121
+ * @template SubState - The type of the nested state being accessed
122
+ */
123
+ export class KeyStateContext<S extends PatchableState, SubState>
124
+ implements StateContext<S, SubState> {
125
+ private readonly keys: string[];
126
+
127
+ constructor(
128
+ public readonly state: S,
129
+ public readonly path: KeyToSubState<S, SubState>
130
+ ) {
131
+ this.keys = path.split('.');
132
+ }
133
+
134
+ get(): SubState | undefined {
135
+ const keys = this.keys;
136
+ let raw = this.state ? (<any>this.state)[keys[0]] : undefined;
137
+ for (let i = 1; i < keys.length && !!raw; i++) {
138
+ raw = raw[keys[i]];
139
+ }
140
+ return raw;
141
+ }
142
+
143
+ put(value: SubState | DeepPartial<SubState> | undefined | null) {
144
+ this.putDeep(value, this.state);
145
+ }
146
+
147
+ patch(value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) {
148
+ if (Array.isArray(value)) {
149
+ const animation: AnimatedPatch<S> = [];
150
+ for(const v of value){
151
+ animation.push(this.createPatch(v));
152
+ }
153
+ this.state.patch(animation);
154
+ }
155
+ this.state.patch(this.createPatch(value as DeepPartial<SubState>));
156
+ }
157
+
158
+ /**
159
+ * Creates a render-patch for the parent state by setting a nested sub-state value while creating necessary structure.
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const ctx = new StateContext(state, 'user.profile.settings');
164
+ * const patch = ctx.createPatch({ theme: 'light' });
165
+ * // patch is { user: { profile: { settings: { theme: 'light' } } } }
166
+ * ```
167
+ *
168
+ * @param value
169
+ * @returns {{key-path}:{...: value}} render-patch for the parent state
170
+ */
171
+ createPatch(value: SubState | DeepPartial<SubState> | undefined | null): RenderPatch<S> {
172
+ const renderPatch: DeepPartial<S> = {};
173
+ this.putDeep(value, renderPatch);
174
+ return renderPatch;
175
+ }
176
+
177
+ private putDeep(value: SubState | DeepPartial<SubState> | undefined | null, target: S | DeepPartial<S>) {
178
+ const keys = this.keys;
179
+ if (keys.length > 1) {
180
+ let i = 0;
181
+ let raw = (<any>target)[keys[i]];
182
+ if (typeof raw !== "object" || raw === null) {
183
+ (<any>target)[keys[i]] = raw = {};
184
+ }
185
+ for (i = 1; i < keys.length - 1; i++) {
186
+ const p = raw;
187
+ raw = raw[keys[i]];
188
+ if (typeof raw !== "object" || raw === null) {
189
+ p[keys[i]] = raw = {};
190
+ }
191
+ }
192
+ raw[keys[i]] = value;
193
+ } else {
194
+ if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object")
195
+ Object.assign((<any>target)[keys[0]], value);
196
+ else
197
+ (<any>target)[keys[0]] = value;
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Provides type-safe access to sub-state with fetch & store delegate functions.
204
+ * Implementer is responsible for reading/writing the sub-state correctly.
205
+ *
206
+ * **When to use:**
207
+ * - State structure is dynamic or complex
208
+ * - You need custom logic for accessing nested state
209
+ * - You want to encapsulate access logic outside of parent state
210
+ *
211
+ * **When to avoid:**
212
+ * - Simple, static state structures
213
+ * - You want automatic path-based access (use KeyStateContext instead)
214
+ * - Learning vode for the first time (start simpler)
215
+ */
216
+ export class DelegateStateContext<S extends PatchableState, SubState>
217
+ implements StateContext<S, SubState> {
218
+ constructor(
219
+ public readonly state: S,
220
+
221
+ public readonly get: () => SubState | undefined,
222
+
223
+ public readonly put: (value: SubState | DeepPartial<SubState> | undefined | null) => void,
224
+
225
+ public readonly patch: (value: SubState | DeepPartial<SubState> | Array<DeepPartial<SubState>> | undefined | null) => void,
226
+ ) {
227
+ }
228
+ }