@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.
package/src/patch.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { Signal } from "@pyreon/reactivity"
2
- import { batch } from "@pyreon/reactivity"
3
- import { instanceMeta, isModelInstance } from "./registry"
4
- import type { Patch, PatchListener } from "./types"
1
+ import type { Signal } from '@pyreon/reactivity'
2
+ import { batch } from '@pyreon/reactivity'
3
+ import { instanceMeta, isModelInstance } from './registry'
4
+ import type { Patch, PatchListener } from './types'
5
5
 
6
6
  /** Property names that must never be used as patch path segments. */
7
- const RESERVED_KEYS = new Set(["__proto__", "constructor", "prototype"])
7
+ const RESERVED_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
8
8
 
9
9
  // ─── Tracked signal ───────────────────────────────────────────────────────────
10
10
 
@@ -36,7 +36,7 @@ export function trackedSignal<T>(
36
36
  // For model instances, emit the snapshot rather than the live object
37
37
  // so patches are always plain JSON-serializable values.
38
38
  const patchValue = isModelInstance(newValue) ? snapshotValue(newValue as object) : newValue
39
- emitPatch({ op: "replace", path, value: patchValue })
39
+ emitPatch({ op: 'replace', path, value: patchValue })
40
40
  }
41
41
  }
42
42
 
@@ -76,7 +76,7 @@ function snapshotValue(instance: object): Record<string, unknown> {
76
76
  */
77
77
  export function onPatch(instance: object, listener: PatchListener): () => void {
78
78
  const meta = instanceMeta.get(instance)
79
- if (!meta) throw new Error("[@pyreon/state-tree] onPatch: not a model instance")
79
+ if (!meta) throw new Error('[@pyreon/state-tree] onPatch: not a model instance')
80
80
  meta.patchListeners.add(listener)
81
81
  return () => meta.patchListeners.delete(listener)
82
82
  }
@@ -105,13 +105,13 @@ export function applyPatch(instance: object, patch: Patch | Patch[]): void {
105
105
 
106
106
  batch(() => {
107
107
  for (const p of patches) {
108
- if (p.op !== "replace") {
108
+ if (p.op !== 'replace') {
109
109
  throw new Error(`[@pyreon/state-tree] applyPatch: unsupported op "${p.op}"`)
110
110
  }
111
111
 
112
- const segments = p.path.split("/").filter(Boolean)
112
+ const segments = p.path.split('/').filter(Boolean)
113
113
  if (segments.length === 0) {
114
- throw new Error("[@pyreon/state-tree] applyPatch: empty path")
114
+ throw new Error('[@pyreon/state-tree] applyPatch: empty path')
115
115
  }
116
116
 
117
117
  // Walk to the target instance for nested paths
@@ -125,11 +125,11 @@ export function applyPatch(instance: object, patch: Patch | Patch[]): void {
125
125
  if (!meta)
126
126
  throw new Error(`[@pyreon/state-tree] applyPatch: not a model instance at "${segment}"`)
127
127
  const sig = (target as Record<string, Signal<unknown>>)[segment]
128
- if (!sig || typeof sig.peek !== "function") {
128
+ if (!sig || typeof sig.peek !== 'function') {
129
129
  throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key "${segment}"`)
130
130
  }
131
131
  const nested = sig.peek()
132
- if (!nested || typeof nested !== "object" || !isModelInstance(nested)) {
132
+ if (!nested || typeof nested !== 'object' || !isModelInstance(nested)) {
133
133
  throw new Error(
134
134
  `[@pyreon/state-tree] applyPatch: "${segment}" is not a nested model instance`,
135
135
  )
@@ -142,13 +142,13 @@ export function applyPatch(instance: object, patch: Patch | Patch[]): void {
142
142
  throw new Error(`[@pyreon/state-tree] applyPatch: reserved property name "${lastKey}"`)
143
143
  }
144
144
  const meta = instanceMeta.get(target)
145
- if (!meta) throw new Error("[@pyreon/state-tree] applyPatch: not a model instance")
145
+ if (!meta) throw new Error('[@pyreon/state-tree] applyPatch: not a model instance')
146
146
  if (!meta.stateKeys.includes(lastKey)) {
147
147
  throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key "${lastKey}"`)
148
148
  }
149
149
 
150
150
  const sig = (target as Record<string, Signal<unknown>>)[lastKey]
151
- if (sig && typeof sig.set === "function") {
151
+ if (sig && typeof sig.set === 'function') {
152
152
  sig.set(p.value)
153
153
  }
154
154
  }
package/src/registry.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InstanceMeta } from "./types"
1
+ import type { InstanceMeta } from './types'
2
2
 
3
3
  /**
4
4
  * WeakMap from every model instance object → its internal metadata.
@@ -8,5 +8,5 @@ export const instanceMeta = new WeakMap<object, InstanceMeta>()
8
8
 
9
9
  /** Returns true when a value is a model instance (has metadata registered). */
10
10
  export function isModelInstance(value: unknown): boolean {
11
- return value != null && typeof value === "object" && instanceMeta.has(value as object)
11
+ return value != null && typeof value === 'object' && instanceMeta.has(value as object)
12
12
  }
package/src/snapshot.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { Signal } from "@pyreon/reactivity"
2
- import { batch } from "@pyreon/reactivity"
3
- import { instanceMeta, isModelInstance } from "./registry"
4
- import type { Snapshot, StateShape } from "./types"
1
+ import type { Signal } from '@pyreon/reactivity'
2
+ import { batch } from '@pyreon/reactivity'
3
+ import { instanceMeta, isModelInstance } from './registry'
4
+ import type { Snapshot, StateShape } from './types'
5
5
 
6
6
  // ─── getSnapshot ──────────────────────────────────────────────────────────────
7
7
 
@@ -15,7 +15,7 @@ import type { Snapshot, StateShape } from "./types"
15
15
  */
16
16
  export function getSnapshot<TState extends StateShape>(instance: object): Snapshot<TState> {
17
17
  const meta = instanceMeta.get(instance)
18
- if (!meta) throw new Error("[@pyreon/state-tree] getSnapshot: not a model instance")
18
+ if (!meta) throw new Error('[@pyreon/state-tree] getSnapshot: not a model instance')
19
19
 
20
20
  const out: Record<string, unknown> = {}
21
21
  for (const key of meta.stateKeys) {
@@ -42,7 +42,7 @@ export function applySnapshot<TState extends StateShape>(
42
42
  snapshot: Partial<Snapshot<TState>>,
43
43
  ): void {
44
44
  const meta = instanceMeta.get(instance)
45
- if (!meta) throw new Error("[@pyreon/state-tree] applySnapshot: not a model instance")
45
+ if (!meta) throw new Error('[@pyreon/state-tree] applySnapshot: not a model instance')
46
46
 
47
47
  batch(() => {
48
48
  for (const key of meta.stateKeys) {
@@ -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,7 +9,7 @@ import {
9
9
  onPatch,
10
10
  resetAllHooks,
11
11
  resetHook,
12
- } from "../index"
12
+ } from '../index'
13
13
 
14
14
  // ─── Fixtures ─────────────────────────────────────────────────────────────────
15
15
 
@@ -27,7 +27,7 @@ const Counter = model({
27
27
  })
28
28
 
29
29
  const Profile = model({
30
- state: { name: "", bio: "" },
30
+ state: { name: '', bio: '' },
31
31
  actions: (self) => ({
32
32
  rename: (n: string) => self.name.set(n),
33
33
  setBio: (b: string) => self.bio.set(b),
@@ -35,7 +35,7 @@ const Profile = model({
35
35
  })
36
36
 
37
37
  const App = model({
38
- state: { profile: Profile, title: "My App" },
38
+ state: { profile: Profile, title: 'My App' },
39
39
  actions: (self) => ({
40
40
  setTitle: (t: string) => self.title.set(t),
41
41
  }),
@@ -43,7 +43,7 @@ const App = model({
43
43
 
44
44
  // ─── getSnapshot — JSON-serializable output ────────────────────────────────
45
45
 
46
- describe("getSnapshot — JSON-serializable output", () => {
46
+ describe('getSnapshot — JSON-serializable output', () => {
47
47
  it("returns a plain object that can be JSON.stringify'd and parsed back", () => {
48
48
  const c = Counter.create({ count: 42 })
49
49
  const snap = getSnapshot(c)
@@ -52,47 +52,47 @@ describe("getSnapshot — JSON-serializable output", () => {
52
52
  expect(parsed).toEqual({ count: 42 })
53
53
  })
54
54
 
55
- it("snapshot contains no signal functions", () => {
55
+ it('snapshot contains no signal functions', () => {
56
56
  const c = Counter.create({ count: 5 })
57
57
  const snap = getSnapshot(c)
58
58
  for (const val of Object.values(snap)) {
59
- expect(typeof val).not.toBe("function")
59
+ expect(typeof val).not.toBe('function')
60
60
  }
61
61
  })
62
62
 
63
- it("nested model snapshot is fully serializable", () => {
63
+ it('nested model snapshot is fully serializable', () => {
64
64
  const app = App.create({
65
- profile: { name: "Alice", bio: "dev" },
66
- title: "Test",
65
+ profile: { name: 'Alice', bio: 'dev' },
66
+ title: 'Test',
67
67
  })
68
68
  const snap = getSnapshot(app)
69
69
  const json = JSON.stringify(snap)
70
70
  const parsed = JSON.parse(json)
71
71
  expect(parsed).toEqual({
72
- profile: { name: "Alice", bio: "dev" },
73
- title: "Test",
72
+ profile: { name: 'Alice', bio: 'dev' },
73
+ title: 'Test',
74
74
  })
75
75
  })
76
76
 
77
- it("snapshot does not include views or actions", () => {
77
+ it('snapshot does not include views or actions', () => {
78
78
  const c = Counter.create({ count: 3 })
79
79
  const snap = getSnapshot(c)
80
80
  expect(snap).toEqual({ count: 3 })
81
- expect(snap).not.toHaveProperty("doubled")
82
- expect(snap).not.toHaveProperty("inc")
81
+ expect(snap).not.toHaveProperty('doubled')
82
+ expect(snap).not.toHaveProperty('inc')
83
83
  })
84
84
  })
85
85
 
86
86
  // ─── applySnapshot — restores model state ──────────────────────────────────
87
87
 
88
- describe("applySnapshot — restores model state", () => {
89
- it("restores a complete snapshot", () => {
88
+ describe('applySnapshot — restores model state', () => {
89
+ it('restores a complete snapshot', () => {
90
90
  const c = Counter.create({ count: 99 })
91
91
  applySnapshot(c, { count: 0 })
92
92
  expect(c.count()).toBe(0)
93
93
  })
94
94
 
95
- it("restores partial snapshot — only specified keys", () => {
95
+ it('restores partial snapshot — only specified keys', () => {
96
96
  const M = model({ state: { a: 1, b: 2, c: 3 } })
97
97
  const m = M.create({ a: 10, b: 20, c: 30 })
98
98
  applySnapshot(m, { b: 99 })
@@ -101,24 +101,24 @@ describe("applySnapshot — restores model state", () => {
101
101
  expect(m.c()).toBe(30)
102
102
  })
103
103
 
104
- it("restores nested model state recursively", () => {
104
+ it('restores nested model state recursively', () => {
105
105
  const app = App.create({
106
- profile: { name: "Alice", bio: "dev" },
107
- title: "Old",
106
+ profile: { name: 'Alice', bio: 'dev' },
107
+ title: 'Old',
108
108
  })
109
109
  applySnapshot(app, {
110
- profile: { name: "Bob", bio: "engineer" },
111
- title: "New",
110
+ profile: { name: 'Bob', bio: 'engineer' },
111
+ title: 'New',
112
112
  })
113
- expect(app.profile().name()).toBe("Bob")
114
- expect(app.profile().bio()).toBe("engineer")
115
- expect(app.title()).toBe("New")
113
+ expect(app.profile().name()).toBe('Bob')
114
+ expect(app.profile().bio()).toBe('engineer')
115
+ expect(app.title()).toBe('New')
116
116
  })
117
117
 
118
- it("roundtrips: getSnapshot -> applySnapshot produces same state", () => {
118
+ it('roundtrips: getSnapshot -> applySnapshot produces same state', () => {
119
119
  const app = App.create({
120
- profile: { name: "Carol", bio: "designer" },
121
- title: "Portfolio",
120
+ profile: { name: 'Carol', bio: 'designer' },
121
+ title: 'Portfolio',
122
122
  })
123
123
  const snap = getSnapshot(app)
124
124
 
@@ -127,7 +127,7 @@ describe("applySnapshot — restores model state", () => {
127
127
  expect(getSnapshot(app2)).toEqual(snap)
128
128
  })
129
129
 
130
- it("batches updates — effect fires once for multi-field snapshot", () => {
130
+ it('batches updates — effect fires once for multi-field snapshot', () => {
131
131
  const M = model({ state: { x: 0, y: 0, z: 0 } })
132
132
  const m = M.create()
133
133
  let effectRuns = 0
@@ -145,19 +145,19 @@ describe("applySnapshot — restores model state", () => {
145
145
 
146
146
  // ─── onPatch — listener receives correct format ────────────────────────────
147
147
 
148
- describe("onPatch — patch format", () => {
149
- it("patch has op, path, and value fields", () => {
148
+ describe('onPatch — patch format', () => {
149
+ it('patch has op, path, and value fields', () => {
150
150
  const c = Counter.create()
151
151
  const patches: Patch[] = []
152
152
  onPatch(c, (p) => patches.push(p))
153
153
  c.inc()
154
154
  expect(patches).toHaveLength(1)
155
- expect(patches[0]).toHaveProperty("op", "replace")
156
- expect(patches[0]).toHaveProperty("path", "/count")
157
- expect(patches[0]).toHaveProperty("value", 1)
155
+ expect(patches[0]).toHaveProperty('op', 'replace')
156
+ expect(patches[0]).toHaveProperty('path', '/count')
157
+ expect(patches[0]).toHaveProperty('value', 1)
158
158
  })
159
159
 
160
- it("path uses JSON pointer format with leading slash", () => {
160
+ it('path uses JSON pointer format with leading slash', () => {
161
161
  const c = Counter.create()
162
162
  const patches: Patch[] = []
163
163
  onPatch(c, (p) => patches.push(p))
@@ -165,16 +165,16 @@ describe("onPatch — patch format", () => {
165
165
  expect(patches[0]!.path).toMatch(/^\//)
166
166
  })
167
167
 
168
- it("nested model patches have composite paths", () => {
169
- const app = App.create({ profile: { name: "A", bio: "" }, title: "" })
168
+ it('nested model patches have composite paths', () => {
169
+ const app = App.create({ profile: { name: 'A', bio: '' }, title: '' })
170
170
  const patches: Patch[] = []
171
171
  onPatch(app, (p) => patches.push(p))
172
172
 
173
- app.profile().rename("B")
174
- expect(patches[0]!.path).toBe("/profile/name")
173
+ app.profile().rename('B')
174
+ expect(patches[0]!.path).toBe('/profile/name')
175
175
  })
176
176
 
177
- it("value contains new value after mutation, not old", () => {
177
+ it('value contains new value after mutation, not old', () => {
178
178
  const c = Counter.create({ count: 10 })
179
179
  const patches: Patch[] = []
180
180
  onPatch(c, (p) => patches.push(p))
@@ -183,7 +183,7 @@ describe("onPatch — patch format", () => {
183
183
  expect(patches[0]!.value).toBe(15)
184
184
  })
185
185
 
186
- it("emits patches for each signal write in sequence", () => {
186
+ it('emits patches for each signal write in sequence', () => {
187
187
  const c = Counter.create()
188
188
  const patches: Patch[] = []
189
189
  onPatch(c, (p) => patches.push(p))
@@ -199,32 +199,32 @@ describe("onPatch — patch format", () => {
199
199
 
200
200
  // ─── applyPatch — applies patches correctly ────────────────────────────────
201
201
 
202
- describe("applyPatch — applies patches", () => {
203
- it("applies a single replace patch to top-level field", () => {
202
+ describe('applyPatch — applies patches', () => {
203
+ it('applies a single replace patch to top-level field', () => {
204
204
  const c = Counter.create()
205
- applyPatch(c, { op: "replace", path: "/count", value: 42 })
205
+ applyPatch(c, { op: 'replace', path: '/count', value: 42 })
206
206
  expect(c.count()).toBe(42)
207
207
  })
208
208
 
209
- it("applies array of patches in order", () => {
209
+ it('applies array of patches in order', () => {
210
210
  const c = Counter.create()
211
211
  applyPatch(c, [
212
- { op: "replace", path: "/count", value: 5 },
213
- { op: "replace", path: "/count", value: 10 },
212
+ { op: 'replace', path: '/count', value: 5 },
213
+ { op: 'replace', path: '/count', value: 10 },
214
214
  ])
215
215
  expect(c.count()).toBe(10)
216
216
  })
217
217
 
218
- it("applies patches to nested model instances", () => {
218
+ it('applies patches to nested model instances', () => {
219
219
  const app = App.create({
220
- profile: { name: "A", bio: "b" },
221
- title: "t",
220
+ profile: { name: 'A', bio: 'b' },
221
+ title: 't',
222
222
  })
223
- applyPatch(app, { op: "replace", path: "/profile/name", value: "B" })
224
- expect(app.profile().name()).toBe("B")
223
+ applyPatch(app, { op: 'replace', path: '/profile/name', value: 'B' })
224
+ expect(app.profile().name()).toBe('B')
225
225
  })
226
226
 
227
- it("roundtrip: record patches with onPatch, replay on fresh instance", () => {
227
+ it('roundtrip: record patches with onPatch, replay on fresh instance', () => {
228
228
  const original = Counter.create()
229
229
  const patches: Patch[] = []
230
230
  onPatch(original, (p) => patches.push({ ...p }))
@@ -239,36 +239,36 @@ describe("applyPatch — applies patches", () => {
239
239
  expect(getSnapshot(replica)).toEqual(getSnapshot(original))
240
240
  })
241
241
 
242
- it("throws for unsupported op", () => {
242
+ it('throws for unsupported op', () => {
243
243
  const c = Counter.create()
244
- expect(() => applyPatch(c, { op: "add" as any, path: "/count", value: 1 })).toThrow(
245
- "unsupported op",
244
+ expect(() => applyPatch(c, { op: 'add' as any, path: '/count', value: 1 })).toThrow(
245
+ 'unsupported op',
246
246
  )
247
247
  })
248
248
 
249
- it("throws for empty path", () => {
249
+ it('throws for empty path', () => {
250
250
  const c = Counter.create()
251
- expect(() => applyPatch(c, { op: "replace", path: "", value: 1 })).toThrow("empty path")
251
+ expect(() => applyPatch(c, { op: 'replace', path: '', value: 1 })).toThrow('empty path')
252
252
  })
253
253
 
254
- it("throws for unknown key", () => {
254
+ it('throws for unknown key', () => {
255
255
  const c = Counter.create()
256
- expect(() => applyPatch(c, { op: "replace", path: "/unknown", value: 1 })).toThrow(
257
- "unknown state key",
256
+ expect(() => applyPatch(c, { op: 'replace', path: '/unknown', value: 1 })).toThrow(
257
+ 'unknown state key',
258
258
  )
259
259
  })
260
260
 
261
- it("throws for non-model instance", () => {
262
- expect(() => applyPatch({}, { op: "replace", path: "/x", value: 1 })).toThrow(
263
- "not a model instance",
261
+ it('throws for non-model instance', () => {
262
+ expect(() => applyPatch({}, { op: 'replace', path: '/x', value: 1 })).toThrow(
263
+ 'not a model instance',
264
264
  )
265
265
  })
266
266
  })
267
267
 
268
268
  // ─── addMiddleware — intercepts actions ────────────────────────────────────
269
269
 
270
- describe("addMiddleware — intercepts actions", () => {
271
- it("captures action name and args", () => {
270
+ describe('addMiddleware — intercepts actions', () => {
271
+ it('captures action name and args', () => {
272
272
  const c = Counter.create()
273
273
  const calls: { name: string; args: unknown[] }[] = []
274
274
  addMiddleware(c, (call, next) => {
@@ -276,10 +276,10 @@ describe("addMiddleware — intercepts actions", () => {
276
276
  return next(call)
277
277
  })
278
278
  c.add(5)
279
- expect(calls).toEqual([{ name: "add", args: [5] }])
279
+ expect(calls).toEqual([{ name: 'add', args: [5] }])
280
280
  })
281
281
 
282
- it("middleware can block action by not calling next", () => {
282
+ it('middleware can block action by not calling next', () => {
283
283
  const c = Counter.create()
284
284
  addMiddleware(c, () => {
285
285
  /* intentionally block */
@@ -288,10 +288,10 @@ describe("addMiddleware — intercepts actions", () => {
288
288
  expect(c.count()).toBe(0)
289
289
  })
290
290
 
291
- it("middleware can modify args", () => {
291
+ it('middleware can modify args', () => {
292
292
  const c = Counter.create()
293
293
  addMiddleware(c, (call, next) => {
294
- if (call.name === "add") {
294
+ if (call.name === 'add') {
295
295
  return next({ ...call, args: [(call.args[0] as number) * 3] })
296
296
  }
297
297
  return next(call)
@@ -300,7 +300,7 @@ describe("addMiddleware — intercepts actions", () => {
300
300
  expect(c.count()).toBe(15) // 5 * 3
301
301
  })
302
302
 
303
- it("unsub removes the middleware", () => {
303
+ it('unsub removes the middleware', () => {
304
304
  const c = Counter.create()
305
305
  const log: string[] = []
306
306
  const unsub = addMiddleware(c, (call, next) => {
@@ -315,30 +315,30 @@ describe("addMiddleware — intercepts actions", () => {
315
315
  expect(log).toHaveLength(1) // no new entries
316
316
  })
317
317
 
318
- it("multiple middlewares execute in Koa-style onion order", () => {
318
+ it('multiple middlewares execute in Koa-style onion order', () => {
319
319
  const c = Counter.create()
320
320
  const log: string[] = []
321
321
  addMiddleware(c, (call, next) => {
322
- log.push("A:before")
322
+ log.push('A:before')
323
323
  const r = next(call)
324
- log.push("A:after")
324
+ log.push('A:after')
325
325
  return r
326
326
  })
327
327
  addMiddleware(c, (call, next) => {
328
- log.push("B:before")
328
+ log.push('B:before')
329
329
  const r = next(call)
330
- log.push("B:after")
330
+ log.push('B:after')
331
331
  return r
332
332
  })
333
333
  c.inc()
334
- expect(log).toEqual(["A:before", "B:before", "B:after", "A:after"])
334
+ expect(log).toEqual(['A:before', 'B:before', 'B:after', 'A:after'])
335
335
  })
336
336
  })
337
337
 
338
338
  // ─── Nested model composition ──────────────────────────────────────────────
339
339
 
340
- describe("nested model composition", () => {
341
- it("deeply nested models work correctly", () => {
340
+ describe('nested model composition', () => {
341
+ it('deeply nested models work correctly', () => {
342
342
  const Leaf = model({
343
343
  state: { val: 0 },
344
344
  actions: (self) => ({
@@ -346,26 +346,26 @@ describe("nested model composition", () => {
346
346
  }),
347
347
  })
348
348
  const Branch = model({
349
- state: { leaf: Leaf, tag: "" },
349
+ state: { leaf: Leaf, tag: '' },
350
350
  actions: (self) => ({
351
351
  setTag: (t: string) => self.tag.set(t),
352
352
  }),
353
353
  })
354
354
  const Root = model({
355
- state: { branch: Branch, name: "root" },
355
+ state: { branch: Branch, name: 'root' },
356
356
  })
357
357
 
358
358
  const root = Root.create({
359
- branch: { leaf: { val: 42 }, tag: "test" },
360
- name: "myRoot",
359
+ branch: { leaf: { val: 42 }, tag: 'test' },
360
+ name: 'myRoot',
361
361
  })
362
362
 
363
363
  expect(root.branch().leaf().val()).toBe(42)
364
- expect(root.branch().tag()).toBe("test")
365
- expect(root.name()).toBe("myRoot")
364
+ expect(root.branch().tag()).toBe('test')
365
+ expect(root.name()).toBe('myRoot')
366
366
  })
367
367
 
368
- it("nested model patches propagate up with correct paths", () => {
368
+ it('nested model patches propagate up with correct paths', () => {
369
369
  const Leaf = model({
370
370
  state: { val: 0 },
371
371
  actions: (self) => ({
@@ -385,11 +385,11 @@ describe("nested model composition", () => {
385
385
 
386
386
  root.branch().leaf().setVal(99)
387
387
  expect(patches).toHaveLength(1)
388
- expect(patches[0]!.path).toBe("/branch/leaf/val")
388
+ expect(patches[0]!.path).toBe('/branch/leaf/val')
389
389
  expect(patches[0]!.value).toBe(99)
390
390
  })
391
391
 
392
- it("nested getSnapshot serializes all levels", () => {
392
+ it('nested getSnapshot serializes all levels', () => {
393
393
  const Leaf = model({ state: { x: 1 } })
394
394
  const Mid = model({ state: { leaf: Leaf, y: 2 } })
395
395
  const Top = model({ state: { mid: Mid, z: 3 } })
@@ -401,53 +401,53 @@ describe("nested model composition", () => {
401
401
  })
402
402
  })
403
403
 
404
- it("applyPatch to deeply nested path works", () => {
404
+ it('applyPatch to deeply nested path works', () => {
405
405
  const Leaf = model({ state: { x: 0 } })
406
406
  const Mid = model({ state: { leaf: Leaf } })
407
407
  const Top = model({ state: { mid: Mid } })
408
408
 
409
409
  const top = Top.create()
410
- applyPatch(top, { op: "replace", path: "/mid/leaf/x", value: 999 })
410
+ applyPatch(top, { op: 'replace', path: '/mid/leaf/x', value: 999 })
411
411
  expect(top.mid().leaf().x()).toBe(999)
412
412
  })
413
413
  })
414
414
 
415
415
  // ─── asHook — singleton hook ───────────────────────────────────────────────
416
416
 
417
- describe("asHook — creates singleton hook", () => {
417
+ describe('asHook — creates singleton hook', () => {
418
418
  afterEach(() => resetAllHooks())
419
419
 
420
- it("returns the same instance every time", () => {
421
- const useC = Counter.asHook("hook-same")
420
+ it('returns the same instance every time', () => {
421
+ const useC = Counter.asHook('hook-same')
422
422
  const a = useC()
423
423
  const b = useC()
424
424
  expect(a).toBe(b)
425
425
  })
426
426
 
427
- it("state mutations persist across calls", () => {
428
- const useC = Counter.asHook("hook-persist")
427
+ it('state mutations persist across calls', () => {
428
+ const useC = Counter.asHook('hook-persist')
429
429
  useC().add(10)
430
430
  expect(useC().count()).toBe(10)
431
431
  })
432
432
 
433
- it("different ids yield independent instances", () => {
434
- const useA = Counter.asHook("hook-id-a")
435
- const useB = Counter.asHook("hook-id-b")
433
+ it('different ids yield independent instances', () => {
434
+ const useA = Counter.asHook('hook-id-a')
435
+ const useB = Counter.asHook('hook-id-b')
436
436
  useA().add(5)
437
437
  expect(useA().count()).toBe(5)
438
438
  expect(useB().count()).toBe(0)
439
439
  })
440
440
 
441
- it("resetHook clears specific singleton", () => {
442
- const useC = Counter.asHook("hook-reset-2")
441
+ it('resetHook clears specific singleton', () => {
442
+ const useC = Counter.asHook('hook-reset-2')
443
443
  useC().add(100)
444
- resetHook("hook-reset-2")
444
+ resetHook('hook-reset-2')
445
445
  expect(useC().count()).toBe(0)
446
446
  })
447
447
 
448
- it("resetAllHooks clears all singletons", () => {
449
- const useA = Counter.asHook("hook-all-1")
450
- const useB = Counter.asHook("hook-all-2")
448
+ it('resetAllHooks clears all singletons', () => {
449
+ const useA = Counter.asHook('hook-all-1')
450
+ const useB = Counter.asHook('hook-all-2')
451
451
  useA().add(5)
452
452
  useB().add(10)
453
453
 
@@ -460,8 +460,8 @@ describe("asHook — creates singleton hook", () => {
460
460
 
461
461
  // ─── Effect reactivity ─────────────────────────────────────────────────────
462
462
 
463
- describe("effect reactivity with model instances", () => {
464
- it("effect tracks signal reads from model instance", () => {
463
+ describe('effect reactivity with model instances', () => {
464
+ it('effect tracks signal reads from model instance', () => {
465
465
  const c = Counter.create()
466
466
  const observed: number[] = []
467
467
  effect(() => {
@@ -472,7 +472,7 @@ describe("effect reactivity with model instances", () => {
472
472
  expect(observed).toEqual([0, 1, 2])
473
473
  })
474
474
 
475
- it("effect tracks computed views", () => {
475
+ it('effect tracks computed views', () => {
476
476
  const c = Counter.create({ count: 1 })
477
477
  const observed: number[] = []
478
478
  effect(() => {