@pyreon/state-tree 0.11.5 → 0.11.7

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,
@@ -9,8 +9,8 @@ import {
9
9
  onPatch,
10
10
  resetAllHooks,
11
11
  resetHook,
12
- } from "../index"
13
- import { instanceMeta } from "../registry"
12
+ } from '../index'
13
+ import { instanceMeta } from '../registry'
14
14
 
15
15
  // ─── Fixtures ─────────────────────────────────────────────────────────────────
16
16
 
@@ -29,7 +29,7 @@ const Counter = model({
29
29
  })
30
30
 
31
31
  const Profile = model({
32
- state: { name: "", bio: "" },
32
+ state: { name: '', bio: '' },
33
33
  actions: (self) => ({
34
34
  rename: (n: string) => self.name.set(n),
35
35
  setBio: (b: string) => self.bio.set(b),
@@ -37,7 +37,7 @@ const Profile = model({
37
37
  })
38
38
 
39
39
  const App = model({
40
- state: { profile: Profile, title: "My App" },
40
+ state: { profile: Profile, title: 'My App' },
41
41
  actions: (self) => ({
42
42
  setTitle: (t: string) => self.title.set(t),
43
43
  }),
@@ -45,54 +45,54 @@ const App = model({
45
45
 
46
46
  // ─── State signals ─────────────────────────────────────────────────────────────
47
47
 
48
- describe("state signals", () => {
49
- it("create() returns instance with callable signals", () => {
48
+ describe('state signals', () => {
49
+ it('create() returns instance with callable signals', () => {
50
50
  const c = Counter.create()
51
- expect(typeof c.count).toBe("function")
51
+ expect(typeof c.count).toBe('function')
52
52
  expect(c.count()).toBe(0)
53
53
  })
54
54
 
55
- it("uses defaults when no initial value supplied", () => {
55
+ it('uses defaults when no initial value supplied', () => {
56
56
  const c = Counter.create()
57
57
  expect(c.count()).toBe(0)
58
58
  })
59
59
 
60
- it("overrides defaults with supplied initial values", () => {
60
+ it('overrides defaults with supplied initial values', () => {
61
61
  const c = Counter.create({ count: 42 })
62
62
  expect(c.count()).toBe(42)
63
63
  })
64
64
 
65
- it("partial initial — unspecified keys use defaults", () => {
66
- const NamedCounter = model({ state: { count: 0, label: "default" } })
65
+ it('partial initial — unspecified keys use defaults', () => {
66
+ const NamedCounter = model({ state: { count: 0, label: 'default' } })
67
67
  const c = NamedCounter.create({ count: 10 })
68
68
  expect(c.count()).toBe(10)
69
- expect(c.label()).toBe("default")
69
+ expect(c.label()).toBe('default')
70
70
  })
71
71
  })
72
72
 
73
73
  // ─── Actions ──────────────────────────────────────────────────────────────────
74
74
 
75
- describe("actions", () => {
76
- it("actions update state signals", () => {
75
+ describe('actions', () => {
76
+ it('actions update state signals', () => {
77
77
  const c = Counter.create()
78
78
  c.inc()
79
79
  expect(c.count()).toBe(1)
80
80
  })
81
81
 
82
- it("actions with arguments work correctly", () => {
82
+ it('actions with arguments work correctly', () => {
83
83
  const c = Counter.create()
84
84
  c.add(5)
85
85
  expect(c.count()).toBe(5)
86
86
  })
87
87
 
88
- it("self closure allows reading current signal values", () => {
88
+ it('self closure allows reading current signal values', () => {
89
89
  const c = Counter.create({ count: 3 })
90
90
  c.inc()
91
91
  c.inc()
92
92
  expect(c.count()).toBe(5)
93
93
  })
94
94
 
95
- it("actions can call other actions via self (Proxy)", () => {
95
+ it('actions can call other actions via self (Proxy)', () => {
96
96
  const M = model({
97
97
  state: { x: 0 },
98
98
  actions: (self) => ({
@@ -111,20 +111,20 @@ describe("actions", () => {
111
111
 
112
112
  // ─── Views ────────────────────────────────────────────────────────────────────
113
113
 
114
- describe("views", () => {
115
- it("views return computed signals", () => {
114
+ describe('views', () => {
115
+ it('views return computed signals', () => {
116
116
  const c = Counter.create({ count: 5 })
117
117
  expect(c.doubled()).toBe(10)
118
118
  })
119
119
 
120
- it("views recompute when state changes", () => {
120
+ it('views recompute when state changes', () => {
121
121
  const c = Counter.create({ count: 3 })
122
122
  expect(c.doubled()).toBe(6)
123
123
  c.inc()
124
124
  expect(c.doubled()).toBe(8)
125
125
  })
126
126
 
127
- it("views are reactive in effects", () => {
127
+ it('views are reactive in effects', () => {
128
128
  const c = Counter.create()
129
129
  const observed: boolean[] = []
130
130
  effect(() => {
@@ -138,34 +138,34 @@ describe("views", () => {
138
138
 
139
139
  // ─── asHook ───────────────────────────────────────────────────────────────────
140
140
 
141
- describe("asHook", () => {
141
+ describe('asHook', () => {
142
142
  afterEach(() => resetAllHooks())
143
143
 
144
- it("returns the same instance for the same id", () => {
145
- const useC = Counter.asHook("hook-test")
144
+ it('returns the same instance for the same id', () => {
145
+ const useC = Counter.asHook('hook-test')
146
146
  expect(useC()).toBe(useC())
147
147
  })
148
148
 
149
- it("different ids give independent instances", () => {
150
- const useA = Counter.asHook("hook-a")
151
- const useB = Counter.asHook("hook-b")
149
+ it('different ids give independent instances', () => {
150
+ const useA = Counter.asHook('hook-a')
151
+ const useB = Counter.asHook('hook-b')
152
152
  useA().inc()
153
153
  expect(useA().count()).toBe(1)
154
154
  expect(useB().count()).toBe(0)
155
155
  })
156
156
 
157
- it("resetHook clears a singleton so next call creates fresh instance", () => {
158
- const useC = Counter.asHook("hook-reset")
157
+ it('resetHook clears a singleton so next call creates fresh instance', () => {
158
+ const useC = Counter.asHook('hook-reset')
159
159
  useC().add(10)
160
160
  expect(useC().count()).toBe(10)
161
161
 
162
- resetHook("hook-reset")
162
+ resetHook('hook-reset')
163
163
  expect(useC().count()).toBe(0)
164
164
  })
165
165
 
166
- it("resetAllHooks clears all singletons", () => {
167
- const useA = Counter.asHook("hook-all-a")
168
- const useB = Counter.asHook("hook-all-b")
166
+ it('resetAllHooks clears all singletons', () => {
167
+ const useA = Counter.asHook('hook-all-a')
168
+ const useB = Counter.asHook('hook-all-b')
169
169
  useA().inc()
170
170
  useB().add(5)
171
171
 
@@ -174,42 +174,42 @@ describe("asHook", () => {
174
174
  expect(useB().count()).toBe(0)
175
175
  })
176
176
 
177
- it("resetHook on non-existent id is a no-op", () => {
178
- expect(() => resetHook("no-such-hook")).not.toThrow()
177
+ it('resetHook on non-existent id is a no-op', () => {
178
+ expect(() => resetHook('no-such-hook')).not.toThrow()
179
179
  })
180
180
  })
181
181
 
182
182
  // ─── Error guards ────────────────────────────────────────────────────────────
183
183
 
184
- describe("error guards", () => {
185
- it("onPatch throws for non-model-instance", () => {
184
+ describe('error guards', () => {
185
+ it('onPatch throws for non-model-instance', () => {
186
186
  expect(() =>
187
187
  onPatch({} as object, () => {
188
188
  /* noop */
189
189
  }),
190
- ).toThrow("[@pyreon/state-tree]")
190
+ ).toThrow('[@pyreon/state-tree]')
191
191
  })
192
192
 
193
- it("addMiddleware throws for non-model-instance", () => {
194
- expect(() => addMiddleware({} as object, (_c, n) => n(_c))).toThrow("[@pyreon/state-tree]")
193
+ it('addMiddleware throws for non-model-instance', () => {
194
+ expect(() => addMiddleware({} as object, (_c, n) => n(_c))).toThrow('[@pyreon/state-tree]')
195
195
  })
196
196
 
197
- it("applySnapshot throws for non-model-instance", () => {
198
- expect(() => applySnapshot({} as object, {})).toThrow("[@pyreon/state-tree]")
197
+ it('applySnapshot throws for non-model-instance', () => {
198
+ expect(() => applySnapshot({} as object, {})).toThrow('[@pyreon/state-tree]')
199
199
  })
200
200
  })
201
201
 
202
202
  // ─── getSnapshot ──────────────────────────────────────────────────────────────
203
203
 
204
- describe("getSnapshot", () => {
205
- it("returns a plain JS object", () => {
204
+ describe('getSnapshot', () => {
205
+ it('returns a plain JS object', () => {
206
206
  const c = Counter.create({ count: 7 })
207
207
  const snap = getSnapshot(c)
208
208
  expect(snap).toEqual({ count: 7 })
209
- expect(typeof snap.count).toBe("number")
209
+ expect(typeof snap.count).toBe('number')
210
210
  })
211
211
 
212
- it("snapshot reflects current state after mutations", () => {
212
+ it('snapshot reflects current state after mutations', () => {
213
213
  const c = Counter.create()
214
214
  c.inc()
215
215
  c.inc()
@@ -217,29 +217,29 @@ describe("getSnapshot", () => {
217
217
  expect(getSnapshot(c)).toEqual({ count: 3 })
218
218
  })
219
219
 
220
- it("throws for non-model-instance values", () => {
221
- expect(() => getSnapshot({} as object)).toThrow("[@pyreon/state-tree]")
220
+ it('throws for non-model-instance values', () => {
221
+ expect(() => getSnapshot({} as object)).toThrow('[@pyreon/state-tree]')
222
222
  })
223
223
  })
224
224
 
225
225
  // ─── applySnapshot ────────────────────────────────────────────────────────────
226
226
 
227
- describe("applySnapshot", () => {
228
- it("restores state from a plain snapshot", () => {
227
+ describe('applySnapshot', () => {
228
+ it('restores state from a plain snapshot', () => {
229
229
  const c = Counter.create({ count: 10 })
230
230
  applySnapshot(c, { count: 0 })
231
231
  expect(c.count()).toBe(0)
232
232
  })
233
233
 
234
- it("partial snapshot — only specified keys are updated", () => {
235
- const NamedCounter = model({ state: { count: 0, label: "x" } })
236
- const c = NamedCounter.create({ count: 5, label: "hello" })
234
+ it('partial snapshot — only specified keys are updated', () => {
235
+ const NamedCounter = model({ state: { count: 0, label: 'x' } })
236
+ const c = NamedCounter.create({ count: 5, label: 'hello' })
237
237
  applySnapshot(c, { count: 99 })
238
238
  expect(c.count()).toBe(99)
239
- expect(c.label()).toBe("hello")
239
+ expect(c.label()).toBe('hello')
240
240
  })
241
241
 
242
- it("batch: effects fire once even for multi-field updates", () => {
242
+ it('batch: effects fire once even for multi-field updates', () => {
243
243
  const M = model({ state: { a: 0, b: 0 } })
244
244
  const m = M.create()
245
245
  let effectRuns = 0
@@ -258,17 +258,17 @@ describe("applySnapshot", () => {
258
258
 
259
259
  // ─── onPatch ──────────────────────────────────────────────────────────────────
260
260
 
261
- describe("onPatch", () => {
262
- it("fires when a signal is written", () => {
261
+ describe('onPatch', () => {
262
+ it('fires when a signal is written', () => {
263
263
  const c = Counter.create()
264
264
  const patches: Patch[] = []
265
265
  onPatch(c, (p) => patches.push(p))
266
266
  c.inc()
267
267
  expect(patches).toHaveLength(1)
268
- expect(patches[0]).toEqual({ op: "replace", path: "/count", value: 1 })
268
+ expect(patches[0]).toEqual({ op: 'replace', path: '/count', value: 1 })
269
269
  })
270
270
 
271
- it("does NOT fire when value is unchanged", () => {
271
+ it('does NOT fire when value is unchanged', () => {
272
272
  const c = Counter.create()
273
273
  const patches: Patch[] = []
274
274
  onPatch(c, (p) => patches.push(p))
@@ -276,7 +276,7 @@ describe("onPatch", () => {
276
276
  expect(patches).toHaveLength(0)
277
277
  })
278
278
 
279
- it("unsub stops patch events", () => {
279
+ it('unsub stops patch events', () => {
280
280
  const c = Counter.create()
281
281
  const patches: Patch[] = []
282
282
  const unsub = onPatch(c, (p) => patches.push(p))
@@ -285,7 +285,7 @@ describe("onPatch", () => {
285
285
  expect(patches).toHaveLength(0)
286
286
  })
287
287
 
288
- it("includes correct value in patch", () => {
288
+ it('includes correct value in patch', () => {
289
289
  const c = Counter.create()
290
290
  const values: number[] = []
291
291
  onPatch(c, (p) => values.push(p.value as number))
@@ -297,8 +297,8 @@ describe("onPatch", () => {
297
297
 
298
298
  // ─── addMiddleware ────────────────────────────────────────────────────────────
299
299
 
300
- describe("addMiddleware", () => {
301
- it("intercepts action calls", () => {
300
+ describe('addMiddleware', () => {
301
+ it('intercepts action calls', () => {
302
302
  const c = Counter.create()
303
303
  const intercepted: string[] = []
304
304
  addMiddleware(c, (call, next) => {
@@ -306,17 +306,17 @@ describe("addMiddleware", () => {
306
306
  return next(call)
307
307
  })
308
308
  c.inc()
309
- expect(intercepted).toContain("inc")
309
+ expect(intercepted).toContain('inc')
310
310
  })
311
311
 
312
- it("next() executes the action", () => {
312
+ it('next() executes the action', () => {
313
313
  const c = Counter.create()
314
314
  addMiddleware(c, (call, next) => next(call))
315
315
  c.add(5)
316
316
  expect(c.count()).toBe(5)
317
317
  })
318
318
 
319
- it("middleware can prevent action from running by not calling next", () => {
319
+ it('middleware can prevent action from running by not calling next', () => {
320
320
  const c = Counter.create()
321
321
  addMiddleware(c, (_call, _next) => {
322
322
  /* block */
@@ -325,25 +325,25 @@ describe("addMiddleware", () => {
325
325
  expect(c.count()).toBe(0)
326
326
  })
327
327
 
328
- it("multiple middlewares run in registration order", () => {
328
+ it('multiple middlewares run in registration order', () => {
329
329
  const c = Counter.create()
330
330
  const log: string[] = []
331
331
  addMiddleware(c, (call, next) => {
332
- log.push("A")
332
+ log.push('A')
333
333
  next(call)
334
334
  log.push("A'")
335
335
  })
336
336
  addMiddleware(c, (call, next) => {
337
- log.push("B")
337
+ log.push('B')
338
338
  next(call)
339
339
  log.push("B'")
340
340
  })
341
341
  c.inc()
342
342
  // Koa-style: A→B→action→B'→A' (inner middleware unwraps first)
343
- expect(log).toEqual(["A", "B", "B'", "A'"])
343
+ expect(log).toEqual(['A', 'B', "B'", "A'"])
344
344
  })
345
345
 
346
- it("unsub removes the middleware", () => {
346
+ it('unsub removes the middleware', () => {
347
347
  const c = Counter.create()
348
348
  const log: string[] = []
349
349
  const unsub = addMiddleware(c, (call, next) => {
@@ -358,8 +358,8 @@ describe("addMiddleware", () => {
358
358
 
359
359
  // ─── patch.ts trackedSignal coverage ─────────────────────────────────────────
360
360
 
361
- describe("trackedSignal extra paths", () => {
362
- it("subscribe on a state signal works (trackedSignal.subscribe)", () => {
361
+ describe('trackedSignal extra paths', () => {
362
+ it('subscribe on a state signal works (trackedSignal.subscribe)', () => {
363
363
  const c = Counter.create()
364
364
  const calls: number[] = []
365
365
  const unsub = c.count.subscribe(() => {
@@ -373,13 +373,13 @@ describe("trackedSignal extra paths", () => {
373
373
  expect(calls).toHaveLength(1)
374
374
  })
375
375
 
376
- it("update on a state signal uses the updater function (trackedSignal.update)", () => {
376
+ it('update on a state signal uses the updater function (trackedSignal.update)', () => {
377
377
  const c = Counter.create({ count: 5 })
378
378
  c.count.update((n: number) => n * 2)
379
379
  expect(c.count()).toBe(10)
380
380
  })
381
381
 
382
- it("peek on a state signal reads without tracking (trackedSignal.peek)", () => {
382
+ it('peek on a state signal reads without tracking (trackedSignal.peek)', () => {
383
383
  const c = Counter.create({ count: 42 })
384
384
  expect(c.count.peek()).toBe(42)
385
385
  })
@@ -387,8 +387,8 @@ describe("trackedSignal extra paths", () => {
387
387
 
388
388
  // ─── patch.ts snapshotValue coverage ─────────────────────────────────────────
389
389
 
390
- describe("patch snapshotValue", () => {
391
- it("emits a snapshot (not a live instance) when setting a nested model signal", () => {
390
+ describe('patch snapshotValue', () => {
391
+ it('emits a snapshot (not a live instance) when setting a nested model signal', () => {
392
392
  // When a nested model instance is set as a value and a patch listener is active,
393
393
  // snapshotValue should recursively serialize the nested model.
394
394
  const Inner = model({
@@ -399,7 +399,7 @@ describe("patch snapshotValue", () => {
399
399
  })
400
400
 
401
401
  const Outer = model({
402
- state: { child: Inner, label: "hi" },
402
+ state: { child: Inner, label: 'hi' },
403
403
  actions: (self) => ({
404
404
  replaceChild: (newChild: any) => self.child.set(newChild),
405
405
  }),
@@ -414,22 +414,22 @@ describe("patch snapshotValue", () => {
414
414
  outer.replaceChild(newInner)
415
415
 
416
416
  expect(patches).toHaveLength(1)
417
- expect(patches[0]!.op).toBe("replace")
418
- expect(patches[0]!.path).toBe("/child")
417
+ expect(patches[0]!.op).toBe('replace')
418
+ expect(patches[0]!.path).toBe('/child')
419
419
  // The value should be a plain snapshot, not the live model instance
420
420
  expect(patches[0]!.value).toEqual({ x: 99, y: 42 })
421
421
  expect(patches[0]!.value).not.toBe(newInner)
422
422
  })
423
423
 
424
- it("snapshotValue recursively serializes deeply nested model instances", () => {
424
+ it('snapshotValue recursively serializes deeply nested model instances', () => {
425
425
  const Leaf = model({
426
426
  state: { val: 0 },
427
427
  })
428
428
  const Mid = model({
429
- state: { leaf: Leaf, tag: "mid" },
429
+ state: { leaf: Leaf, tag: 'mid' },
430
430
  })
431
431
  const Root = model({
432
- state: { mid: Mid, name: "root" },
432
+ state: { mid: Mid, name: 'root' },
433
433
  actions: (self) => ({
434
434
  replaceMid: (m: any) => self.mid.set(m),
435
435
  }),
@@ -439,13 +439,13 @@ describe("patch snapshotValue", () => {
439
439
  const patches: Patch[] = []
440
440
  onPatch(root, (p) => patches.push(p))
441
441
 
442
- const newMid = Mid.create({ leaf: { val: 77 }, tag: "new" })
442
+ const newMid = Mid.create({ leaf: { val: 77 }, tag: 'new' })
443
443
  root.replaceMid(newMid)
444
444
 
445
- expect(patches[0]!.value).toEqual({ leaf: { val: 77 }, tag: "new" })
445
+ expect(patches[0]!.value).toEqual({ leaf: { val: 77 }, tag: 'new' })
446
446
  })
447
447
 
448
- it("snapshotValue returns the object as-is when it has no meta (!meta branch)", () => {
448
+ it('snapshotValue returns the object as-is when it has no meta (!meta branch)', () => {
449
449
  // To trigger the !meta branch in snapshotValue, we need isModelInstance to return true
450
450
  // for an object that has no actual meta. We do this by temporarily registering a
451
451
  // fake object in instanceMeta, making it pass isModelInstance, then deleting the meta
@@ -472,7 +472,7 @@ describe("patch snapshotValue", () => {
472
472
  // but with stateKeys that reference properties that don't exist on the object (!sig branch)
473
473
  const fakeInstance = {} as any
474
474
  instanceMeta.set(fakeInstance, {
475
- stateKeys: ["missing"],
475
+ stateKeys: ['missing'],
476
476
  patchListeners: new Set(),
477
477
  middlewares: [],
478
478
  emitPatch: () => {
@@ -486,12 +486,12 @@ describe("patch snapshotValue", () => {
486
486
  expect(patches[0]!.value).toEqual({})
487
487
  })
488
488
 
489
- it("snapshotValue handles stateKey with nested model value recursively in patches", () => {
489
+ it('snapshotValue handles stateKey with nested model value recursively in patches', () => {
490
490
  // Ensure the recursive path in snapshotValue is covered:
491
491
  // when a stateKey's peek() returns a model instance, it recurses.
492
492
  const Leaf = model({ state: { v: 1 } })
493
493
  const Branch = model({
494
- state: { leaf: Leaf, tag: "a" },
494
+ state: { leaf: Leaf, tag: 'a' },
495
495
  })
496
496
  const Root = model({
497
497
  state: { branch: Branch },
@@ -504,24 +504,24 @@ describe("patch snapshotValue", () => {
504
504
  const patches: Patch[] = []
505
505
  onPatch(root, (p) => patches.push(p))
506
506
 
507
- const newBranch = Branch.create({ leaf: { v: 99 }, tag: "b" })
507
+ const newBranch = Branch.create({ leaf: { v: 99 }, tag: 'b' })
508
508
  root.replaceBranch(newBranch)
509
509
 
510
- expect(patches[0]!.value).toEqual({ leaf: { v: 99 }, tag: "b" })
510
+ expect(patches[0]!.value).toEqual({ leaf: { v: 99 }, tag: 'b' })
511
511
  })
512
512
  })
513
513
 
514
514
  // ─── middleware.ts edge cases ────────────────────────────────────────────────
515
515
 
516
- describe("middleware edge cases", () => {
517
- it("action runs directly when middleware array is empty (idx >= length branch)", () => {
516
+ describe('middleware edge cases', () => {
517
+ it('action runs directly when middleware array is empty (idx >= length branch)', () => {
518
518
  const c = Counter.create()
519
519
  // No middleware added — dispatch(0, call) hits idx >= meta.middlewares.length immediately
520
520
  c.inc()
521
521
  expect(c.count()).toBe(1)
522
522
  })
523
523
 
524
- it("skips falsy middleware entries (!mw branch)", () => {
524
+ it('skips falsy middleware entries (!mw branch)', () => {
525
525
  const c = Counter.create()
526
526
  // Add a real middleware so the array is non-empty
527
527
  addMiddleware(c, (call, next) => next(call))
@@ -533,7 +533,7 @@ describe("middleware edge cases", () => {
533
533
  expect(c.count()).toBe(1)
534
534
  })
535
535
 
536
- it("double-unsub is a no-op (indexOf returns -1 branch)", () => {
536
+ it('double-unsub is a no-op (indexOf returns -1 branch)', () => {
537
537
  const c = Counter.create()
538
538
  const unsub = addMiddleware(c, (call, next) => next(call))
539
539
  unsub()
@@ -544,8 +544,8 @@ describe("middleware edge cases", () => {
544
544
 
545
545
  // ─── snapshot.ts edge cases ──────────────────────────────────────────────────
546
546
 
547
- describe("snapshot edge cases", () => {
548
- it("getSnapshot skips keys whose signal does not exist (!sig branch)", () => {
547
+ describe('snapshot edge cases', () => {
548
+ it('getSnapshot skips keys whose signal does not exist (!sig branch)', () => {
549
549
  // Create an instance then tamper with it to have a missing signal for a state key
550
550
  const c = Counter.create({ count: 5 })
551
551
  // Delete the signal to simulate the !sig branch
@@ -555,7 +555,7 @@ describe("snapshot edge cases", () => {
555
555
  expect(snap).toEqual({})
556
556
  })
557
557
 
558
- it("applySnapshot skips keys not present in the snapshot object", () => {
558
+ it('applySnapshot skips keys not present in the snapshot object', () => {
559
559
  const M = model({ state: { a: 1, b: 2, c: 3 } })
560
560
  const m = M.create({ a: 10, b: 20, c: 30 })
561
561
  // Only apply 'b' — 'a' and 'c' should remain unchanged
@@ -565,7 +565,7 @@ describe("snapshot edge cases", () => {
565
565
  expect(m.c()).toBe(30)
566
566
  })
567
567
 
568
- it("applySnapshot skips keys whose signal does not exist (!sig branch)", () => {
568
+ it('applySnapshot skips keys whose signal does not exist (!sig branch)', () => {
569
569
  const c = Counter.create({ count: 5 })
570
570
  // Delete the signal to simulate the !sig branch in applySnapshot
571
571
  delete (c as any).count
@@ -576,95 +576,95 @@ describe("snapshot edge cases", () => {
576
576
 
577
577
  // ─── Nested models ────────────────────────────────────────────────────────────
578
578
 
579
- describe("nested models", () => {
580
- it("creates nested instance from snapshot", () => {
579
+ describe('nested models', () => {
580
+ it('creates nested instance from snapshot', () => {
581
581
  const app = App.create({
582
- profile: { name: "Alice", bio: "dev" },
583
- title: "App",
582
+ profile: { name: 'Alice', bio: 'dev' },
583
+ title: 'App',
584
584
  })
585
- expect(app.profile().name()).toBe("Alice")
585
+ expect(app.profile().name()).toBe('Alice')
586
586
  })
587
587
 
588
- it("nested instance has its own actions", () => {
588
+ it('nested instance has its own actions', () => {
589
589
  const app = App.create({
590
- profile: { name: "Alice", bio: "" },
591
- title: "App",
590
+ profile: { name: 'Alice', bio: '' },
591
+ title: 'App',
592
592
  })
593
- app.profile().rename("Bob")
594
- expect(app.profile().name()).toBe("Bob")
593
+ app.profile().rename('Bob')
594
+ expect(app.profile().name()).toBe('Bob')
595
595
  })
596
596
 
597
- it("nested defaults used when no snapshot provided", () => {
597
+ it('nested defaults used when no snapshot provided', () => {
598
598
  const app = App.create()
599
- expect(app.profile().name()).toBe("")
600
- expect(app.title()).toBe("My App")
599
+ expect(app.profile().name()).toBe('')
600
+ expect(app.title()).toBe('My App')
601
601
  })
602
602
 
603
- it("getSnapshot recursively serializes nested instances", () => {
603
+ it('getSnapshot recursively serializes nested instances', () => {
604
604
  const app = App.create({
605
- profile: { name: "Alice", bio: "dev" },
606
- title: "Hello",
605
+ profile: { name: 'Alice', bio: 'dev' },
606
+ title: 'Hello',
607
607
  })
608
608
  const snap = getSnapshot(app)
609
609
  expect(snap).toEqual({
610
- profile: { name: "Alice", bio: "dev" },
611
- title: "Hello",
610
+ profile: { name: 'Alice', bio: 'dev' },
611
+ title: 'Hello',
612
612
  })
613
613
  })
614
614
 
615
- it("onPatch emits nested path for nested state change", () => {
616
- const app = App.create({ profile: { name: "Alice", bio: "" }, title: "" })
615
+ it('onPatch emits nested path for nested state change', () => {
616
+ const app = App.create({ profile: { name: 'Alice', bio: '' }, title: '' })
617
617
  const patches: Patch[] = []
618
618
  onPatch(app, (p) => patches.push(p))
619
- app.profile().rename("Bob")
619
+ app.profile().rename('Bob')
620
620
  expect(patches).toHaveLength(1)
621
621
  expect(patches[0]).toEqual({
622
- op: "replace",
623
- path: "/profile/name",
624
- value: "Bob",
622
+ op: 'replace',
623
+ path: '/profile/name',
624
+ value: 'Bob',
625
625
  })
626
626
  })
627
627
 
628
- it("applySnapshot restores nested state", () => {
628
+ it('applySnapshot restores nested state', () => {
629
629
  const app = App.create({
630
- profile: { name: "Alice", bio: "" },
631
- title: "old",
630
+ profile: { name: 'Alice', bio: '' },
631
+ title: 'old',
632
632
  })
633
- applySnapshot(app, { profile: { name: "Carol", bio: "new" }, title: "new" })
634
- expect(app.profile().name()).toBe("Carol")
635
- expect(app.title()).toBe("new")
633
+ applySnapshot(app, { profile: { name: 'Carol', bio: 'new' }, title: 'new' })
634
+ expect(app.profile().name()).toBe('Carol')
635
+ expect(app.title()).toBe('new')
636
636
  })
637
637
  })
638
638
 
639
639
  // ─── applyPatch ──────────────────────────────────────────────────────────────
640
640
 
641
- describe("applyPatch", () => {
642
- it("applies a single replace patch", () => {
641
+ describe('applyPatch', () => {
642
+ it('applies a single replace patch', () => {
643
643
  const c = Counter.create({ count: 0 })
644
- applyPatch(c, { op: "replace", path: "/count", value: 42 })
644
+ applyPatch(c, { op: 'replace', path: '/count', value: 42 })
645
645
  expect(c.count()).toBe(42)
646
646
  })
647
647
 
648
- it("applies an array of patches", () => {
648
+ it('applies an array of patches', () => {
649
649
  const c = Counter.create({ count: 0 })
650
650
  applyPatch(c, [
651
- { op: "replace", path: "/count", value: 1 },
652
- { op: "replace", path: "/count", value: 2 },
653
- { op: "replace", path: "/count", value: 3 },
651
+ { op: 'replace', path: '/count', value: 1 },
652
+ { op: 'replace', path: '/count', value: 2 },
653
+ { op: 'replace', path: '/count', value: 3 },
654
654
  ])
655
655
  expect(c.count()).toBe(3)
656
656
  })
657
657
 
658
- it("applies patch to nested model instance", () => {
658
+ it('applies patch to nested model instance', () => {
659
659
  const app = App.create({
660
- profile: { name: "Alice", bio: "" },
661
- title: "old",
660
+ profile: { name: 'Alice', bio: '' },
661
+ title: 'old',
662
662
  })
663
- applyPatch(app, { op: "replace", path: "/profile/name", value: "Bob" })
664
- expect(app.profile().name()).toBe("Bob")
663
+ applyPatch(app, { op: 'replace', path: '/profile/name', value: 'Bob' })
664
+ expect(app.profile().name()).toBe('Bob')
665
665
  })
666
666
 
667
- it("roundtrips with onPatch — record and replay", () => {
667
+ it('roundtrips with onPatch — record and replay', () => {
668
668
  const c = Counter.create({ count: 0 })
669
669
  const patches: Patch[] = []
670
670
  onPatch(c, (p) => patches.push({ ...p }))
@@ -675,9 +675,9 @@ describe("applyPatch", () => {
675
675
  expect(c.count()).toBe(12)
676
676
  expect(patches).toHaveLength(3)
677
677
  // Verify patches contain the final values at each step
678
- expect(patches[0]).toEqual({ op: "replace", path: "/count", value: 1 })
679
- expect(patches[1]).toEqual({ op: "replace", path: "/count", value: 2 })
680
- expect(patches[2]).toEqual({ op: "replace", path: "/count", value: 12 })
678
+ expect(patches[0]).toEqual({ op: 'replace', path: '/count', value: 1 })
679
+ expect(patches[1]).toEqual({ op: 'replace', path: '/count', value: 2 })
680
+ expect(patches[2]).toEqual({ op: 'replace', path: '/count', value: 12 })
681
681
 
682
682
  // Replay on a fresh instance
683
683
  const c2 = Counter.create({ count: 0 })
@@ -685,27 +685,27 @@ describe("applyPatch", () => {
685
685
  expect(c2.count()).toBe(12)
686
686
  })
687
687
 
688
- it("throws for non-model instance", () => {
689
- expect(() => applyPatch({}, { op: "replace", path: "/x", value: 1 })).toThrow(
690
- "not a model instance",
688
+ it('throws for non-model instance', () => {
689
+ expect(() => applyPatch({}, { op: 'replace', path: '/x', value: 1 })).toThrow(
690
+ 'not a model instance',
691
691
  )
692
692
  })
693
693
 
694
- it("throws for empty path", () => {
694
+ it('throws for empty path', () => {
695
695
  const c = Counter.create({ count: 0 })
696
- expect(() => applyPatch(c, { op: "replace", path: "", value: 1 })).toThrow("empty path")
696
+ expect(() => applyPatch(c, { op: 'replace', path: '', value: 1 })).toThrow('empty path')
697
697
  })
698
698
 
699
- it("throws for unknown state key", () => {
699
+ it('throws for unknown state key', () => {
700
700
  const c = Counter.create({ count: 0 })
701
- expect(() => applyPatch(c, { op: "replace", path: "/nonexistent", value: 1 })).toThrow(
702
- "unknown state key",
701
+ expect(() => applyPatch(c, { op: 'replace', path: '/nonexistent', value: 1 })).toThrow(
702
+ 'unknown state key',
703
703
  )
704
704
  })
705
705
 
706
- it("throws for unsupported op", () => {
706
+ it('throws for unsupported op', () => {
707
707
  const c = Counter.create({ count: 0 })
708
- expect(() => applyPatch(c, { op: "add" as any, path: "/count", value: 1 })).toThrow(
708
+ expect(() => applyPatch(c, { op: 'add' as any, path: '/count', value: 1 })).toThrow(
709
709
  'unsupported op "add"',
710
710
  )
711
711
  })