@ryupold/vode 1.8.12 → 1.9.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.
@@ -1,16 +1,10 @@
1
- import { AnimatedPatch, DeepPartial, Patchable, PatchableState, RenderPatch } from "./vode";
1
+ import { DeepPartial, Patchable, PatchableState, RenderPatch } from "./vode";
2
2
 
3
3
  /**
4
4
  * State context for type-safe access and manipulation of nested state paths
5
5
  * while still be able to access the parent state.
6
6
  */
7
- export interface StateContext<S extends Patchable<S>, SubState> extends SubContext<SubState> {
8
- /**
9
- * parent state
10
- * @see PatchableState<S>
11
- */
12
- get state(): S;
13
- }
7
+ export interface StateContext<S extends Patchable<S>, SubState> extends SubContext<SubState> { }
14
8
 
15
9
  /**
16
10
  * State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state.
@@ -52,43 +46,56 @@ export type ProxySubContext<SubState> = SubContext<SubState> & {
52
46
  : SubContext<SubState[K]>
53
47
  };
54
48
 
49
+ type ProxyState<SubState> = SubState & {
50
+ [K in keyof SubState]-?: NonNullable<SubState[K]> extends object | null
51
+ ? ProxyState<NonNullable<SubState[K]>>
52
+ : SubState[K]
53
+ };
54
+
55
55
  /**
56
- * create a ProxyStateContext for type-safe dynamic access to nested state
57
- *
58
- * @example
56
+ * Creates a `ProxyStateContext` for type-safe access and manipulation of nested state.
57
+ *
58
+ * There are two ways to reach a subcontext:
59
+ *
60
+ * **1. Property chaining** — traverse the proxy directly via property access:
59
61
  * ```typescript
60
- * const state = createState({
61
- * user: {
62
- * profile: {
63
- * settings: { theme: 'dark', lang: 'en' }
64
- * }
65
- * });
66
- *
67
- * // Create a proxy context for the state
68
62
  * const ctx = context(state).user.profile.settings;
69
- *
70
- * // Access nested state dynamically
71
- * const settings = ctx.get(); // { theme: 'dark', lang: 'en' }
72
- *
73
- * // Update and trigger render
74
- * ctx.patch({ theme: 'light' });
75
- *
76
- * // Update without render (silent mutation)
77
- * ctx.put({ lang: 'de' });
78
63
  * ```
79
- *
80
- * @param state
81
- * @returns
64
+ *
65
+ * **2. Path producer function** — pass a callback that navigates
66
+ * the state tree; needed if your intermediate path contains 'get', 'put' or 'patch' properties that would conflict with the context API:
67
+ * ```typescript
68
+ * const ctx = context(state, s => s.user.profile.settings);
69
+ * ```
70
+ *
71
+ * Both forms return a `ProxyStateContext` that supports the same operations:
72
+ * ```typescript
73
+ * ctx.get(); // read current value
74
+ * ctx.patch({ theme: 'light' }); // update and trigger render
75
+ * ctx.put({ lang: 'de' }); // update without render (silent mutation)
76
+ * ```
77
+ *
78
+ * @param state - The root `PatchableState` to create a context for
79
+ * @param producePath - Optional path producer; receives a proxy of the state and should return the desired sub-node
80
+ * @returns A `ProxyStateContext` rooted at the given path, with further property-chain access available
82
81
  */
83
- export function context<S extends PatchableState>(state: S): ProxyStateContext<S, S> {
84
- return new ProxyStateContextImpl<S, S>(state, []) as unknown as ProxyStateContext<S, S>;
82
+
83
+ export function context<S extends PatchableState, SS = S>(state: S): ProxyStateContext<S, SS>;
84
+ export function context<S extends PatchableState, SS>(state: S, producePath: (ctx: ProxyState<S>) => ProxyState<SS>): ProxyStateContext<S, SS>;
85
+ export function context<S extends PatchableState, SS = S>(state: S, producePath?: (ctx: ProxyState<S>) => ProxyState<SS>): ProxyStateContext<S, SS> {
86
+ if (producePath) {
87
+ const proxy = producePath(proxyState<S>(state, [] as string[]));
88
+ const keys = (proxy as any)["___KeYs___"] as string[];
89
+ return new ProxyStateContextImpl<S, SS>(state, keys) as unknown as ProxyStateContext<S, SS>;
90
+ }
91
+ return new ProxyStateContextImpl<S, S>(state, []) as unknown as ProxyStateContext<S, SS>;
85
92
  }
86
93
 
87
94
  class ProxyStateContextImpl<S extends PatchableState, SubState>
88
95
  implements StateContext<S, SubState> {
89
96
 
90
97
  constructor(
91
- public readonly state: S,
98
+ private readonly state: S,
92
99
  private readonly keys: string[]
93
100
  ) {
94
101
  function putDeep(value: SubState | DeepPartial<SubState> | undefined | null, target: S | DeepPartial<S>) {
@@ -150,8 +157,6 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
150
157
 
151
158
  return new Proxy(this, {
152
159
  get: (target, prop, receiver) => {
153
- if (prop === 'state')
154
- return state;
155
160
 
156
161
  if (prop === 'get')
157
162
  return get;
@@ -162,10 +167,12 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
162
167
  if (prop === 'patch')
163
168
  return patch;
164
169
 
165
-
166
170
  // otherwise return a new ProxyStateContext for nested access
167
- const newKeys = [...target.keys, String(prop)];
171
+ const newKeys = [...keys, String(prop)];
168
172
  return new ProxyStateContextImpl<S, any>(target.state, newKeys);
173
+ },
174
+ set: (target: this, p: string | symbol, newValue: any, receiver: any) => {
175
+ throw new Error("ProxyStateContext is not meant to be directly mutated. Use put() or patch() methods on the StateContext instead");
169
176
  }
170
177
  });
171
178
  }
@@ -174,3 +181,23 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
174
181
  put(value: SubState | DeepPartial<SubState>): void { }
175
182
  patch(value: SubState | DeepPartial<SubState> | DeepPartial<SubState>[]): void { }
176
183
  }
184
+
185
+
186
+ function proxyState<S extends PatchableState>(
187
+ state: S,
188
+ keys: string[]
189
+ ) {
190
+ return new Proxy(state, {
191
+ get: (target, prop, receiver) => {
192
+ if (prop === "___KeYs___") {
193
+ return keys;
194
+ }
195
+
196
+ const newKeys = [...keys, String(prop)];
197
+ return proxyState<S>(state, newKeys);
198
+ },
199
+ set: (target: any, p: string | symbol, newValue: any, receiver: any) => {
200
+ throw new Error("ProxyState is not meant to be directly mutated");
201
+ }
202
+ });
203
+ }
package/src/vode.ts CHANGED
@@ -228,10 +228,10 @@ export function app<S extends PatchableState = PatchableState>(
228
228
 
229
229
  function renderDom(isAnimated: boolean) {
230
230
  const sw = performance.now();
231
- const vom = dom(_vode.state);
232
- _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
231
+ _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, dom(_vode.state))!;
233
232
 
234
- if ((<ContainerNode<S>>container).tagName.toUpperCase() !== (vom[0] as Tag).toUpperCase()) { //the tag name was changed during render -> update reference to vode-app-root
233
+ if ((<ContainerNode<S>>container).tagName.toLowerCase() !== (<Vode<S>>_vode.vode)[0].toLowerCase()) {
234
+ //the tag name was changed during render -> update reference to vode-app-root
235
235
  container = _vode.vode.node as Element;
236
236
  (<ContainerNode<S>>container)._vode = _vode
237
237
  }
@@ -299,6 +299,13 @@ export function app<S extends PatchableState = PatchableState>(
299
299
  hydrate<S>(container, true) as AttachedVode<S>,
300
300
  dom(<S>state)
301
301
  )!;
302
+
303
+ // if during initial render the tag of the root vode was changed (catch or different Tag)
304
+ if (container.tagName.toLowerCase() !== (<Vode<S>>_vode.vode)[0].toLowerCase()) {
305
+ container = _vode.vode.node as Element;
306
+ (container as ContainerNode<S>)._vode = _vode;
307
+ }
308
+
302
309
  const continueRendering = _vode.stats.syncRenderPatchCount !== patchCountBefore;
303
310
  _vode.isRendering = 0;
304
311
  _vode.stats.syncRenderCount++;
package/test/tests-app.ts CHANGED
@@ -120,8 +120,7 @@ export default {
120
120
 
121
121
  await state.patch([{ a: 10 }, { b: 20 }]);
122
122
 
123
- await expect(state.a).toEqual(10);
124
- await expect(state.b).toEqual(20);
123
+ await expect(state).toEqual({ a: 10, b: 20 });
125
124
  },
126
125
 
127
126
  "app(): multiple sequential patches both apply": async () => {
@@ -1,5 +1,5 @@
1
1
  import { expect } from "./helper";
2
- import { app, createState, DIV, ARTICLE, SECTION, P } from "../index";
2
+ import { app, createState, DIV, ARTICLE, SECTION, P, MAIN } from "../index";
3
3
 
4
4
  function setup() {
5
5
  const root = document.createElement("div");
@@ -157,4 +157,64 @@ export default {
157
157
  ]
158
158
  );
159
159
  },
160
+
161
+ "catch: bubbles up to the root component if deeply nested vodes dont catch it earlier": async () => {
162
+ const root = document.createElement("div");
163
+ const container = document.createElement("div");
164
+ root.appendChild(container);
165
+
166
+ app(container, {}, () =>
167
+ [DIV,
168
+ {
169
+ catch: (s: unknown, err: Error) => [DIV, `caught: ${err.message}`]
170
+ },
171
+
172
+ [MAIN,
173
+ [SECTION,
174
+ [ARTICLE, {
175
+ onMount: () => {
176
+ throw new Error("boom");
177
+ }
178
+ }],
179
+ ],
180
+ ]
181
+ ]
182
+ );
183
+
184
+ await expect(container).toMatch(
185
+ [DIV, "caught: boom"],
186
+ );
187
+ },
188
+
189
+ "catch: if catching in root vode with different Tag -> container will be replaced": async () => {
190
+ const root = document.createElement("div");
191
+ const container = document.createElement("div");
192
+ root.appendChild(container);
193
+
194
+ await expect(root.firstChild === container).toEqual(true);
195
+
196
+ app(container, {}, () =>
197
+ [DIV,
198
+ {
199
+ catch: (s: unknown, err: Error) => [P, `caught: ${err.message}`]
200
+ },
201
+
202
+ [MAIN,
203
+ [SECTION,
204
+ [ARTICLE, {
205
+ onMount: () => {
206
+ throw new Error("boom");
207
+ }
208
+ }],
209
+ ],
210
+ ]
211
+ ]
212
+ );
213
+
214
+ await expect(root.firstChild === container).toEqual(false);
215
+
216
+ await expect(root.firstChild).toMatch(
217
+ [P, "caught: boom"],
218
+ );
219
+ },
160
220
  };
@@ -11,8 +11,8 @@ function setup() {
11
11
  export default {
12
12
  "patch(): generator function yields multiple state updates": async () => {
13
13
  const container = setup();
14
- const state: any = createState({ count: 0 });
15
- app(container, state, (s: any) => [DIV, String(s.count)]);
14
+ const state = createState({ count: 0 });
15
+ app<typeof state>(container, state, (s) => [DIV, String(s.count)]);
16
16
 
17
17
  await expect(state.count).toEqual(0);
18
18
 
@@ -30,8 +30,8 @@ export default {
30
30
 
31
31
  "patch(): async generator yields over time": async () => {
32
32
  const container = setup();
33
- const state: any = createState({ phase: "start", value: 0 });
34
- app(container, state, (s: any) => [DIV, s.phase, String(s.value)]);
33
+ const state = createState({ phase: "start", value: 0 });
34
+ app<typeof state>(container, state, (s) => [DIV, s.phase, String(s.value)]);
35
35
 
36
36
  await expect(state.phase).toEqual("start");
37
37
 
@@ -53,38 +53,37 @@ export default {
53
53
 
54
54
  "patch(): Promise resolves and applies patch": async () => {
55
55
  const container = setup();
56
- const state: any = createState({ msg: "before" });
57
- app(container, state, (s: any) => [DIV, s.msg]);
58
-
59
- state.patch(Promise.resolve({ msg: "after" }));
56
+ const state = createState({ msg: "before" });
57
+ app<typeof state>(container, state, (s) => [DIV, s.msg]);
60
58
 
61
- await delay(10);
59
+ await state.patch(Promise.resolve({ msg: "after" }));
62
60
 
63
- await expect(state.msg).toEqual("after");
61
+ await expect(state).toEqual({ msg: "after" });
64
62
  await expect(container).toMatch([DIV, "after"]);
65
63
  },
66
64
 
67
65
  "patch(): array with empty patches applies nothing": async () => {
68
66
  const container = setup();
69
- const state: any = createState({ x: 1, y: 2 });
70
- app(container, state, (s: any) => [DIV]);
67
+ const state = createState({ x: 1, y: 2 });
68
+ app(container, state, (s) => [DIV]);
69
+
70
+ await state.patch([{}, {}]);
71
71
 
72
- state.patch([{}, {}]);
73
- await expect(state.x).toEqual(1);
74
- await expect(state.y).toEqual(2);
72
+ await delay(10);
73
+ await expect(state).toEqual({ x: 1, y: 2 });
75
74
  },
76
75
 
77
76
  "patch(): array with null/undefined items skips them": async () => {
78
77
  const container = setup();
79
- const state: any = createState({ x: 0, y: 0 });
80
- app(container, state, (s: any) => [DIV, String(s.x), String(s.y)]);
78
+ const state = createState({ x: 0, y: 0 });
79
+ app<typeof state>(container, state, (s) => [DIV, String(s.x), String(s.y)]);
81
80
 
82
81
  state.patch([null, { x: 10 }, undefined, { y: 20 }]);
83
82
 
84
- await delay(10);
85
-
86
- await expect(state.x).toEqual(10);
87
- await expect(state.y).toEqual(20);
83
+ await expect(() => expect(state.x).toEqual(10))
84
+ .toSucceedAsync();
85
+ await expect(() => expect(state.y).toEqual(20))
86
+ .toSucceedAsync();
88
87
  },
89
88
 
90
89
  "patch(): returns Promise for generator functions, can be awaited": async () => {
@@ -1,17 +1,9 @@
1
1
  import { expect } from "./helper";
2
- import { context, ProxySubContext } from "../src/state-context";
2
+ import { context } from "../src/state-context";
3
3
  import { createState } from "../src/vode";
4
4
 
5
5
  export default {
6
- "StateContext.state: returns the state reference": async () => {
7
- const state = createState({ x: 10 });
8
- const ctx = context(state);
9
-
10
- await expect((ctx).state === state)
11
- .toEqual(true);
12
- },
13
-
14
- "StateContext.get() returns whole state": async () => {
6
+ "context(s)...get(): returns whole state": async () => {
15
7
  const state = createState({ a: 1, b: 2 });
16
8
  const ctx = context(state);
17
9
 
@@ -20,7 +12,7 @@ export default {
20
12
  },
21
13
 
22
14
 
23
- "StateContext.get(): deep nested": async () => {
15
+ "context(s)...get(): deep nested": async () => {
24
16
  const state = createState({ a: { b: { c: 42 } } });
25
17
  const ctx = context(state);
26
18
 
@@ -28,7 +20,7 @@ export default {
28
20
  .toEqual(42);
29
21
  },
30
22
 
31
- "StateContext.get(): missing nested path returns undefined": async () => {
23
+ "context(s)...get(): missing nested path returns undefined": async () => {
32
24
  const state = createState({ a: {} });
33
25
  const ctx = context(state);
34
26
 
@@ -36,7 +28,7 @@ export default {
36
28
  .toEqual(undefined);
37
29
  },
38
30
 
39
- "StateContext.put(): silently mutates state": async () => {
31
+ "context(s)...put(): silently mutates state": async () => {
40
32
  const state = createState({ a: { b: 1 } });
41
33
  const ctx = context(state);
42
34
  ctx.a.b.put(2);
@@ -45,7 +37,7 @@ export default {
45
37
  .toEqual(2);
46
38
  },
47
39
 
48
- "StateContext.put() on nested object replaces the sub-object": async () => {
40
+ "context(s)...put(): on nested object replaces the sub-object": async () => {
49
41
  const state = createState({ a: { b: { x: 1, y: 2 } } });
50
42
  const ctx = context(state);
51
43
  ctx.a.b.put({ y: 99 });
@@ -54,7 +46,7 @@ export default {
54
46
  .toEqual({ y: 99 });
55
47
  },
56
48
 
57
- "StateContext.put() at root level with empty keys": async () => {
49
+ "context(s)...put(): at root level with empty keys": async () => {
58
50
  const state = createState({ a: 1, b: 2 });
59
51
  const ctx = context(state);
60
52
  ctx.put({ b: undefined });
@@ -63,7 +55,7 @@ export default {
63
55
  .toEqual({ a: 1 });
64
56
  },
65
57
 
66
- "StateContext.patch(): calls state.patch with proper deep partial": async () => {
58
+ "context(s)...patch(): calls state.patch with proper deep partial": async () => {
67
59
  const state = createState({ a: { b: 1 } });
68
60
  const ctx = context(state);
69
61
  ctx.a.b.patch(2);
@@ -76,7 +68,7 @@ export default {
76
68
  .toEqual({ a: { b: 2 } });
77
69
  },
78
70
 
79
- "StateContext.patch(): async wraps in array": async () => {
71
+ "context(s)...patch(): async wraps in array": async () => {
80
72
  const state = createState({ a: { b: 1 } });
81
73
  const ctx = context(state);
82
74
  ctx.a.b.patch(2, true);
@@ -91,7 +83,7 @@ export default {
91
83
  .toEqual({ a: { b: 2 } });
92
84
  },
93
85
 
94
- "StateContext.patch() on nested deep path three levels": async () => {
86
+ "context(s)...patch(): on nested deep path three levels": async () => {
95
87
  const state = createState({ x: { y: { z: 0 } } });
96
88
  const ctx = context(state);
97
89
  ctx.x.y.z.patch(100);
@@ -104,7 +96,7 @@ export default {
104
96
  .toEqual({ x: { y: { z: 100 } } });
105
97
  },
106
98
 
107
- "StateContext.put() with intermediate null creates objects along the path": async () => {
99
+ "context(s)...put(): with intermediate null creates objects along the path": async () => {
108
100
  const state = createState({ a: null as { b: number } | null });
109
101
  const ctx = context(state);
110
102
 
@@ -116,7 +108,7 @@ export default {
116
108
  await expect(state.a?.b).toEqual(undefined);
117
109
  },
118
110
 
119
- "StateContext.put() with three-level intermediate null": async () => {
111
+ "context(s)...put(): with three-level intermediate null": async () => {
120
112
  const state = createState({ a: null as { b: { c: number } } | null });
121
113
  const ctx = context(state);
122
114
 
@@ -125,7 +117,7 @@ export default {
125
117
  await expect(state.a?.b.c).toEqual(99);
126
118
  },
127
119
 
128
- "StateContext.put() with multiple intermediate nulls": async () => {
120
+ "context(s)...put(): with multiple intermediate nulls": async () => {
129
121
  const state = createState({ a: { x: null as { z: string } | null, y: 1 } });
130
122
  const ctx = context(state);
131
123
 
@@ -135,11 +127,171 @@ export default {
135
127
  await expect(state.a.y).toEqual(1);
136
128
  },
137
129
 
138
- "StateContext.put() merges into existing object properties via Object.assign": async () => {
130
+ "context(s)...put(): merges into existing object properties via Object.assign": async () => {
139
131
  const state = createState({ items: { count: 0, name: "test", hidden: false } });
140
132
  const ctx = context(state);
141
- // Line 110-111: when existing value is object and new value is object, Object.assign merges
142
133
  ctx.items.put({ count: 5 });
143
134
  await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
144
135
  },
136
+
137
+ "context(state, s => s).get(): returns whole state": async () => {
138
+ const state = createState({ a: 1, b: 2 });
139
+ const ctx = context(state, s => s);
140
+
141
+ await expect(ctx.get())
142
+ .toEqual({ a: 1, b: 2 });
143
+ },
144
+
145
+ "context(state, s => s.a.b.c).get(): deep nested": async () => {
146
+ const state = createState({ a: { b: { c: 42 } } });
147
+ const ctx = context(state, s => s.a.b.c);
148
+
149
+ await expect(ctx.get())
150
+ .toEqual(42);
151
+ },
152
+
153
+ "context(state, s => s.a.b).get(): missing nested path returns undefined": async () => {
154
+ const state = createState({ a: {} });
155
+ const ctx = context(state, s => (s.a as any).b);
156
+
157
+ await expect(ctx.get())
158
+ .toEqual(undefined);
159
+ },
160
+
161
+ "context(state, s => s.a.b).put(): silently mutates state": async () => {
162
+ const state = createState({ a: { b: 1 } });
163
+ const ctx = context(state, s => s.a.b);
164
+ ctx.put(2);
165
+
166
+ await expect(state.a.b)
167
+ .toEqual(2);
168
+ },
169
+
170
+ "context(state, s => s.a.b).put(): on nested object replaces the sub-object": async () => {
171
+ const state = createState({ a: { b: { x: 1, y: 2 } } });
172
+ const ctx = context(state, s => s.a.b);
173
+ ctx.put({ y: 99 });
174
+
175
+ await expect(state.a.b)
176
+ .toEqual({ y: 99 });
177
+ },
178
+
179
+ "context(state, s => s).put(): at root level with empty keys": async () => {
180
+ const state = createState({ a: 1, b: 2 });
181
+ const ctx = context(state, s => s);
182
+ ctx.put({ b: undefined });
183
+
184
+ await expect(state)
185
+ .toEqual({ a: 1 });
186
+ },
187
+
188
+ "context(state, s => s.a.b).patch(): calls state.patch with proper deep partial": async () => {
189
+ const state = createState({ a: { b: 1 } });
190
+ const ctx = context(state, s => s.a.b);
191
+ ctx.patch(2);
192
+
193
+ const patches = (state as any).patch.initialPatches;
194
+
195
+ await expect(patches.length)
196
+ .toEqual(1);
197
+ await expect(patches[0])
198
+ .toEqual({ a: { b: 2 } });
199
+ },
200
+
201
+ "context(state, s => s.a.b).patch(): async wraps in array": async () => {
202
+ const state = createState({ a: { b: 1 } });
203
+ const ctx = context(state, s => s.a.b);
204
+ ctx.patch(2, true);
205
+
206
+ const patches = (state as any).patch.initialPatches;
207
+
208
+ await expect(patches.length)
209
+ .toEqual(1);
210
+ await expect(Array.isArray(patches[0]))
211
+ .toEqual(true);
212
+ await expect(patches[0][0])
213
+ .toEqual({ a: { b: 2 } });
214
+ },
215
+
216
+ "context(state, s => s.x.y.z).patch(): on nested deep path three levels": async () => {
217
+ const state = createState({ x: { y: { z: 0 } } });
218
+ const ctx = context(state, s => s.x.y.z);
219
+ ctx.patch(100);
220
+
221
+ const patches = (state as any).patch.initialPatches;
222
+
223
+ await expect(patches.length)
224
+ .toEqual(1);
225
+ await expect(patches[0])
226
+ .toEqual({ x: { y: { z: 100 } } });
227
+ },
228
+
229
+ "context(state, s => s.x).y.z: continue proxy sub-state targeting": async () => {
230
+ const state = createState({ x: { y: { z: 0 } } });
231
+ const ctx = context(state, s => s.x).y.z;
232
+ ctx.patch(100);
233
+
234
+ const patches = (state as any).patch.initialPatches;
235
+
236
+ await expect(patches.length)
237
+ .toEqual(1);
238
+ await expect(patches[0])
239
+ .toEqual({ x: { y: { z: 100 } } });
240
+ },
241
+
242
+ "context(state, s => s.a.b).put(): with intermediate null creates objects": async () => {
243
+ const state = createState({ a: null as { b: number } | null });
244
+ const ctx = context(state, s => s.a.b);
245
+
246
+ ctx.put(42);
247
+ await expect(state.a?.b).toEqual(42);
248
+ },
249
+
250
+ "context(state, s => s.a.b.c).put(): with three-level intermediate null": async () => {
251
+ const state = createState({ a: null as { b: { c: number } } | null });
252
+ const ctx = context(state, s => s.a.b.c);
253
+
254
+ ctx.put(99);
255
+
256
+ await expect(state.a?.b.c).toEqual(99);
257
+ },
258
+
259
+ "context(state, s => s.a.x.z).put(): with multiple intermediate nulls": async () => {
260
+ const state = createState({ a: { x: null as { z: string } | null, y: 1 } });
261
+ const ctx = context(state, s => s.a.x.z);
262
+
263
+ ctx.put("deep");
264
+
265
+ await expect(state.a.x?.z).toEqual("deep");
266
+ await expect(state.a.y).toEqual(1);
267
+ },
268
+
269
+ "context(state, s => s.items).put(): merges into existing object properties via Object.assign": async () => {
270
+ const state = createState({ items: { count: 0, name: "test", hidden: false } });
271
+ const ctx = context(state, s => s.items);
272
+ ctx.put({ count: 5 });
273
+ await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
274
+ },
275
+
276
+ "context(state, s => s.get|put|patch...): 'get','put','patch' as intermediate properties without conflict": async () => {
277
+ const state = createState({
278
+ endpoints: {
279
+ get: { count: 1 },
280
+ put: { count: 2 },
281
+ patch: { count: 3 }
282
+ }
283
+ });
284
+
285
+ const getCtx = context(state, s => s.endpoints.get);
286
+ await expect(getCtx.get()).toEqual({ count: 1 });
287
+
288
+ const putCtx = context(state, s => s.endpoints.put);
289
+ putCtx.put({ count: 99 });
290
+ await expect(state.endpoints.put).toEqual({ count: 99 });
291
+
292
+ const patchCtx = context(state, s => s.endpoints.patch);
293
+ patchCtx.patch({ count: 42 });
294
+ const patches = (state as any).patch.initialPatches;
295
+ await expect(patches[0]).toEqual({ endpoints: { patch: { count: 42 } } });
296
+ },
145
297
  };