@pyreon/state-tree 0.11.5 → 0.11.6

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,5 +1,5 @@
1
- import { computed, effect } from "@pyreon/reactivity"
2
- import type { Patch } from "../index"
1
+ import { computed, effect } from '@pyreon/reactivity'
2
+ import type { Patch } from '../index'
3
3
  import {
4
4
  addMiddleware,
5
5
  applyPatch,
@@ -8,12 +8,12 @@ import {
8
8
  model,
9
9
  onPatch,
10
10
  resetAllHooks,
11
- } from "../index"
11
+ } from '../index'
12
12
 
13
13
  // ─── Fixtures ─────────────────────────────────────────────────────────────────
14
14
 
15
15
  const Profile = model({
16
- state: { name: "", bio: "" },
16
+ state: { name: '', bio: '' },
17
17
  actions: (self) => ({
18
18
  rename: (n: string) => self.name.set(n),
19
19
  setBio: (b: string) => self.bio.set(b),
@@ -21,7 +21,7 @@ const Profile = model({
21
21
  })
22
22
 
23
23
  const App = model({
24
- state: { profile: Profile, title: "My App" },
24
+ state: { profile: Profile, title: 'My App' },
25
25
  actions: (self) => ({
26
26
  setTitle: (t: string) => self.title.set(t),
27
27
  replaceProfile: (p: any) => self.profile.set(p),
@@ -42,8 +42,8 @@ const Counter = model({
42
42
 
43
43
  // ─── 1. Nested model deletion ────────────────────────────────────────────────
44
44
 
45
- describe("nested model deletion", () => {
46
- it("replacing a nested child model updates snapshot correctly", () => {
45
+ describe('nested model deletion', () => {
46
+ it('replacing a nested child model updates snapshot correctly', () => {
47
47
  const Child = model({
48
48
  state: { value: 0 },
49
49
  actions: (self) => ({
@@ -52,7 +52,7 @@ describe("nested model deletion", () => {
52
52
  })
53
53
 
54
54
  const Parent = model({
55
- state: { child: Child, label: "parent" },
55
+ state: { child: Child, label: 'parent' },
56
56
  actions: (self) => ({
57
57
  replaceChild: (c: any) => self.child.set(c),
58
58
  setLabel: (l: string) => self.label.set(l),
@@ -61,12 +61,12 @@ describe("nested model deletion", () => {
61
61
 
62
62
  const parent = Parent.create({
63
63
  child: { value: 10 },
64
- label: "original",
64
+ label: 'original',
65
65
  })
66
66
 
67
67
  expect(getSnapshot(parent)).toEqual({
68
68
  child: { value: 10 },
69
- label: "original",
69
+ label: 'original',
70
70
  })
71
71
 
72
72
  // Replace child with a new instance
@@ -76,11 +76,11 @@ describe("nested model deletion", () => {
76
76
  // Snapshot should reflect the new child
77
77
  expect(getSnapshot(parent)).toEqual({
78
78
  child: { value: 99 },
79
- label: "original",
79
+ label: 'original',
80
80
  })
81
81
  })
82
82
 
83
- it("nested patches stop propagating after child replacement", () => {
83
+ it('nested patches stop propagating after child replacement', () => {
84
84
  const Child = model({
85
85
  state: { x: 0 },
86
86
  actions: (self) => ({
@@ -104,20 +104,20 @@ describe("nested model deletion", () => {
104
104
  // Mutate old child — should propagate
105
105
  oldChild.setX(2)
106
106
  expect(patches).toHaveLength(1)
107
- expect(patches[0]).toEqual({ op: "replace", path: "/child/x", value: 2 })
107
+ expect(patches[0]).toEqual({ op: 'replace', path: '/child/x', value: 2 })
108
108
 
109
109
  // Replace child
110
110
  const newChild = Child.create({ x: 50 })
111
111
  parent.replaceChild(newChild)
112
112
  expect(patches).toHaveLength(2)
113
- expect(patches[1]!.path).toBe("/child")
113
+ expect(patches[1]!.path).toBe('/child')
114
114
  })
115
115
  })
116
116
 
117
117
  // ─── 2. Snapshot edge cases ──────────────────────────────────────────────────
118
118
 
119
- describe("snapshot edge cases", () => {
120
- it("handles null values in state", () => {
119
+ describe('snapshot edge cases', () => {
120
+ it('handles null values in state', () => {
121
121
  const M = model({
122
122
  state: { data: null as string | null },
123
123
  actions: (self) => ({
@@ -128,14 +128,14 @@ describe("snapshot edge cases", () => {
128
128
  const m = M.create()
129
129
  expect(getSnapshot(m)).toEqual({ data: null })
130
130
 
131
- m.setData("hello")
132
- expect(getSnapshot(m)).toEqual({ data: "hello" })
131
+ m.setData('hello')
132
+ expect(getSnapshot(m)).toEqual({ data: 'hello' })
133
133
 
134
134
  m.setData(null)
135
135
  expect(getSnapshot(m)).toEqual({ data: null })
136
136
  })
137
137
 
138
- it("handles empty arrays in state", () => {
138
+ it('handles empty arrays in state', () => {
139
139
  const M = model({
140
140
  state: { items: [] as number[] },
141
141
  actions: (self) => ({
@@ -150,8 +150,8 @@ describe("snapshot edge cases", () => {
150
150
  expect(getSnapshot(m)).toEqual({ items: [1, 2, 3] })
151
151
  })
152
152
 
153
- it("handles Date objects in state", () => {
154
- const now = new Date("2025-01-01T00:00:00Z")
153
+ it('handles Date objects in state', () => {
154
+ const now = new Date('2025-01-01T00:00:00Z')
155
155
  const M = model({
156
156
  state: { createdAt: now },
157
157
  })
@@ -159,23 +159,23 @@ describe("snapshot edge cases", () => {
159
159
  const m = M.create()
160
160
  const snap = getSnapshot(m)
161
161
  expect(snap.createdAt).toBeInstanceOf(Date)
162
- expect((snap.createdAt as Date).toISOString()).toBe("2025-01-01T00:00:00.000Z")
162
+ expect((snap.createdAt as Date).toISOString()).toBe('2025-01-01T00:00:00.000Z')
163
163
  })
164
164
 
165
- it("handles undefined initial values by falling back to defaults", () => {
165
+ it('handles undefined initial values by falling back to defaults', () => {
166
166
  const M = model({
167
- state: { x: 10, y: "hello", z: true },
167
+ state: { x: 10, y: 'hello', z: true },
168
168
  })
169
169
 
170
170
  // Pass undefined for all — should use defaults
171
171
  const m = M.create()
172
- expect(getSnapshot(m)).toEqual({ x: 10, y: "hello", z: true })
172
+ expect(getSnapshot(m)).toEqual({ x: 10, y: 'hello', z: true })
173
173
  })
174
174
 
175
- it("handles complex nested objects in state", () => {
175
+ it('handles complex nested objects in state', () => {
176
176
  const M = model({
177
177
  state: {
178
- config: { theme: "dark", fontSize: 14, plugins: ["a", "b"] },
178
+ config: { theme: 'dark', fontSize: 14, plugins: ['a', 'b'] },
179
179
  },
180
180
  actions: (self) => ({
181
181
  setConfig: (c: any) => self.config.set(c),
@@ -184,22 +184,22 @@ describe("snapshot edge cases", () => {
184
184
 
185
185
  const m = M.create()
186
186
  expect(getSnapshot(m)).toEqual({
187
- config: { theme: "dark", fontSize: 14, plugins: ["a", "b"] },
187
+ config: { theme: 'dark', fontSize: 14, plugins: ['a', 'b'] },
188
188
  })
189
189
 
190
- m.setConfig({ theme: "light", fontSize: 16, plugins: [] })
190
+ m.setConfig({ theme: 'light', fontSize: 16, plugins: [] })
191
191
  expect(getSnapshot(m)).toEqual({
192
- config: { theme: "light", fontSize: 16, plugins: [] },
192
+ config: { theme: 'light', fontSize: 16, plugins: [] },
193
193
  })
194
194
  })
195
195
  })
196
196
 
197
197
  // ─── 3. Patch replay ─────────────────────────────────────────────────────────
198
198
 
199
- describe("patch replay", () => {
200
- it("replaying recorded patches on a fresh instance reproduces final state", () => {
199
+ describe('patch replay', () => {
200
+ it('replaying recorded patches on a fresh instance reproduces final state', () => {
201
201
  const M = model({
202
- state: { a: 0, b: "" },
202
+ state: { a: 0, b: '' },
203
203
  actions: (self) => ({
204
204
  setA: (v: number) => self.a.set(v),
205
205
  setB: (v: string) => self.b.set(v),
@@ -211,9 +211,9 @@ describe("patch replay", () => {
211
211
  onPatch(original, (p) => patches.push({ ...p }))
212
212
 
213
213
  original.setA(1)
214
- original.setB("hello")
214
+ original.setB('hello')
215
215
  original.setA(2)
216
- original.setB("world")
216
+ original.setB('world')
217
217
  original.setA(42)
218
218
 
219
219
  expect(patches).toHaveLength(5)
@@ -224,10 +224,10 @@ describe("patch replay", () => {
224
224
 
225
225
  expect(getSnapshot(replica)).toEqual(getSnapshot(original))
226
226
  expect(replica.a()).toBe(42)
227
- expect(replica.b()).toBe("world")
227
+ expect(replica.b()).toBe('world')
228
228
  })
229
229
 
230
- it("replaying patches preserves intermediate state transitions", () => {
230
+ it('replaying patches preserves intermediate state transitions', () => {
231
231
  const c = Counter.create()
232
232
  const patches: Patch[] = []
233
233
  onPatch(c, (p) => patches.push({ ...p }))
@@ -248,8 +248,8 @@ describe("patch replay", () => {
248
248
 
249
249
  // ─── 4. Patch with nested operations ─────────────────────────────────────────
250
250
 
251
- describe("patch with nested operations", () => {
252
- it("applies replace on deeply nested model property", () => {
251
+ describe('patch with nested operations', () => {
252
+ it('applies replace on deeply nested model property', () => {
253
253
  const Leaf = model({
254
254
  state: { value: 0 },
255
255
  actions: (self) => ({
@@ -258,168 +258,168 @@ describe("patch with nested operations", () => {
258
258
  })
259
259
 
260
260
  const Branch = model({
261
- state: { leaf: Leaf, tag: "" },
261
+ state: { leaf: Leaf, tag: '' },
262
262
  actions: (self) => ({
263
263
  setTag: (t: string) => self.tag.set(t),
264
264
  }),
265
265
  })
266
266
 
267
267
  const Root = model({
268
- state: { branch: Branch, name: "root" },
268
+ state: { branch: Branch, name: 'root' },
269
269
  actions: (self) => ({
270
270
  setName: (n: string) => self.name.set(n),
271
271
  }),
272
272
  })
273
273
 
274
274
  const root = Root.create({
275
- branch: { leaf: { value: 1 }, tag: "a" },
276
- name: "root",
275
+ branch: { leaf: { value: 1 }, tag: 'a' },
276
+ name: 'root',
277
277
  })
278
278
 
279
279
  // Apply patch to deeply nested leaf
280
- applyPatch(root, { op: "replace", path: "/branch/leaf/value", value: 999 })
280
+ applyPatch(root, { op: 'replace', path: '/branch/leaf/value', value: 999 })
281
281
  expect(root.branch().leaf().value()).toBe(999)
282
282
 
283
283
  // Apply patch to intermediate level
284
- applyPatch(root, { op: "replace", path: "/branch/tag", value: "updated" })
285
- expect(root.branch().tag()).toBe("updated")
284
+ applyPatch(root, { op: 'replace', path: '/branch/tag', value: 'updated' })
285
+ expect(root.branch().tag()).toBe('updated')
286
286
 
287
287
  // Apply patch to top level
288
- applyPatch(root, { op: "replace", path: "/name", value: "new-root" })
289
- expect(root.name()).toBe("new-root")
288
+ applyPatch(root, { op: 'replace', path: '/name', value: 'new-root' })
289
+ expect(root.name()).toBe('new-root')
290
290
  })
291
291
 
292
- it("records nested patches with correct paths", () => {
292
+ it('records nested patches with correct paths', () => {
293
293
  const app = App.create({
294
- profile: { name: "Alice", bio: "dev" },
295
- title: "Test",
294
+ profile: { name: 'Alice', bio: 'dev' },
295
+ title: 'Test',
296
296
  })
297
297
 
298
298
  const patches: Patch[] = []
299
299
  onPatch(app, (p) => patches.push({ ...p }))
300
300
 
301
- app.profile().rename("Bob")
302
- app.profile().setBio("engineer")
303
- app.setTitle("New Title")
301
+ app.profile().rename('Bob')
302
+ app.profile().setBio('engineer')
303
+ app.setTitle('New Title')
304
304
 
305
305
  expect(patches).toEqual([
306
- { op: "replace", path: "/profile/name", value: "Bob" },
307
- { op: "replace", path: "/profile/bio", value: "engineer" },
308
- { op: "replace", path: "/title", value: "New Title" },
306
+ { op: 'replace', path: '/profile/name', value: 'Bob' },
307
+ { op: 'replace', path: '/profile/bio', value: 'engineer' },
308
+ { op: 'replace', path: '/title', value: 'New Title' },
309
309
  ])
310
310
  })
311
311
 
312
- it("replays nested patches on fresh instance", () => {
312
+ it('replays nested patches on fresh instance', () => {
313
313
  const app = App.create({
314
- profile: { name: "Alice", bio: "" },
315
- title: "v1",
314
+ profile: { name: 'Alice', bio: '' },
315
+ title: 'v1',
316
316
  })
317
317
 
318
318
  const patches: Patch[] = []
319
319
  onPatch(app, (p) => patches.push({ ...p }))
320
320
 
321
- app.profile().rename("Carol")
322
- app.setTitle("v2")
321
+ app.profile().rename('Carol')
322
+ app.setTitle('v2')
323
323
 
324
324
  const fresh = App.create({
325
- profile: { name: "Alice", bio: "" },
326
- title: "v1",
325
+ profile: { name: 'Alice', bio: '' },
326
+ title: 'v1',
327
327
  })
328
328
  applyPatch(fresh, patches)
329
329
 
330
- expect(fresh.profile().name()).toBe("Carol")
331
- expect(fresh.title()).toBe("v2")
330
+ expect(fresh.profile().name()).toBe('Carol')
331
+ expect(fresh.title()).toBe('v2')
332
332
  })
333
333
  })
334
334
 
335
335
  // ─── 5. Middleware error handling ─────────────────────────────────────────────
336
336
 
337
- describe("middleware error handling", () => {
338
- it("throwing middleware does not corrupt state", () => {
337
+ describe('middleware error handling', () => {
338
+ it('throwing middleware does not corrupt state', () => {
339
339
  const c = Counter.create({ count: 5 })
340
340
 
341
341
  addMiddleware(c, (_call, _next) => {
342
- throw new Error("middleware boom")
342
+ throw new Error('middleware boom')
343
343
  })
344
344
 
345
- expect(() => c.inc()).toThrow("middleware boom")
345
+ expect(() => c.inc()).toThrow('middleware boom')
346
346
  // State should remain unchanged since the action never ran
347
347
  expect(c.count()).toBe(5)
348
348
  })
349
349
 
350
- it("error in middleware after next() does not undo the action", () => {
350
+ it('error in middleware after next() does not undo the action', () => {
351
351
  const c = Counter.create({ count: 0 })
352
352
 
353
353
  addMiddleware(c, (call, next) => {
354
354
  next(call)
355
- throw new Error("post-action error")
355
+ throw new Error('post-action error')
356
356
  })
357
357
 
358
- expect(() => c.inc()).toThrow("post-action error")
358
+ expect(() => c.inc()).toThrow('post-action error')
359
359
  // The action DID run before the error
360
360
  expect(c.count()).toBe(1)
361
361
  })
362
362
 
363
- it("error in one middleware prevents subsequent middlewares from running", () => {
363
+ it('error in one middleware prevents subsequent middlewares from running', () => {
364
364
  const c = Counter.create()
365
365
  const log: string[] = []
366
366
 
367
367
  addMiddleware(c, (_call, _next) => {
368
- log.push("first")
369
- throw new Error("first fails")
368
+ log.push('first')
369
+ throw new Error('first fails')
370
370
  })
371
371
 
372
372
  addMiddleware(c, (call, next) => {
373
- log.push("second")
373
+ log.push('second')
374
374
  return next(call)
375
375
  })
376
376
 
377
- expect(() => c.inc()).toThrow("first fails")
377
+ expect(() => c.inc()).toThrow('first fails')
378
378
  // Only the first middleware ran (Koa-style: first wraps second)
379
- expect(log).toEqual(["first"])
379
+ expect(log).toEqual(['first'])
380
380
  expect(c.count()).toBe(0)
381
381
  })
382
382
  })
383
383
 
384
384
  // ─── 6. Middleware chain order ────────────────────────────────────────────────
385
385
 
386
- describe("middleware chain order", () => {
387
- it("middlewares fire in registration order (Koa-style onion)", () => {
386
+ describe('middleware chain order', () => {
387
+ it('middlewares fire in registration order (Koa-style onion)', () => {
388
388
  const c = Counter.create()
389
389
  const log: string[] = []
390
390
 
391
391
  addMiddleware(c, (call, next) => {
392
- log.push("A:before")
392
+ log.push('A:before')
393
393
  const result = next(call)
394
- log.push("A:after")
394
+ log.push('A:after')
395
395
  return result
396
396
  })
397
397
 
398
398
  addMiddleware(c, (call, next) => {
399
- log.push("B:before")
399
+ log.push('B:before')
400
400
  const result = next(call)
401
- log.push("B:after")
401
+ log.push('B:after')
402
402
  return result
403
403
  })
404
404
 
405
405
  addMiddleware(c, (call, next) => {
406
- log.push("C:before")
406
+ log.push('C:before')
407
407
  const result = next(call)
408
- log.push("C:after")
408
+ log.push('C:after')
409
409
  return result
410
410
  })
411
411
 
412
412
  c.inc()
413
413
 
414
- expect(log).toEqual(["A:before", "B:before", "C:before", "C:after", "B:after", "A:after"])
414
+ expect(log).toEqual(['A:before', 'B:before', 'C:before', 'C:after', 'B:after', 'A:after'])
415
415
  })
416
416
 
417
- it("middleware can modify action args before passing to next", () => {
417
+ it('middleware can modify action args before passing to next', () => {
418
418
  const c = Counter.create()
419
419
 
420
420
  addMiddleware(c, (call, next) => {
421
421
  // Double the argument to add()
422
- if (call.name === "add") {
422
+ if (call.name === 'add') {
423
423
  return next({ ...call, args: [(call.args[0] as number) * 2] })
424
424
  }
425
425
  return next(call)
@@ -429,9 +429,9 @@ describe("middleware chain order", () => {
429
429
  expect(c.count()).toBe(10)
430
430
  })
431
431
 
432
- it("middleware can replace action result", () => {
432
+ it('middleware can replace action result', () => {
433
433
  const M = model({
434
- state: { value: "" },
434
+ state: { value: '' },
435
435
  actions: (self) => ({
436
436
  getValue: () => {
437
437
  return self.value()
@@ -441,28 +441,28 @@ describe("middleware chain order", () => {
441
441
  })
442
442
 
443
443
  const m = M.create()
444
- m.setValue("original")
444
+ m.setValue('original')
445
445
 
446
446
  addMiddleware(m, (call, next) => {
447
447
  const result = next(call)
448
- if (call.name === "getValue") {
448
+ if (call.name === 'getValue') {
449
449
  return `intercepted:${result}`
450
450
  }
451
451
  return result
452
452
  })
453
453
 
454
- expect(m.getValue()).toBe("intercepted:original")
454
+ expect(m.getValue()).toBe('intercepted:original')
455
455
  })
456
456
  })
457
457
 
458
458
  // ─── 7. Hook singleton behavior ──────────────────────────────────────────────
459
459
 
460
- describe("hook singleton behavior", () => {
460
+ describe('hook singleton behavior', () => {
461
461
  afterEach(() => resetAllHooks())
462
462
 
463
- it("asHook returns the same hook function for the same id", () => {
464
- const useCounter1 = Counter.asHook("singleton-test")
465
- const useCounter2 = Counter.asHook("singleton-test")
463
+ it('asHook returns the same hook function for the same id', () => {
464
+ const useCounter1 = Counter.asHook('singleton-test')
465
+ const useCounter2 = Counter.asHook('singleton-test')
466
466
 
467
467
  // The hook functions may be different references but the instance they return is the same
468
468
  const instance1 = useCounter1()
@@ -470,9 +470,9 @@ describe("hook singleton behavior", () => {
470
470
  expect(instance1).toBe(instance2)
471
471
  })
472
472
 
473
- it("mutations via one hook reference are visible via another", () => {
474
- const useA = Counter.asHook("shared-hook")
475
- const useB = Counter.asHook("shared-hook")
473
+ it('mutations via one hook reference are visible via another', () => {
474
+ const useA = Counter.asHook('shared-hook')
475
+ const useB = Counter.asHook('shared-hook')
476
476
 
477
477
  useA().inc()
478
478
  useA().inc()
@@ -481,18 +481,18 @@ describe("hook singleton behavior", () => {
481
481
  expect(useA().doubled()).toBe(4)
482
482
  })
483
483
 
484
- it("different models with same hook id share the same registry entry", () => {
484
+ it('different models with same hook id share the same registry entry', () => {
485
485
  // First model claims the id
486
- const useFirst = Counter.asHook("conflict-id")
486
+ const useFirst = Counter.asHook('conflict-id')
487
487
  const instance = useFirst()
488
488
 
489
489
  // Second model with same id returns the already-created instance (Counter, not Profile)
490
- const useSecond = Profile.asHook("conflict-id")
490
+ const useSecond = Profile.asHook('conflict-id')
491
491
  expect(useSecond()).toBe(instance)
492
492
  })
493
493
 
494
- it("resetAllHooks makes subsequent calls create fresh instances", () => {
495
- const useC = Counter.asHook("fresh-test")
494
+ it('resetAllHooks makes subsequent calls create fresh instances', () => {
495
+ const useC = Counter.asHook('fresh-test')
496
496
  const old = useC()
497
497
  old.add(100)
498
498
 
@@ -506,18 +506,18 @@ describe("hook singleton behavior", () => {
506
506
 
507
507
  // ─── 8. Model with no actions ────────────────────────────────────────────────
508
508
 
509
- describe("model with no actions", () => {
510
- it("creates instance with only state", () => {
509
+ describe('model with no actions', () => {
510
+ it('creates instance with only state', () => {
511
511
  const ReadOnly = model({
512
- state: { x: 10, y: "hello" },
512
+ state: { x: 10, y: 'hello' },
513
513
  })
514
514
 
515
515
  const r = ReadOnly.create()
516
516
  expect(r.x()).toBe(10)
517
- expect(r.y()).toBe("hello")
517
+ expect(r.y()).toBe('hello')
518
518
  })
519
519
 
520
- it("supports views without actions", () => {
520
+ it('supports views without actions', () => {
521
521
  const Derived = model({
522
522
  state: { a: 3, b: 4 },
523
523
  views: (self) => ({
@@ -531,20 +531,20 @@ describe("model with no actions", () => {
531
531
  expect(d.product()).toBe(12)
532
532
  })
533
533
 
534
- it("snapshots work on action-less models", () => {
534
+ it('snapshots work on action-less models', () => {
535
535
  const M = model({ state: { val: 42 } })
536
536
  const m = M.create({ val: 100 })
537
537
  expect(getSnapshot(m)).toEqual({ val: 100 })
538
538
  })
539
539
 
540
- it("applySnapshot works on action-less models", () => {
540
+ it('applySnapshot works on action-less models', () => {
541
541
  const M = model({ state: { val: 0 } })
542
542
  const m = M.create()
543
543
  applySnapshot(m, { val: 999 })
544
544
  expect(m.val()).toBe(999)
545
545
  })
546
546
 
547
- it("onPatch works on action-less models when signals are set directly", () => {
547
+ it('onPatch works on action-less models when signals are set directly', () => {
548
548
  const M = model({ state: { val: 0 } })
549
549
  const m = M.create()
550
550
  const patches: Patch[] = []
@@ -553,10 +553,10 @@ describe("model with no actions", () => {
553
553
  // Set via tracked signal directly
554
554
  m.val.set(5)
555
555
  expect(patches).toHaveLength(1)
556
- expect(patches[0]).toEqual({ op: "replace", path: "/val", value: 5 })
556
+ expect(patches[0]).toEqual({ op: 'replace', path: '/val', value: 5 })
557
557
  })
558
558
 
559
- it("middleware can be added to action-less models (no-op since no actions)", () => {
559
+ it('middleware can be added to action-less models (no-op since no actions)', () => {
560
560
  const M = model({ state: { val: 0 } })
561
561
  const m = M.create()
562
562
  const log: string[] = []
@@ -577,32 +577,32 @@ describe("model with no actions", () => {
577
577
 
578
578
  // ─── 9. applySnapshot with partial data ──────────────────────────────────────
579
579
 
580
- describe("applySnapshot with partial data", () => {
581
- it("only updates specified fields, keeps others unchanged", () => {
580
+ describe('applySnapshot with partial data', () => {
581
+ it('only updates specified fields, keeps others unchanged', () => {
582
582
  const M = model({
583
- state: { name: "default", age: 0, active: false },
583
+ state: { name: 'default', age: 0, active: false },
584
584
  actions: (self) => ({
585
585
  setName: (n: string) => self.name.set(n),
586
586
  setAge: (a: number) => self.age.set(a),
587
587
  }),
588
588
  })
589
589
 
590
- const m = M.create({ name: "Alice", age: 30, active: true })
590
+ const m = M.create({ name: 'Alice', age: 30, active: true })
591
591
 
592
592
  // Only update name
593
- applySnapshot(m, { name: "Bob" })
594
- expect(m.name()).toBe("Bob")
593
+ applySnapshot(m, { name: 'Bob' })
594
+ expect(m.name()).toBe('Bob')
595
595
  expect(m.age()).toBe(30)
596
596
  expect(m.active()).toBe(true)
597
597
 
598
598
  // Only update age
599
599
  applySnapshot(m, { age: 25 })
600
- expect(m.name()).toBe("Bob")
600
+ expect(m.name()).toBe('Bob')
601
601
  expect(m.age()).toBe(25)
602
602
  expect(m.active()).toBe(true)
603
603
  })
604
604
 
605
- it("empty snapshot changes nothing", () => {
605
+ it('empty snapshot changes nothing', () => {
606
606
  const M = model({ state: { x: 1, y: 2 } })
607
607
  const m = M.create()
608
608
 
@@ -611,20 +611,20 @@ describe("applySnapshot with partial data", () => {
611
611
  expect(m.y()).toBe(2)
612
612
  })
613
613
 
614
- it("partial nested snapshot updates only specified nested fields", () => {
614
+ it('partial nested snapshot updates only specified nested fields', () => {
615
615
  const app = App.create({
616
- profile: { name: "Alice", bio: "dev" },
617
- title: "Old",
616
+ profile: { name: 'Alice', bio: 'dev' },
617
+ title: 'Old',
618
618
  })
619
619
 
620
620
  // Update only the nested profile name, leave bio unchanged
621
- applySnapshot(app, { profile: { name: "Bob" } } as any)
622
- expect(app.profile().name()).toBe("Bob")
623
- expect(app.profile().bio()).toBe("dev")
624
- expect(app.title()).toBe("Old")
621
+ applySnapshot(app, { profile: { name: 'Bob' } } as any)
622
+ expect(app.profile().name()).toBe('Bob')
623
+ expect(app.profile().bio()).toBe('dev')
624
+ expect(app.title()).toBe('Old')
625
625
  })
626
626
 
627
- it("applySnapshot batches updates — effect fires once", () => {
627
+ it('applySnapshot batches updates — effect fires once', () => {
628
628
  const M = model({ state: { a: 0, b: 0, c: 0 } })
629
629
  const m = M.create()
630
630
 
@@ -644,8 +644,8 @@ describe("applySnapshot with partial data", () => {
644
644
 
645
645
  // ─── 10. onPatch listener cleanup ────────────────────────────────────────────
646
646
 
647
- describe("onPatch listener cleanup", () => {
648
- it("unsubscribe prevents further callbacks", () => {
647
+ describe('onPatch listener cleanup', () => {
648
+ it('unsubscribe prevents further callbacks', () => {
649
649
  const c = Counter.create()
650
650
  const patches: Patch[] = []
651
651
  const unsub = onPatch(c, (p) => patches.push(p))
@@ -660,7 +660,7 @@ describe("onPatch listener cleanup", () => {
660
660
  expect(patches).toHaveLength(1) // No new patches after unsub
661
661
  })
662
662
 
663
- it("multiple listeners can be independently unsubscribed", () => {
663
+ it('multiple listeners can be independently unsubscribed', () => {
664
664
  const c = Counter.create()
665
665
  const logA: Patch[] = []
666
666
  const logB: Patch[] = []
@@ -685,7 +685,7 @@ describe("onPatch listener cleanup", () => {
685
685
  expect(logB).toHaveLength(2)
686
686
  })
687
687
 
688
- it("double unsubscribe is safe (no-op)", () => {
688
+ it('double unsubscribe is safe (no-op)', () => {
689
689
  const c = Counter.create()
690
690
  const unsub = onPatch(c, () => {})
691
691
 
@@ -693,7 +693,7 @@ describe("onPatch listener cleanup", () => {
693
693
  expect(() => unsub()).not.toThrow()
694
694
  })
695
695
 
696
- it("listener added after unsub receives only new patches", () => {
696
+ it('listener added after unsub receives only new patches', () => {
697
697
  const c = Counter.create()
698
698
  const log1: Patch[] = []
699
699
  const log2: Patch[] = []