@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.
package/README.md CHANGED
@@ -11,8 +11,8 @@ bun add @pyreon/state-tree
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { model, getSnapshot, onPatch } from "@pyreon/state-tree"
15
- import { computed } from "@pyreon/reactivity"
14
+ import { model, getSnapshot, onPatch } from '@pyreon/state-tree'
15
+ import { computed } from '@pyreon/reactivity'
16
16
 
17
17
  const Counter = model({
18
18
  state: { count: 0 },
@@ -26,11 +26,11 @@ const Counter = model({
26
26
  })
27
27
 
28
28
  const counter = Counter.create({ count: 5 })
29
- counter.count() // 5
29
+ counter.count() // 5
30
30
  counter.inc()
31
- counter.doubled() // 12
31
+ counter.doubled() // 12
32
32
 
33
- getSnapshot(counter) // { count: 6 }
33
+ getSnapshot(counter) // { count: 6 }
34
34
  ```
35
35
 
36
36
  Models are defined once, then instantiated with `.create()` or used as singletons with `.asHook(id)`.
@@ -41,11 +41,11 @@ Models are defined once, then instantiated with `.create()` or used as singleton
41
41
 
42
42
  Define a reactive model. Returns a `ModelDefinition` with `.create()` and `.asHook()`.
43
43
 
44
- | Parameter | Type | Description |
45
- | --- | --- | --- |
46
- | `config.state` | `StateShape` | Plain object — each key becomes a `Signal<T>` on the instance |
47
- | `config.views` | `(self) => Record<string, Computed>` | Factory returning computed signals for derived state |
48
- | `config.actions` | `(self) => Record<string, Function>` | Factory returning functions that mutate state |
44
+ | Parameter | Type | Description |
45
+ | ---------------- | ------------------------------------ | ------------------------------------------------------------- |
46
+ | `config.state` | `StateShape` | Plain object — each key becomes a `Signal<T>` on the instance |
47
+ | `config.views` | `(self) => Record<string, Computed>` | Factory returning computed signals for derived state |
48
+ | `config.actions` | `(self) => Record<string, Function>` | Factory returning functions that mutate state |
49
49
 
50
50
  **Returns:** `ModelDefinition<TState, TActions, TViews>`
51
51
 
@@ -53,9 +53,9 @@ The `self` parameter in views and actions is strongly typed for state signals an
53
53
 
54
54
  ```ts
55
55
  const Todo = model({
56
- state: { text: "", done: false },
56
+ state: { text: '', done: false },
57
57
  views: (self) => ({
58
- summary: computed(() => `${self.done() ? "[x]" : "[ ]"} ${self.text()}`),
58
+ summary: computed(() => `${self.done() ? '[x]' : '[ ]'} ${self.text()}`),
59
59
  }),
60
60
  actions: (self) => ({
61
61
  toggle: () => self.done.update((d) => !d),
@@ -67,30 +67,30 @@ const Todo = model({
67
67
 
68
68
  Create an independent model instance, optionally overriding default state values.
69
69
 
70
- | Parameter | Type | Description |
71
- | --- | --- | --- |
70
+ | Parameter | Type | Description |
71
+ | --------- | --------------------------- | ------------------------------------- |
72
72
  | `initial` | `Partial<Snapshot<TState>>` | Partial snapshot to override defaults |
73
73
 
74
74
  **Returns:** `ModelInstance<TState, TActions, TViews>`
75
75
 
76
76
  ```ts
77
- const todo = Todo.create({ text: "Buy milk" })
78
- todo.text() // "Buy milk"
79
- todo.done() // false (default)
77
+ const todo = Todo.create({ text: 'Buy milk' })
78
+ todo.text() // "Buy milk"
79
+ todo.done() // false (default)
80
80
  ```
81
81
 
82
82
  ### `ModelDefinition.asHook(id)`
83
83
 
84
84
  Return a singleton hook function. Every call returns the same instance for the given ID.
85
85
 
86
- | Parameter | Type | Description |
87
- | --- | --- | --- |
88
- | `id` | `string` | Unique identifier for the singleton |
86
+ | Parameter | Type | Description |
87
+ | --------- | -------- | ----------------------------------- |
88
+ | `id` | `string` | Unique identifier for the singleton |
89
89
 
90
90
  **Returns:** `() => ModelInstance<TState, TActions, TViews>`
91
91
 
92
92
  ```ts
93
- const useCounter = Counter.asHook("app-counter")
93
+ const useCounter = Counter.asHook('app-counter')
94
94
  const a = useCounter()
95
95
  const b = useCounter()
96
96
  // a === b (same instance)
@@ -100,44 +100,44 @@ const b = useCounter()
100
100
 
101
101
  Serialize a model instance to a plain JS object. Nested model instances are recursively serialized.
102
102
 
103
- | Parameter | Type | Description |
104
- | --- | --- | --- |
103
+ | Parameter | Type | Description |
104
+ | ---------- | -------- | ------------------------------------------------------- |
105
105
  | `instance` | `object` | A model instance created via `.create()` or `.asHook()` |
106
106
 
107
107
  **Returns:** `Snapshot<TState>`
108
108
 
109
109
  ```ts
110
- getSnapshot(counter) // { count: 6 }
110
+ getSnapshot(counter) // { count: 6 }
111
111
  ```
112
112
 
113
113
  ### `applySnapshot(instance, snapshot)`
114
114
 
115
115
  Restore state from a plain object. Writes are batched for a single reactive flush. Missing keys are left unchanged.
116
116
 
117
- | Parameter | Type | Description |
118
- | --- | --- | --- |
119
- | `instance` | `object` | Target model instance |
117
+ | Parameter | Type | Description |
118
+ | ---------- | --------------------------- | --------------------------------- |
119
+ | `instance` | `object` | Target model instance |
120
120
  | `snapshot` | `Partial<Snapshot<TState>>` | Partial or full snapshot to apply |
121
121
 
122
122
  ```ts
123
123
  applySnapshot(counter, { count: 0 })
124
- counter.count() // 0
124
+ counter.count() // 0
125
125
  ```
126
126
 
127
127
  ### `onPatch(instance, listener)`
128
128
 
129
129
  Subscribe to state mutations as JSON patches. Returns an unsubscribe function.
130
130
 
131
- | Parameter | Type | Description |
132
- | --- | --- | --- |
133
- | `instance` | `object` | Model instance to observe |
131
+ | Parameter | Type | Description |
132
+ | ---------- | --------------- | ---------------------------------- |
133
+ | `instance` | `object` | Model instance to observe |
134
134
  | `listener` | `PatchListener` | Callback receiving `Patch` objects |
135
135
 
136
136
  **Returns:** `() => void` (unsubscribe)
137
137
 
138
138
  ```ts
139
139
  const unsub = onPatch(counter, (patch) => {
140
- console.log(patch) // { op: "replace", path: "/count", value: 7 }
140
+ console.log(patch) // { op: "replace", path: "/count", value: 7 }
141
141
  })
142
142
  counter.inc()
143
143
  unsub()
@@ -147,18 +147,18 @@ unsub()
147
147
 
148
148
  Apply a JSON patch (or array of patches) to a model instance. Only `"replace"` operations are supported. Multiple patches are batched.
149
149
 
150
- | Parameter | Type | Description |
151
- | --- | --- | --- |
152
- | `instance` | `object` | Target model instance |
153
- | `patch` | `Patch \| Patch[]` | Single patch or array of patches |
150
+ | Parameter | Type | Description |
151
+ | ---------- | ------------------ | -------------------------------- |
152
+ | `instance` | `object` | Target model instance |
153
+ | `patch` | `Patch \| Patch[]` | Single patch or array of patches |
154
154
 
155
155
  ```ts
156
- applyPatch(counter, { op: "replace", path: "/count", value: 10 })
156
+ applyPatch(counter, { op: 'replace', path: '/count', value: 10 })
157
157
 
158
158
  // Replay recorded patches (undo/redo, time-travel):
159
159
  applyPatch(counter, [
160
- { op: "replace", path: "/count", value: 1 },
161
- { op: "replace", path: "/count", value: 2 },
160
+ { op: 'replace', path: '/count', value: 1 },
161
+ { op: 'replace', path: '/count', value: 2 },
162
162
  ])
163
163
  ```
164
164
 
@@ -166,9 +166,9 @@ applyPatch(counter, [
166
166
 
167
167
  Intercept every action call. Middlewares run in registration order. Call `next(call)` to continue the chain.
168
168
 
169
- | Parameter | Type | Description |
170
- | --- | --- | --- |
171
- | `instance` | `object` | Model instance |
169
+ | Parameter | Type | Description |
170
+ | ------------ | -------------- | ------------------------- |
171
+ | `instance` | `object` | Model instance |
172
172
  | `middleware` | `MiddlewareFn` | `(call, next) => unknown` |
173
173
 
174
174
  **Returns:** `() => void` (unsubscribe)
@@ -186,13 +186,13 @@ const unsub = addMiddleware(counter, (call, next) => {
186
186
 
187
187
  Clear singleton instances created via `.asHook()`. Useful for testing and HMR.
188
188
 
189
- | Parameter | Type | Description |
190
- | --- | --- | --- |
191
- | `id` | `string` | Hook ID to reset (for `resetHook`) |
189
+ | Parameter | Type | Description |
190
+ | --------- | -------- | ---------------------------------- |
191
+ | `id` | `string` | Hook ID to reset (for `resetHook`) |
192
192
 
193
193
  ```ts
194
- resetHook("app-counter") // Clear one hook
195
- resetAllHooks() // Clear all hooks
194
+ resetHook('app-counter') // Clear one hook
195
+ resetAllHooks() // Clear all hooks
196
196
  ```
197
197
 
198
198
  ## Patterns
@@ -202,14 +202,14 @@ resetAllHooks() // Clear all hooks
202
202
  Use `ModelDefinition` values in `state` to compose models. Snapshots and patches resolve nested paths automatically.
203
203
 
204
204
  ```ts
205
- const Profile = model({ state: { name: "", age: 0 } })
205
+ const Profile = model({ state: { name: '', age: 0 } })
206
206
 
207
207
  const App = model({
208
- state: { title: "My App", profile: Profile },
208
+ state: { title: 'My App', profile: Profile },
209
209
  })
210
210
 
211
- const app = App.create({ title: "Hello", profile: { name: "Alice", age: 30 } })
212
- getSnapshot(app) // { title: "Hello", profile: { name: "Alice", age: 30 } }
211
+ const app = App.create({ title: 'Hello', profile: { name: 'Alice', age: 30 } })
212
+ getSnapshot(app) // { title: "Hello", profile: { name: "Alice", age: 30 } }
213
213
  ```
214
214
 
215
215
  ### Time-Travel Debugging
@@ -224,22 +224,22 @@ counter.inc()
224
224
  counter.inc()
225
225
 
226
226
  applySnapshot(counter, { count: 0 })
227
- applyPatch(counter, history) // replays to count: 2
227
+ applyPatch(counter, history) // replays to count: 2
228
228
  ```
229
229
 
230
230
  ## Types
231
231
 
232
- | Type | Description |
233
- | --- | --- |
234
- | `ModelDefinition` | Returned by `model()` — has `.create()` and `.asHook()` |
235
- | `ModelInstance` | The instance type: state signals + actions + views |
236
- | `ModelSelf` | The `self` type inside views/actions factories |
237
- | `StateShape` | `Record<string, unknown>` — the state config shape |
238
- | `Snapshot` | Recursive plain-object serialization of state |
239
- | `Patch` | `{ op: "replace", path: string, value: unknown }` |
240
- | `PatchListener` | `(patch: Patch) => void` |
241
- | `ActionCall` | `{ name: string, args: unknown[], path: string }` |
242
- | `MiddlewareFn` | `(call: ActionCall, next: (call: ActionCall) => unknown) => unknown` |
232
+ | Type | Description |
233
+ | ----------------- | -------------------------------------------------------------------- |
234
+ | `ModelDefinition` | Returned by `model()` — has `.create()` and `.asHook()` |
235
+ | `ModelInstance` | The instance type: state signals + actions + views |
236
+ | `ModelSelf` | The `self` type inside views/actions factories |
237
+ | `StateShape` | `Record<string, unknown>` — the state config shape |
238
+ | `Snapshot` | Recursive plain-object serialization of state |
239
+ | `Patch` | `{ op: "replace", path: string, value: unknown }` |
240
+ | `PatchListener` | `(patch: Patch) => void` |
241
+ | `ActionCall` | `{ name: string, args: unknown[], path: string }` |
242
+ | `MiddlewareFn` | `(call: ActionCall, next: (call: ActionCall) => unknown) => unknown` |
243
243
 
244
244
  ## Gotchas
245
245
 
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.js","names":[],"sources":["../src/registry.ts","../src/snapshot.ts","../src/devtools.ts"],"sourcesContent":["import type { InstanceMeta } from \"./types\"\n\n/**\n * WeakMap from every model instance object → its internal metadata.\n * Shared across patch, middleware, and snapshot modules.\n */\nexport const instanceMeta = new WeakMap<object, InstanceMeta>()\n\n/** Returns true when a value is a model instance (has metadata registered). */\nexport function isModelInstance(value: unknown): boolean {\n return value != null && typeof value === \"object\" && instanceMeta.has(value as object)\n}\n","import type { Signal } from \"@pyreon/reactivity\"\nimport { batch } from \"@pyreon/reactivity\"\nimport { instanceMeta, isModelInstance } from \"./registry\"\nimport type { Snapshot, StateShape } from \"./types\"\n\n// ─── getSnapshot ──────────────────────────────────────────────────────────────\n\n/**\n * Serialize a model instance to a plain JS object (no signals, no functions).\n * Nested model instances are recursively serialized.\n *\n * @example\n * getSnapshot(counter) // { count: 6 }\n * getSnapshot(app) // { profile: { name: \"Alice\" }, title: \"My App\" }\n */\nexport function getSnapshot<TState extends StateShape>(instance: object): Snapshot<TState> {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] getSnapshot: not a model instance\")\n\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? getSnapshot(val as object) : val\n }\n return out as Snapshot<TState>\n}\n\n// ─── applySnapshot ────────────────────────────────────────────────────────────\n\n/**\n * Restore a model instance from a plain-object snapshot.\n * All signal writes are coalesced via `batch()` for a single reactive flush.\n * Keys absent from the snapshot are left unchanged.\n *\n * @example\n * applySnapshot(counter, { count: 0 })\n */\nexport function applySnapshot<TState extends StateShape>(\n instance: object,\n snapshot: Partial<Snapshot<TState>>,\n): void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] applySnapshot: not a model instance\")\n\n batch(() => {\n for (const key of meta.stateKeys) {\n if (!(key in snapshot)) continue\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = (snapshot as Record<string, unknown>)[key]\n const current = sig.peek()\n if (isModelInstance(current)) {\n // Recurse into nested model instance\n applySnapshot(current as object, val as Record<string, unknown>)\n } else {\n sig.set(val)\n }\n }\n })\n}\n","/**\n * @pyreon/state-tree devtools introspection API.\n * Import: `import { ... } from \"@pyreon/state-tree/devtools\"`\n */\n\nimport { getSnapshot } from \"./snapshot\"\n\n// Track active model instances (devtools-only, opt-in)\nconst _activeModels = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register a model instance for devtools inspection.\n * Call this when creating instances you want visible in devtools.\n *\n * @example\n * const counter = Counter.create()\n * registerInstance(\"app-counter\", counter)\n */\nexport function registerInstance(name: string, instance: object): void {\n _activeModels.set(name, new WeakRef(instance))\n _notify()\n}\n\n/**\n * Unregister a model instance.\n */\nexport function unregisterInstance(name: string): void {\n _activeModels.delete(name)\n _notify()\n}\n\n/**\n * Get all registered model instance names.\n * Automatically cleans up garbage-collected instances.\n */\nexport function getActiveModels(): string[] {\n for (const [name, ref] of _activeModels) {\n if (ref.deref() === undefined) _activeModels.delete(name)\n }\n return [..._activeModels.keys()]\n}\n\n/**\n * Get a model instance by name (or undefined if GC'd or not registered).\n */\nexport function getModelInstance(name: string): object | undefined {\n const ref = _activeModels.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeModels.delete(name)\n return undefined\n }\n return instance\n}\n\n/**\n * Get a snapshot of a registered model instance.\n */\nexport function getModelSnapshot(name: string): Record<string, unknown> | undefined {\n const instance = getModelInstance(name)\n if (!instance) return undefined\n return getSnapshot(instance)\n}\n\n/**\n * Subscribe to model registry changes. Returns unsubscribe function.\n */\nexport function onModelChange(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n\n/** @internal — reset devtools registry (for tests). */\nexport function _resetDevtools(): void {\n _activeModels.clear()\n _listeners.clear()\n}\n"],"mappings":";;;;;AAMA,MAAa,+BAAe,IAAI,SAA+B;;AAG/D,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,SAAS,QAAQ,OAAO,UAAU,YAAY,aAAa,IAAI,MAAgB;;;;;;;;;;;;;ACKxF,SAAgB,YAAuC,UAAoC;CACzF,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,yDAAyD;CAEpF,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,KAAK,WAAW;EAChC,MAAM,MAAO,SAA6C;AAC1D,MAAI,CAAC,IAAK;EACV,MAAM,MAAM,IAAI,MAAM;AACtB,MAAI,OAAO,gBAAgB,IAAI,GAAG,YAAY,IAAc,GAAG;;AAEjE,QAAO;;;;;;;;;AClBT,MAAM,gCAAgB,IAAI,KAA8B;AACxD,MAAM,6BAAa,IAAI,KAAiB;AAExC,SAAS,UAAgB;AACvB,MAAK,MAAM,YAAY,WAAY,WAAU;;;;;;;;;;AAW/C,SAAgB,iBAAiB,MAAc,UAAwB;AACrE,eAAc,IAAI,MAAM,IAAI,QAAQ,SAAS,CAAC;AAC9C,UAAS;;;;;AAMX,SAAgB,mBAAmB,MAAoB;AACrD,eAAc,OAAO,KAAK;AAC1B,UAAS;;;;;;AAOX,SAAgB,kBAA4B;AAC1C,MAAK,MAAM,CAAC,MAAM,QAAQ,cACxB,KAAI,IAAI,OAAO,KAAK,OAAW,eAAc,OAAO,KAAK;AAE3D,QAAO,CAAC,GAAG,cAAc,MAAM,CAAC;;;;;AAMlC,SAAgB,iBAAiB,MAAkC;CACjE,MAAM,MAAM,cAAc,IAAI,KAAK;AACnC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,WAAW,IAAI,OAAO;AAC5B,KAAI,CAAC,UAAU;AACb,gBAAc,OAAO,KAAK;AAC1B;;AAEF,QAAO;;;;;AAMT,SAAgB,iBAAiB,MAAmD;CAClF,MAAM,WAAW,iBAAiB,KAAK;AACvC,KAAI,CAAC,SAAU,QAAO;AACtB,QAAO,YAAY,SAAS;;;;;AAM9B,SAAgB,cAAc,UAAkC;AAC9D,YAAW,IAAI,SAAS;AACxB,cAAa;AACX,aAAW,OAAO,SAAS;;;;AAK/B,SAAgB,iBAAuB;AACrC,eAAc,OAAO;AACrB,YAAW,OAAO"}
1
+ {"version":3,"file":"devtools.js","names":[],"sources":["../src/registry.ts","../src/snapshot.ts","../src/devtools.ts"],"sourcesContent":["import type { InstanceMeta } from './types'\n\n/**\n * WeakMap from every model instance object → its internal metadata.\n * Shared across patch, middleware, and snapshot modules.\n */\nexport const instanceMeta = new WeakMap<object, InstanceMeta>()\n\n/** Returns true when a value is a model instance (has metadata registered). */\nexport function isModelInstance(value: unknown): boolean {\n return value != null && typeof value === 'object' && instanceMeta.has(value as object)\n}\n","import type { Signal } from '@pyreon/reactivity'\nimport { batch } from '@pyreon/reactivity'\nimport { instanceMeta, isModelInstance } from './registry'\nimport type { Snapshot, StateShape } from './types'\n\n// ─── getSnapshot ──────────────────────────────────────────────────────────────\n\n/**\n * Serialize a model instance to a plain JS object (no signals, no functions).\n * Nested model instances are recursively serialized.\n *\n * @example\n * getSnapshot(counter) // { count: 6 }\n * getSnapshot(app) // { profile: { name: \"Alice\" }, title: \"My App\" }\n */\nexport function getSnapshot<TState extends StateShape>(instance: object): Snapshot<TState> {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] getSnapshot: not a model instance')\n\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? getSnapshot(val as object) : val\n }\n return out as Snapshot<TState>\n}\n\n// ─── applySnapshot ────────────────────────────────────────────────────────────\n\n/**\n * Restore a model instance from a plain-object snapshot.\n * All signal writes are coalesced via `batch()` for a single reactive flush.\n * Keys absent from the snapshot are left unchanged.\n *\n * @example\n * applySnapshot(counter, { count: 0 })\n */\nexport function applySnapshot<TState extends StateShape>(\n instance: object,\n snapshot: Partial<Snapshot<TState>>,\n): void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] applySnapshot: not a model instance')\n\n batch(() => {\n for (const key of meta.stateKeys) {\n if (!(key in snapshot)) continue\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = (snapshot as Record<string, unknown>)[key]\n const current = sig.peek()\n if (isModelInstance(current)) {\n // Recurse into nested model instance\n applySnapshot(current as object, val as Record<string, unknown>)\n } else {\n sig.set(val)\n }\n }\n })\n}\n","/**\n * @pyreon/state-tree devtools introspection API.\n * Import: `import { ... } from \"@pyreon/state-tree/devtools\"`\n */\n\nimport { getSnapshot } from './snapshot'\n\n// Track active model instances (devtools-only, opt-in)\nconst _activeModels = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register a model instance for devtools inspection.\n * Call this when creating instances you want visible in devtools.\n *\n * @example\n * const counter = Counter.create()\n * registerInstance(\"app-counter\", counter)\n */\nexport function registerInstance(name: string, instance: object): void {\n _activeModels.set(name, new WeakRef(instance))\n _notify()\n}\n\n/**\n * Unregister a model instance.\n */\nexport function unregisterInstance(name: string): void {\n _activeModels.delete(name)\n _notify()\n}\n\n/**\n * Get all registered model instance names.\n * Automatically cleans up garbage-collected instances.\n */\nexport function getActiveModels(): string[] {\n for (const [name, ref] of _activeModels) {\n if (ref.deref() === undefined) _activeModels.delete(name)\n }\n return [..._activeModels.keys()]\n}\n\n/**\n * Get a model instance by name (or undefined if GC'd or not registered).\n */\nexport function getModelInstance(name: string): object | undefined {\n const ref = _activeModels.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeModels.delete(name)\n return undefined\n }\n return instance\n}\n\n/**\n * Get a snapshot of a registered model instance.\n */\nexport function getModelSnapshot(name: string): Record<string, unknown> | undefined {\n const instance = getModelInstance(name)\n if (!instance) return undefined\n return getSnapshot(instance)\n}\n\n/**\n * Subscribe to model registry changes. Returns unsubscribe function.\n */\nexport function onModelChange(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n\n/** @internal — reset devtools registry (for tests). */\nexport function _resetDevtools(): void {\n _activeModels.clear()\n _listeners.clear()\n}\n"],"mappings":";;;;;AAMA,MAAa,+BAAe,IAAI,SAA+B;;AAG/D,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,SAAS,QAAQ,OAAO,UAAU,YAAY,aAAa,IAAI,MAAgB;;;;;;;;;;;;;ACKxF,SAAgB,YAAuC,UAAoC;CACzF,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,yDAAyD;CAEpF,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,KAAK,WAAW;EAChC,MAAM,MAAO,SAA6C;AAC1D,MAAI,CAAC,IAAK;EACV,MAAM,MAAM,IAAI,MAAM;AACtB,MAAI,OAAO,gBAAgB,IAAI,GAAG,YAAY,IAAc,GAAG;;AAEjE,QAAO;;;;;;;;;AClBT,MAAM,gCAAgB,IAAI,KAA8B;AACxD,MAAM,6BAAa,IAAI,KAAiB;AAExC,SAAS,UAAgB;AACvB,MAAK,MAAM,YAAY,WAAY,WAAU;;;;;;;;;;AAW/C,SAAgB,iBAAiB,MAAc,UAAwB;AACrE,eAAc,IAAI,MAAM,IAAI,QAAQ,SAAS,CAAC;AAC9C,UAAS;;;;;AAMX,SAAgB,mBAAmB,MAAoB;AACrD,eAAc,OAAO,KAAK;AAC1B,UAAS;;;;;;AAOX,SAAgB,kBAA4B;AAC1C,MAAK,MAAM,CAAC,MAAM,QAAQ,cACxB,KAAI,IAAI,OAAO,KAAK,OAAW,eAAc,OAAO,KAAK;AAE3D,QAAO,CAAC,GAAG,cAAc,MAAM,CAAC;;;;;AAMlC,SAAgB,iBAAiB,MAAkC;CACjE,MAAM,MAAM,cAAc,IAAI,KAAK;AACnC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,WAAW,IAAI,OAAO;AAC5B,KAAI,CAAC,UAAU;AACb,gBAAc,OAAO,KAAK;AAC1B;;AAEF,QAAO;;;;;AAMT,SAAgB,iBAAiB,MAAmD;CAClF,MAAM,WAAW,iBAAiB,KAAK;AACvC,KAAI,CAAC,SAAU,QAAO;AACtB,QAAO,YAAY,SAAS;;;;;AAM9B,SAAgB,cAAc,UAAkC;AAC9D,YAAW,IAAI,SAAS;AACxB,cAAa;AACX,aAAW,OAAO,SAAS;;;;AAK/B,SAAgB,iBAAuB;AACrC,eAAc,OAAO;AACrB,YAAW,OAAO"}
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/registry.ts","../src/middleware.ts","../src/patch.ts","../src/types.ts","../src/instance.ts","../src/model.ts","../src/snapshot.ts"],"sourcesContent":["import type { InstanceMeta } from \"./types\"\n\n/**\n * WeakMap from every model instance object → its internal metadata.\n * Shared across patch, middleware, and snapshot modules.\n */\nexport const instanceMeta = new WeakMap<object, InstanceMeta>()\n\n/** Returns true when a value is a model instance (has metadata registered). */\nexport function isModelInstance(value: unknown): boolean {\n return value != null && typeof value === \"object\" && instanceMeta.has(value as object)\n}\n","import { instanceMeta } from \"./registry\"\nimport type { ActionCall, InstanceMeta, MiddlewareFn } from \"./types\"\n\n// ─── Action runner ────────────────────────────────────────────────────────────\n\n/**\n * Run an action through the middleware chain registered on `meta`.\n * Each middleware receives the call descriptor and a `next` function.\n * If no middlewares, the action runs directly.\n */\nexport function runAction(\n meta: InstanceMeta,\n name: string,\n fn: (...fnArgs: unknown[]) => unknown,\n args: unknown[],\n): unknown {\n const call: ActionCall = { name, args, path: `/${name}` }\n\n const dispatch = (idx: number, c: ActionCall): unknown => {\n if (idx >= meta.middlewares.length) return fn(...c.args)\n const mw = meta.middlewares[idx]\n if (!mw) return fn(...c.args)\n return mw(c, (nextCall) => dispatch(idx + 1, nextCall))\n }\n\n return dispatch(0, call)\n}\n\n// ─── addMiddleware ────────────────────────────────────────────────────────────\n\n/**\n * Intercept every action call on `instance`.\n * Middlewares run in registration order — call `next(call)` to continue.\n *\n * Returns an unsubscribe function.\n *\n * @example\n * const unsub = addMiddleware(counter, (call, next) => {\n * console.log(`> ${call.name}(${call.args})`)\n * const result = next(call)\n * console.log(`< ${call.name}`)\n * return result\n * })\n */\nexport function addMiddleware(instance: object, middleware: MiddlewareFn): () => void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] addMiddleware: not a model instance\")\n meta.middlewares.push(middleware)\n return () => {\n const idx = meta.middlewares.indexOf(middleware)\n if (idx !== -1) meta.middlewares.splice(idx, 1)\n }\n}\n","import type { Signal } from \"@pyreon/reactivity\"\nimport { batch } from \"@pyreon/reactivity\"\nimport { instanceMeta, isModelInstance } from \"./registry\"\nimport type { Patch, PatchListener } from \"./types\"\n\n/** Property names that must never be used as patch path segments. */\nconst RESERVED_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"])\n\n// ─── Tracked signal ───────────────────────────────────────────────────────────\n\n/**\n * Wraps a signal so that every write emits a JSON patch via `emitPatch`.\n * Reads are pass-through — no overhead on hot reactive paths.\n *\n * @param hasListeners Optional predicate — when provided, patch object allocation\n * and snapshotting are skipped entirely when no listeners are registered.\n */\nexport function trackedSignal<T>(\n inner: Signal<T>,\n path: string,\n emitPatch: (patch: Patch) => void,\n hasListeners?: () => boolean,\n): Signal<T> {\n const read = (): T => inner()\n\n read.peek = (): T => inner.peek()\n\n read.subscribe = (listener: () => void): (() => void) => inner.subscribe(listener)\n\n read.set = (newValue: T): void => {\n const prev = inner.peek()\n inner.set(newValue)\n // Skip patch emission entirely when no one is listening — avoids object\n // allocation and (for nested instances) a full recursive snapshot.\n if (!Object.is(prev, newValue) && (!hasListeners || hasListeners())) {\n // For model instances, emit the snapshot rather than the live object\n // so patches are always plain JSON-serializable values.\n const patchValue = isModelInstance(newValue) ? snapshotValue(newValue as object) : newValue\n emitPatch({ op: \"replace\", path, value: patchValue })\n }\n }\n\n read.update = (fn: (current: T) => T): void => {\n read.set(fn(inner.peek()))\n }\n\n return read as Signal<T>\n}\n\n/** Shallow snapshot helper (avoids importing snapshot.ts to prevent circular deps). */\nfunction snapshotValue(instance: object): Record<string, unknown> {\n const meta = instanceMeta.get(instance)\n if (!meta) return instance as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? snapshotValue(val as object) : val\n }\n return out\n}\n\n// ─── onPatch ──────────────────────────────────────────────────────────────────\n\n/**\n * Subscribe to every state mutation in `instance` as a JSON patch.\n * Also captures mutations in nested model instances (path is prefixed).\n *\n * Returns an unsubscribe function.\n *\n * @example\n * const unsub = onPatch(counter, patch => {\n * // { op: \"replace\", path: \"/count\", value: 6 }\n * })\n */\nexport function onPatch(instance: object, listener: PatchListener): () => void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] onPatch: not a model instance\")\n meta.patchListeners.add(listener)\n return () => meta.patchListeners.delete(listener)\n}\n\n// ─── applyPatch ─────────────────────────────────────────────────────────────\n\n/**\n * Apply a JSON patch (or array of patches) to a model instance.\n * Only \"replace\" operations are supported (matching the patches emitted by `onPatch`).\n *\n * Paths use JSON pointer format: `\"/count\"` for top-level, `\"/profile/name\"` for nested.\n * Nested model instances are resolved automatically.\n *\n * @example\n * applyPatch(counter, { op: \"replace\", path: \"/count\", value: 10 })\n *\n * @example\n * // Replay patches recorded from onPatch (undo/redo, time-travel)\n * applyPatch(counter, [\n * { op: \"replace\", path: \"/count\", value: 1 },\n * { op: \"replace\", path: \"/count\", value: 2 },\n * ])\n */\nexport function applyPatch(instance: object, patch: Patch | Patch[]): void {\n const patches = Array.isArray(patch) ? patch : [patch]\n\n batch(() => {\n for (const p of patches) {\n if (p.op !== \"replace\") {\n throw new Error(`[@pyreon/state-tree] applyPatch: unsupported op \"${p.op}\"`)\n }\n\n const segments = p.path.split(\"/\").filter(Boolean)\n if (segments.length === 0) {\n throw new Error(\"[@pyreon/state-tree] applyPatch: empty path\")\n }\n\n // Walk to the target instance for nested paths\n let target: object = instance\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!\n if (RESERVED_KEYS.has(segment)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: reserved property name \"${segment}\"`)\n }\n const meta = instanceMeta.get(target)\n if (!meta)\n throw new Error(`[@pyreon/state-tree] applyPatch: not a model instance at \"${segment}\"`)\n const sig = (target as Record<string, Signal<unknown>>)[segment]\n if (!sig || typeof sig.peek !== \"function\") {\n throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key \"${segment}\"`)\n }\n const nested = sig.peek()\n if (!nested || typeof nested !== \"object\" || !isModelInstance(nested)) {\n throw new Error(\n `[@pyreon/state-tree] applyPatch: \"${segment}\" is not a nested model instance`,\n )\n }\n target = nested as object\n }\n\n const lastKey = segments[segments.length - 1]!\n if (RESERVED_KEYS.has(lastKey)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: reserved property name \"${lastKey}\"`)\n }\n const meta = instanceMeta.get(target)\n if (!meta) throw new Error(\"[@pyreon/state-tree] applyPatch: not a model instance\")\n if (!meta.stateKeys.includes(lastKey)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key \"${lastKey}\"`)\n }\n\n const sig = (target as Record<string, Signal<unknown>>)[lastKey]\n if (sig && typeof sig.set === \"function\") {\n sig.set(p.value)\n }\n }\n })\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\n\n// ─── Model brand ──────────────────────────────────────────────────────────────\n\n/** Property key stamped on every ModelDefinition to distinguish it from plain objects. */\nexport const MODEL_BRAND = \"__pyreonMod\" as const\n\n// ─── State type helpers ───────────────────────────────────────────────────────\n\nexport type StateShape = Record<string, unknown>\n\n/**\n * Resolve a state field type:\n * - ModelDefinition → the instance type it produces\n * - Anything else → as-is\n */\nexport type ResolveField<T> = T extends {\n readonly __pyreonMod: true\n create(initial?: any): infer I\n}\n ? I\n : T\n\n/** Map state shape to per-field signals. */\nexport type StateSignals<TState extends StateShape> = {\n readonly [K in keyof TState]: Signal<ResolveField<TState[K]>>\n}\n\n/**\n * `self` type inside actions / views:\n * strongly typed for state signals, `any` for actions and views so that\n * actions can call each other without circular type issues.\n */\nexport type ModelSelf<TState extends StateShape> = StateSignals<TState> & Record<string, any>\n\n/** The public instance type returned by `.create()` and hooks. */\nexport type ModelInstance<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n> = StateSignals<TState> & TActions & TViews\n\n/**\n * Extract the state type from a ModelDefinition.\n * Used by Snapshot to recursively resolve nested model types.\n */\ntype ExtractModelState<T> = T extends {\n readonly __pyreonMod: true\n readonly _config: { state: infer S extends StateShape }\n}\n ? S\n : never\n\n/**\n * Snapshot type: plain JS values (no signals, no model instances).\n * Nested model fields recursively produce their own typed snapshot.\n */\nexport type Snapshot<TState extends StateShape> = {\n [K in keyof TState]: TState[K] extends { readonly __pyreonMod: true }\n ? Snapshot<ExtractModelState<TState[K]>>\n : TState[K]\n}\n\n// ─── Patch ────────────────────────────────────────────────────────────────────\n\nexport interface Patch {\n op: \"replace\"\n path: string\n value: unknown\n}\n\nexport type PatchListener = (patch: Patch) => void\n\n// ─── Middleware ───────────────────────────────────────────────────────────────\n\nexport interface ActionCall {\n /** Action name. */\n name: string\n /** Arguments passed to the action. */\n args: unknown[]\n /** JSON-pointer-style path, e.g. `\"/inc\"`. */\n path: string\n}\n\nexport type MiddlewareFn = (call: ActionCall, next: (nextCall: ActionCall) => unknown) => unknown\n\n// ─── Instance metadata ────────────────────────────────────────────────────────\n\nexport interface InstanceMeta {\n stateKeys: string[]\n patchListeners: Set<PatchListener>\n middlewares: MiddlewareFn[]\n emitPatch(patch: Patch): void\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\nimport { signal } from \"@pyreon/reactivity\"\nimport { runAction } from \"./middleware\"\nimport { onPatch, trackedSignal } from \"./patch\"\nimport { instanceMeta } from \"./registry\"\nimport type { InstanceMeta, ModelInstance, Snapshot, StateShape } from \"./types\"\nimport { MODEL_BRAND } from \"./types\"\n\n// ─── Model definition detection ───────────────────────────────────────────────\n\ninterface AnyModelDef {\n readonly [MODEL_BRAND]: true\n readonly _config: ModelConfig<\n StateShape,\n Record<string, (...args: unknown[]) => unknown>,\n Record<string, Signal<unknown>>\n >\n}\n\nfunction isModelDef(v: unknown): v is AnyModelDef {\n if (v == null || typeof v !== \"object\") return false\n return (v as Record<string, unknown>)[MODEL_BRAND] === true\n}\n\n// ─── Config shape ─────────────────────────────────────────────────────────────\n\nexport interface ModelConfig<TState extends StateShape, TActions, TViews> {\n state: TState\n views?: (self: any) => TViews\n actions?: (self: any) => TActions\n}\n\n// ─── createInstance ───────────────────────────────────────────────────────────\n\n/**\n * Create a live model instance from a config + optional initial snapshot.\n * Called by `ModelDefinition.create()`.\n */\nexport function createInstance<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n>(\n config: ModelConfig<TState, TActions, TViews>,\n initial: Partial<Snapshot<TState>>,\n): ModelInstance<TState, TActions, TViews> {\n // Raw object that will become the instance.\n const instance: Record<string, unknown> = {}\n\n // Metadata for this instance.\n const meta: InstanceMeta = {\n stateKeys: [],\n patchListeners: new Set(),\n middlewares: [],\n emitPatch(patch) {\n // Guard avoids iterating an empty Set on the hot signal-write path.\n if (this.patchListeners.size === 0) return\n for (const listener of this.patchListeners) listener(patch)\n },\n }\n instanceMeta.set(instance, meta)\n\n // `self` is a live proxy so that actions/views always see the final\n // (fully-populated) instance — including wrapped actions added later.\n const self = new Proxy(instance, {\n get(_, k) {\n return instance[k as string]\n },\n })\n\n // ── 1. State signals ──────────────────────────────────────────────────────\n for (const [key, defaultValue] of Object.entries(config.state)) {\n meta.stateKeys.push(key)\n const path = `/${key}`\n const initValue: unknown =\n key in initial ? (initial as Record<string, unknown>)[key] : undefined\n\n let rawSig: Signal<unknown>\n\n if (isModelDef(defaultValue)) {\n // Nested model — create its instance from the supplied snapshot (or defaults).\n const nestedInstance = createInstance(\n defaultValue._config,\n (initValue as Record<string, unknown>) ?? {},\n )\n rawSig = signal(nestedInstance)\n\n // Propagate nested patches upward with the key as path prefix.\n onPatch(nestedInstance, (patch) => {\n meta.emitPatch({ ...patch, path: path + patch.path })\n })\n } else {\n rawSig = signal(initValue !== undefined ? initValue : defaultValue)\n }\n\n const tracked = trackedSignal(\n rawSig,\n path,\n (p) => meta.emitPatch(p),\n () => meta.patchListeners.size > 0,\n )\n instance[key] = tracked\n }\n\n // ── 2. Views ──────────────────────────────────────────────────────────────\n if (config.views) {\n const views = config.views(self)\n for (const [key, view] of Object.entries(views as Record<string, unknown>)) {\n instance[key] = view\n }\n }\n\n // ── 3. Actions (wrapped with middleware runner) ───────────────────────────\n if (config.actions) {\n const rawActions = config.actions(self) as Record<string, (...args: unknown[]) => unknown>\n for (const [key, actionFn] of Object.entries(rawActions)) {\n instance[key] = (...args: unknown[]) => runAction(meta, key, actionFn, args)\n }\n }\n\n return instance as ModelInstance<TState, TActions, TViews>\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\nimport { createInstance, type ModelConfig } from \"./instance\"\nimport type { ModelInstance, Snapshot, StateShape } from \"./types\"\nimport { MODEL_BRAND } from \"./types\"\n\n// ─── Hook registry ────────────────────────────────────────────────────────────\n\n// Module-level singleton registry for `asHook()` — isolated per package import.\n// Use `resetHook(id)` or `resetAllHooks()` to clear entries (useful for tests / HMR).\nconst _hookRegistry = new Map<string, unknown>()\n\n/** Destroy a hook singleton by id so next call re-creates the instance. */\nexport function resetHook(id: string): void {\n _hookRegistry.delete(id)\n}\n\n/** Destroy all hook singletons. */\nexport function resetAllHooks(): void {\n _hookRegistry.clear()\n}\n\n// ─── ModelDefinition ──────────────────────────────────────────────────────────\n\n/**\n * Returned by `model()`. Call `.create()` for instances or `.asHook(id)` for\n * a Zustand-style singleton hook.\n */\nexport class ModelDefinition<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n> {\n /** Brand used to identify ModelDefinition objects at runtime (without instanceof). */\n readonly [MODEL_BRAND] = true as const\n\n /** @internal — exposed so nested instance creation can read it. */\n readonly _config: ModelConfig<TState, TActions, TViews>\n\n constructor(config: ModelConfig<TState, TActions, TViews>) {\n this._config = config\n }\n\n /**\n * Create a new independent model instance.\n * Pass a partial snapshot to override defaults.\n *\n * @example\n * const counter = Counter.create({ count: 5 })\n */\n create(initial?: Partial<Snapshot<TState>>): ModelInstance<TState, TActions, TViews> {\n return createInstance(this._config, initial ?? {})\n }\n\n /**\n * Returns a hook function that always returns the same singleton instance\n * for the given `id` — Zustand / Pinia style.\n *\n * @example\n * const useCounter = Counter.asHook(\"app-counter\")\n * // Any call to useCounter() returns the same instance.\n * const store = useCounter()\n */\n asHook(id: string): () => ModelInstance<TState, TActions, TViews> {\n return () => {\n if (!_hookRegistry.has(id)) {\n _hookRegistry.set(id, this.create())\n }\n return _hookRegistry.get(id) as ModelInstance<TState, TActions, TViews>\n }\n }\n}\n\n// ─── model() factory ──────────────────────────────────────────────────────────\n\n/**\n * Define a reactive model with state, views, and actions.\n *\n * - **state** — plain JS object; each key becomes a `Signal<T>` on the instance.\n * - **views** — factory receiving `self`; return computed signals for derived state.\n * - **actions** — factory receiving `self`; return functions that mutate state.\n *\n * Use nested `ModelDefinition` values in `state` to compose models.\n *\n * @example\n * const Counter = model({\n * state: { count: 0 },\n * views: (self) => ({\n * doubled: computed(() => self.count() * 2),\n * }),\n * actions: (self) => ({\n * inc: () => self.count.update(c => c + 1),\n * reset: () => self.count.set(0),\n * }),\n * })\n *\n * const c = Counter.create({ count: 5 })\n * c.count() // 5\n * c.inc()\n * c.doubled() // 12\n */\nexport function model<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any> = Record<never, never>,\n TViews extends Record<string, Signal<any> | Computed<any>> = Record<never, never>,\n>(config: ModelConfig<TState, TActions, TViews>): ModelDefinition<TState, TActions, TViews> {\n return new ModelDefinition(config)\n}\n","import type { Signal } from \"@pyreon/reactivity\"\nimport { batch } from \"@pyreon/reactivity\"\nimport { instanceMeta, isModelInstance } from \"./registry\"\nimport type { Snapshot, StateShape } from \"./types\"\n\n// ─── getSnapshot ──────────────────────────────────────────────────────────────\n\n/**\n * Serialize a model instance to a plain JS object (no signals, no functions).\n * Nested model instances are recursively serialized.\n *\n * @example\n * getSnapshot(counter) // { count: 6 }\n * getSnapshot(app) // { profile: { name: \"Alice\" }, title: \"My App\" }\n */\nexport function getSnapshot<TState extends StateShape>(instance: object): Snapshot<TState> {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] getSnapshot: not a model instance\")\n\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? getSnapshot(val as object) : val\n }\n return out as Snapshot<TState>\n}\n\n// ─── applySnapshot ────────────────────────────────────────────────────────────\n\n/**\n * Restore a model instance from a plain-object snapshot.\n * All signal writes are coalesced via `batch()` for a single reactive flush.\n * Keys absent from the snapshot are left unchanged.\n *\n * @example\n * applySnapshot(counter, { count: 0 })\n */\nexport function applySnapshot<TState extends StateShape>(\n instance: object,\n snapshot: Partial<Snapshot<TState>>,\n): void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error(\"[@pyreon/state-tree] applySnapshot: not a model instance\")\n\n batch(() => {\n for (const key of meta.stateKeys) {\n if (!(key in snapshot)) continue\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = (snapshot as Record<string, unknown>)[key]\n const current = sig.peek()\n if (isModelInstance(current)) {\n // Recurse into nested model instance\n applySnapshot(current as object, val as Record<string, unknown>)\n } else {\n sig.set(val)\n }\n }\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/registry.ts","../src/middleware.ts","../src/patch.ts","../src/types.ts","../src/instance.ts","../src/model.ts","../src/snapshot.ts"],"sourcesContent":["import type { InstanceMeta } from './types'\n\n/**\n * WeakMap from every model instance object → its internal metadata.\n * Shared across patch, middleware, and snapshot modules.\n */\nexport const instanceMeta = new WeakMap<object, InstanceMeta>()\n\n/** Returns true when a value is a model instance (has metadata registered). */\nexport function isModelInstance(value: unknown): boolean {\n return value != null && typeof value === 'object' && instanceMeta.has(value as object)\n}\n","import { instanceMeta } from './registry'\nimport type { ActionCall, InstanceMeta, MiddlewareFn } from './types'\n\n// ─── Action runner ────────────────────────────────────────────────────────────\n\n/**\n * Run an action through the middleware chain registered on `meta`.\n * Each middleware receives the call descriptor and a `next` function.\n * If no middlewares, the action runs directly.\n */\nexport function runAction(\n meta: InstanceMeta,\n name: string,\n fn: (...fnArgs: unknown[]) => unknown,\n args: unknown[],\n): unknown {\n const call: ActionCall = { name, args, path: `/${name}` }\n\n const dispatch = (idx: number, c: ActionCall): unknown => {\n if (idx >= meta.middlewares.length) return fn(...c.args)\n const mw = meta.middlewares[idx]\n if (!mw) return fn(...c.args)\n return mw(c, (nextCall) => dispatch(idx + 1, nextCall))\n }\n\n return dispatch(0, call)\n}\n\n// ─── addMiddleware ────────────────────────────────────────────────────────────\n\n/**\n * Intercept every action call on `instance`.\n * Middlewares run in registration order — call `next(call)` to continue.\n *\n * Returns an unsubscribe function.\n *\n * @example\n * const unsub = addMiddleware(counter, (call, next) => {\n * console.log(`> ${call.name}(${call.args})`)\n * const result = next(call)\n * console.log(`< ${call.name}`)\n * return result\n * })\n */\nexport function addMiddleware(instance: object, middleware: MiddlewareFn): () => void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] addMiddleware: not a model instance')\n meta.middlewares.push(middleware)\n return () => {\n const idx = meta.middlewares.indexOf(middleware)\n if (idx !== -1) meta.middlewares.splice(idx, 1)\n }\n}\n","import type { Signal } from '@pyreon/reactivity'\nimport { batch } from '@pyreon/reactivity'\nimport { instanceMeta, isModelInstance } from './registry'\nimport type { Patch, PatchListener } from './types'\n\n/** Property names that must never be used as patch path segments. */\nconst RESERVED_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n// ─── Tracked signal ───────────────────────────────────────────────────────────\n\n/**\n * Wraps a signal so that every write emits a JSON patch via `emitPatch`.\n * Reads are pass-through — no overhead on hot reactive paths.\n *\n * @param hasListeners Optional predicate — when provided, patch object allocation\n * and snapshotting are skipped entirely when no listeners are registered.\n */\nexport function trackedSignal<T>(\n inner: Signal<T>,\n path: string,\n emitPatch: (patch: Patch) => void,\n hasListeners?: () => boolean,\n): Signal<T> {\n const read = (): T => inner()\n\n read.peek = (): T => inner.peek()\n\n read.subscribe = (listener: () => void): (() => void) => inner.subscribe(listener)\n\n read.set = (newValue: T): void => {\n const prev = inner.peek()\n inner.set(newValue)\n // Skip patch emission entirely when no one is listening — avoids object\n // allocation and (for nested instances) a full recursive snapshot.\n if (!Object.is(prev, newValue) && (!hasListeners || hasListeners())) {\n // For model instances, emit the snapshot rather than the live object\n // so patches are always plain JSON-serializable values.\n const patchValue = isModelInstance(newValue) ? snapshotValue(newValue as object) : newValue\n emitPatch({ op: 'replace', path, value: patchValue })\n }\n }\n\n read.update = (fn: (current: T) => T): void => {\n read.set(fn(inner.peek()))\n }\n\n return read as Signal<T>\n}\n\n/** Shallow snapshot helper (avoids importing snapshot.ts to prevent circular deps). */\nfunction snapshotValue(instance: object): Record<string, unknown> {\n const meta = instanceMeta.get(instance)\n if (!meta) return instance as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? snapshotValue(val as object) : val\n }\n return out\n}\n\n// ─── onPatch ──────────────────────────────────────────────────────────────────\n\n/**\n * Subscribe to every state mutation in `instance` as a JSON patch.\n * Also captures mutations in nested model instances (path is prefixed).\n *\n * Returns an unsubscribe function.\n *\n * @example\n * const unsub = onPatch(counter, patch => {\n * // { op: \"replace\", path: \"/count\", value: 6 }\n * })\n */\nexport function onPatch(instance: object, listener: PatchListener): () => void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] onPatch: not a model instance')\n meta.patchListeners.add(listener)\n return () => meta.patchListeners.delete(listener)\n}\n\n// ─── applyPatch ─────────────────────────────────────────────────────────────\n\n/**\n * Apply a JSON patch (or array of patches) to a model instance.\n * Only \"replace\" operations are supported (matching the patches emitted by `onPatch`).\n *\n * Paths use JSON pointer format: `\"/count\"` for top-level, `\"/profile/name\"` for nested.\n * Nested model instances are resolved automatically.\n *\n * @example\n * applyPatch(counter, { op: \"replace\", path: \"/count\", value: 10 })\n *\n * @example\n * // Replay patches recorded from onPatch (undo/redo, time-travel)\n * applyPatch(counter, [\n * { op: \"replace\", path: \"/count\", value: 1 },\n * { op: \"replace\", path: \"/count\", value: 2 },\n * ])\n */\nexport function applyPatch(instance: object, patch: Patch | Patch[]): void {\n const patches = Array.isArray(patch) ? patch : [patch]\n\n batch(() => {\n for (const p of patches) {\n if (p.op !== 'replace') {\n throw new Error(`[@pyreon/state-tree] applyPatch: unsupported op \"${p.op}\"`)\n }\n\n const segments = p.path.split('/').filter(Boolean)\n if (segments.length === 0) {\n throw new Error('[@pyreon/state-tree] applyPatch: empty path')\n }\n\n // Walk to the target instance for nested paths\n let target: object = instance\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i]!\n if (RESERVED_KEYS.has(segment)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: reserved property name \"${segment}\"`)\n }\n const meta = instanceMeta.get(target)\n if (!meta)\n throw new Error(`[@pyreon/state-tree] applyPatch: not a model instance at \"${segment}\"`)\n const sig = (target as Record<string, Signal<unknown>>)[segment]\n if (!sig || typeof sig.peek !== 'function') {\n throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key \"${segment}\"`)\n }\n const nested = sig.peek()\n if (!nested || typeof nested !== 'object' || !isModelInstance(nested)) {\n throw new Error(\n `[@pyreon/state-tree] applyPatch: \"${segment}\" is not a nested model instance`,\n )\n }\n target = nested as object\n }\n\n const lastKey = segments[segments.length - 1]!\n if (RESERVED_KEYS.has(lastKey)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: reserved property name \"${lastKey}\"`)\n }\n const meta = instanceMeta.get(target)\n if (!meta) throw new Error('[@pyreon/state-tree] applyPatch: not a model instance')\n if (!meta.stateKeys.includes(lastKey)) {\n throw new Error(`[@pyreon/state-tree] applyPatch: unknown state key \"${lastKey}\"`)\n }\n\n const sig = (target as Record<string, Signal<unknown>>)[lastKey]\n if (sig && typeof sig.set === 'function') {\n sig.set(p.value)\n }\n }\n })\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\n\n// ─── Model brand ──────────────────────────────────────────────────────────────\n\n/** Property key stamped on every ModelDefinition to distinguish it from plain objects. */\nexport const MODEL_BRAND = '__pyreonMod' as const\n\n// ─── State type helpers ───────────────────────────────────────────────────────\n\nexport type StateShape = Record<string, unknown>\n\n/**\n * Resolve a state field type:\n * - ModelDefinition → the instance type it produces\n * - Anything else → as-is\n */\nexport type ResolveField<T> = T extends {\n readonly __pyreonMod: true\n create(initial?: any): infer I\n}\n ? I\n : T\n\n/** Map state shape to per-field signals. */\nexport type StateSignals<TState extends StateShape> = {\n readonly [K in keyof TState]: Signal<ResolveField<TState[K]>>\n}\n\n/**\n * `self` type inside actions / views:\n * strongly typed for state signals, `any` for actions and views so that\n * actions can call each other without circular type issues.\n */\nexport type ModelSelf<TState extends StateShape> = StateSignals<TState> & Record<string, any>\n\n/** The public instance type returned by `.create()` and hooks. */\nexport type ModelInstance<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n> = StateSignals<TState> & TActions & TViews\n\n/**\n * Extract the state type from a ModelDefinition.\n * Used by Snapshot to recursively resolve nested model types.\n */\ntype ExtractModelState<T> = T extends {\n readonly __pyreonMod: true\n readonly _config: { state: infer S extends StateShape }\n}\n ? S\n : never\n\n/**\n * Snapshot type: plain JS values (no signals, no model instances).\n * Nested model fields recursively produce their own typed snapshot.\n */\nexport type Snapshot<TState extends StateShape> = {\n [K in keyof TState]: TState[K] extends { readonly __pyreonMod: true }\n ? Snapshot<ExtractModelState<TState[K]>>\n : TState[K]\n}\n\n// ─── Patch ────────────────────────────────────────────────────────────────────\n\nexport interface Patch {\n op: 'replace'\n path: string\n value: unknown\n}\n\nexport type PatchListener = (patch: Patch) => void\n\n// ─── Middleware ───────────────────────────────────────────────────────────────\n\nexport interface ActionCall {\n /** Action name. */\n name: string\n /** Arguments passed to the action. */\n args: unknown[]\n /** JSON-pointer-style path, e.g. `\"/inc\"`. */\n path: string\n}\n\nexport type MiddlewareFn = (call: ActionCall, next: (nextCall: ActionCall) => unknown) => unknown\n\n// ─── Instance metadata ────────────────────────────────────────────────────────\n\nexport interface InstanceMeta {\n stateKeys: string[]\n patchListeners: Set<PatchListener>\n middlewares: MiddlewareFn[]\n emitPatch(patch: Patch): void\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\nimport { signal } from '@pyreon/reactivity'\nimport { runAction } from './middleware'\nimport { onPatch, trackedSignal } from './patch'\nimport { instanceMeta } from './registry'\nimport type { InstanceMeta, ModelInstance, Snapshot, StateShape } from './types'\nimport { MODEL_BRAND } from './types'\n\n// ─── Model definition detection ───────────────────────────────────────────────\n\ninterface AnyModelDef {\n readonly [MODEL_BRAND]: true\n readonly _config: ModelConfig<\n StateShape,\n Record<string, (...args: unknown[]) => unknown>,\n Record<string, Signal<unknown>>\n >\n}\n\nfunction isModelDef(v: unknown): v is AnyModelDef {\n if (v == null || typeof v !== 'object') return false\n return (v as Record<string, unknown>)[MODEL_BRAND] === true\n}\n\n// ─── Config shape ─────────────────────────────────────────────────────────────\n\nexport interface ModelConfig<TState extends StateShape, TActions, TViews> {\n state: TState\n views?: (self: any) => TViews\n actions?: (self: any) => TActions\n}\n\n// ─── createInstance ───────────────────────────────────────────────────────────\n\n/**\n * Create a live model instance from a config + optional initial snapshot.\n * Called by `ModelDefinition.create()`.\n */\nexport function createInstance<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n>(\n config: ModelConfig<TState, TActions, TViews>,\n initial: Partial<Snapshot<TState>>,\n): ModelInstance<TState, TActions, TViews> {\n // Raw object that will become the instance.\n const instance: Record<string, unknown> = {}\n\n // Metadata for this instance.\n const meta: InstanceMeta = {\n stateKeys: [],\n patchListeners: new Set(),\n middlewares: [],\n emitPatch(patch) {\n // Guard avoids iterating an empty Set on the hot signal-write path.\n if (this.patchListeners.size === 0) return\n for (const listener of this.patchListeners) listener(patch)\n },\n }\n instanceMeta.set(instance, meta)\n\n // `self` is a live proxy so that actions/views always see the final\n // (fully-populated) instance — including wrapped actions added later.\n const self = new Proxy(instance, {\n get(_, k) {\n return instance[k as string]\n },\n })\n\n // ── 1. State signals ──────────────────────────────────────────────────────\n for (const [key, defaultValue] of Object.entries(config.state)) {\n meta.stateKeys.push(key)\n const path = `/${key}`\n const initValue: unknown =\n key in initial ? (initial as Record<string, unknown>)[key] : undefined\n\n let rawSig: Signal<unknown>\n\n if (isModelDef(defaultValue)) {\n // Nested model — create its instance from the supplied snapshot (or defaults).\n const nestedInstance = createInstance(\n defaultValue._config,\n (initValue as Record<string, unknown>) ?? {},\n )\n rawSig = signal(nestedInstance)\n\n // Propagate nested patches upward with the key as path prefix.\n onPatch(nestedInstance, (patch) => {\n meta.emitPatch({ ...patch, path: path + patch.path })\n })\n } else {\n rawSig = signal(initValue !== undefined ? initValue : defaultValue)\n }\n\n const tracked = trackedSignal(\n rawSig,\n path,\n (p) => meta.emitPatch(p),\n () => meta.patchListeners.size > 0,\n )\n instance[key] = tracked\n }\n\n // ── 2. Views ──────────────────────────────────────────────────────────────\n if (config.views) {\n const views = config.views(self)\n for (const [key, view] of Object.entries(views as Record<string, unknown>)) {\n instance[key] = view\n }\n }\n\n // ── 3. Actions (wrapped with middleware runner) ───────────────────────────\n if (config.actions) {\n const rawActions = config.actions(self) as Record<string, (...args: unknown[]) => unknown>\n for (const [key, actionFn] of Object.entries(rawActions)) {\n instance[key] = (...args: unknown[]) => runAction(meta, key, actionFn, args)\n }\n }\n\n return instance as ModelInstance<TState, TActions, TViews>\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\nimport { createInstance, type ModelConfig } from './instance'\nimport type { ModelInstance, Snapshot, StateShape } from './types'\nimport { MODEL_BRAND } from './types'\n\n// ─── Hook registry ────────────────────────────────────────────────────────────\n\n// Module-level singleton registry for `asHook()` — isolated per package import.\n// Use `resetHook(id)` or `resetAllHooks()` to clear entries (useful for tests / HMR).\nconst _hookRegistry = new Map<string, unknown>()\n\n/** Destroy a hook singleton by id so next call re-creates the instance. */\nexport function resetHook(id: string): void {\n _hookRegistry.delete(id)\n}\n\n/** Destroy all hook singletons. */\nexport function resetAllHooks(): void {\n _hookRegistry.clear()\n}\n\n// ─── ModelDefinition ──────────────────────────────────────────────────────────\n\n/**\n * Returned by `model()`. Call `.create()` for instances or `.asHook(id)` for\n * a Zustand-style singleton hook.\n */\nexport class ModelDefinition<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any>,\n TViews extends Record<string, Signal<any> | Computed<any>>,\n> {\n /** Brand used to identify ModelDefinition objects at runtime (without instanceof). */\n readonly [MODEL_BRAND] = true as const\n\n /** @internal — exposed so nested instance creation can read it. */\n readonly _config: ModelConfig<TState, TActions, TViews>\n\n constructor(config: ModelConfig<TState, TActions, TViews>) {\n this._config = config\n }\n\n /**\n * Create a new independent model instance.\n * Pass a partial snapshot to override defaults.\n *\n * @example\n * const counter = Counter.create({ count: 5 })\n */\n create(initial?: Partial<Snapshot<TState>>): ModelInstance<TState, TActions, TViews> {\n return createInstance(this._config, initial ?? {})\n }\n\n /**\n * Returns a hook function that always returns the same singleton instance\n * for the given `id` — Zustand / Pinia style.\n *\n * @example\n * const useCounter = Counter.asHook(\"app-counter\")\n * // Any call to useCounter() returns the same instance.\n * const store = useCounter()\n */\n asHook(id: string): () => ModelInstance<TState, TActions, TViews> {\n return () => {\n if (!_hookRegistry.has(id)) {\n _hookRegistry.set(id, this.create())\n }\n return _hookRegistry.get(id) as ModelInstance<TState, TActions, TViews>\n }\n }\n}\n\n// ─── model() factory ──────────────────────────────────────────────────────────\n\n/**\n * Define a reactive model with state, views, and actions.\n *\n * - **state** — plain JS object; each key becomes a `Signal<T>` on the instance.\n * - **views** — factory receiving `self`; return computed signals for derived state.\n * - **actions** — factory receiving `self`; return functions that mutate state.\n *\n * Use nested `ModelDefinition` values in `state` to compose models.\n *\n * @example\n * const Counter = model({\n * state: { count: 0 },\n * views: (self) => ({\n * doubled: computed(() => self.count() * 2),\n * }),\n * actions: (self) => ({\n * inc: () => self.count.update(c => c + 1),\n * reset: () => self.count.set(0),\n * }),\n * })\n *\n * const c = Counter.create({ count: 5 })\n * c.count() // 5\n * c.inc()\n * c.doubled() // 12\n */\nexport function model<\n TState extends StateShape,\n TActions extends Record<string, (...args: any[]) => any> = Record<never, never>,\n TViews extends Record<string, Signal<any> | Computed<any>> = Record<never, never>,\n>(config: ModelConfig<TState, TActions, TViews>): ModelDefinition<TState, TActions, TViews> {\n return new ModelDefinition(config)\n}\n","import type { Signal } from '@pyreon/reactivity'\nimport { batch } from '@pyreon/reactivity'\nimport { instanceMeta, isModelInstance } from './registry'\nimport type { Snapshot, StateShape } from './types'\n\n// ─── getSnapshot ──────────────────────────────────────────────────────────────\n\n/**\n * Serialize a model instance to a plain JS object (no signals, no functions).\n * Nested model instances are recursively serialized.\n *\n * @example\n * getSnapshot(counter) // { count: 6 }\n * getSnapshot(app) // { profile: { name: \"Alice\" }, title: \"My App\" }\n */\nexport function getSnapshot<TState extends StateShape>(instance: object): Snapshot<TState> {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] getSnapshot: not a model instance')\n\n const out: Record<string, unknown> = {}\n for (const key of meta.stateKeys) {\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = sig.peek()\n out[key] = isModelInstance(val) ? getSnapshot(val as object) : val\n }\n return out as Snapshot<TState>\n}\n\n// ─── applySnapshot ────────────────────────────────────────────────────────────\n\n/**\n * Restore a model instance from a plain-object snapshot.\n * All signal writes are coalesced via `batch()` for a single reactive flush.\n * Keys absent from the snapshot are left unchanged.\n *\n * @example\n * applySnapshot(counter, { count: 0 })\n */\nexport function applySnapshot<TState extends StateShape>(\n instance: object,\n snapshot: Partial<Snapshot<TState>>,\n): void {\n const meta = instanceMeta.get(instance)\n if (!meta) throw new Error('[@pyreon/state-tree] applySnapshot: not a model instance')\n\n batch(() => {\n for (const key of meta.stateKeys) {\n if (!(key in snapshot)) continue\n const sig = (instance as Record<string, Signal<unknown>>)[key]\n if (!sig) continue\n const val = (snapshot as Record<string, unknown>)[key]\n const current = sig.peek()\n if (isModelInstance(current)) {\n // Recurse into nested model instance\n applySnapshot(current as object, val as Record<string, unknown>)\n } else {\n sig.set(val)\n }\n }\n })\n}\n"],"mappings":";;;;;;;AAMA,MAAa,+BAAe,IAAI,SAA+B;;AAG/D,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,SAAS,QAAQ,OAAO,UAAU,YAAY,aAAa,IAAI,MAAgB;;;;;;;;;;ACAxF,SAAgB,UACd,MACA,MACA,IACA,MACS;CACT,MAAM,OAAmB;EAAE;EAAM;EAAM,MAAM,IAAI;EAAQ;CAEzD,MAAM,YAAY,KAAa,MAA2B;AACxD,MAAI,OAAO,KAAK,YAAY,OAAQ,QAAO,GAAG,GAAG,EAAE,KAAK;EACxD,MAAM,KAAK,KAAK,YAAY;AAC5B,MAAI,CAAC,GAAI,QAAO,GAAG,GAAG,EAAE,KAAK;AAC7B,SAAO,GAAG,IAAI,aAAa,SAAS,MAAM,GAAG,SAAS,CAAC;;AAGzD,QAAO,SAAS,GAAG,KAAK;;;;;;;;;;;;;;;;AAmB1B,SAAgB,cAAc,UAAkB,YAAsC;CACpF,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,2DAA2D;AACtF,MAAK,YAAY,KAAK,WAAW;AACjC,cAAa;EACX,MAAM,MAAM,KAAK,YAAY,QAAQ,WAAW;AAChD,MAAI,QAAQ,GAAI,MAAK,YAAY,OAAO,KAAK,EAAE;;;;;;;AC5CnD,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAa;CAAe;CAAY,CAAC;;;;;;;;AAWxE,SAAgB,cACd,OACA,MACA,WACA,cACW;CACX,MAAM,aAAgB,OAAO;AAE7B,MAAK,aAAgB,MAAM,MAAM;AAEjC,MAAK,aAAa,aAAuC,MAAM,UAAU,SAAS;AAElF,MAAK,OAAO,aAAsB;EAChC,MAAM,OAAO,MAAM,MAAM;AACzB,QAAM,IAAI,SAAS;AAGnB,MAAI,CAAC,OAAO,GAAG,MAAM,SAAS,KAAK,CAAC,gBAAgB,cAAc,EAIhE,WAAU;GAAE,IAAI;GAAW;GAAM,OADd,gBAAgB,SAAS,GAAG,cAAc,SAAmB,GAAG;GAC/B,CAAC;;AAIzD,MAAK,UAAU,OAAgC;AAC7C,OAAK,IAAI,GAAG,MAAM,MAAM,CAAC,CAAC;;AAG5B,QAAO;;;AAIT,SAAS,cAAc,UAA2C;CAChE,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,QAAO;CAClB,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,KAAK,WAAW;EAChC,MAAM,MAAO,SAA6C;AAC1D,MAAI,CAAC,IAAK;EACV,MAAM,MAAM,IAAI,MAAM;AACtB,MAAI,OAAO,gBAAgB,IAAI,GAAG,cAAc,IAAc,GAAG;;AAEnE,QAAO;;;;;;;;;;;;;AAgBT,SAAgB,QAAQ,UAAkB,UAAqC;CAC7E,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,qDAAqD;AAChF,MAAK,eAAe,IAAI,SAAS;AACjC,cAAa,KAAK,eAAe,OAAO,SAAS;;;;;;;;;;;;;;;;;;;AAsBnD,SAAgB,WAAW,UAAkB,OAA8B;CACzE,MAAM,UAAU,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM;AAEtD,aAAY;AACV,OAAK,MAAM,KAAK,SAAS;AACvB,OAAI,EAAE,OAAO,UACX,OAAM,IAAI,MAAM,oDAAoD,EAAE,GAAG,GAAG;GAG9E,MAAM,WAAW,EAAE,KAAK,MAAM,IAAI,CAAC,OAAO,QAAQ;AAClD,OAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,8CAA8C;GAIhE,IAAI,SAAiB;AACrB,QAAK,IAAI,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;IAC5C,MAAM,UAAU,SAAS;AACzB,QAAI,cAAc,IAAI,QAAQ,CAC5B,OAAM,IAAI,MAAM,4DAA4D,QAAQ,GAAG;AAGzF,QAAI,CADS,aAAa,IAAI,OAAO,CAEnC,OAAM,IAAI,MAAM,6DAA6D,QAAQ,GAAG;IAC1F,MAAM,MAAO,OAA2C;AACxD,QAAI,CAAC,OAAO,OAAO,IAAI,SAAS,WAC9B,OAAM,IAAI,MAAM,uDAAuD,QAAQ,GAAG;IAEpF,MAAM,SAAS,IAAI,MAAM;AACzB,QAAI,CAAC,UAAU,OAAO,WAAW,YAAY,CAAC,gBAAgB,OAAO,CACnE,OAAM,IAAI,MACR,qCAAqC,QAAQ,kCAC9C;AAEH,aAAS;;GAGX,MAAM,UAAU,SAAS,SAAS,SAAS;AAC3C,OAAI,cAAc,IAAI,QAAQ,CAC5B,OAAM,IAAI,MAAM,4DAA4D,QAAQ,GAAG;GAEzF,MAAM,OAAO,aAAa,IAAI,OAAO;AACrC,OAAI,CAAC,KAAM,OAAM,IAAI,MAAM,wDAAwD;AACnF,OAAI,CAAC,KAAK,UAAU,SAAS,QAAQ,CACnC,OAAM,IAAI,MAAM,uDAAuD,QAAQ,GAAG;GAGpF,MAAM,MAAO,OAA2C;AACxD,OAAI,OAAO,OAAO,IAAI,QAAQ,WAC5B,KAAI,IAAI,EAAE,MAAM;;GAGpB;;;;;;ACrJJ,MAAa,cAAc;;;;ACc3B,SAAS,WAAW,GAA8B;AAChD,KAAI,KAAK,QAAQ,OAAO,MAAM,SAAU,QAAO;AAC/C,QAAQ,EAA8B,iBAAiB;;;;;;AAiBzD,SAAgB,eAKd,QACA,SACyC;CAEzC,MAAM,WAAoC,EAAE;CAG5C,MAAM,OAAqB;EACzB,WAAW,EAAE;EACb,gCAAgB,IAAI,KAAK;EACzB,aAAa,EAAE;EACf,UAAU,OAAO;AAEf,OAAI,KAAK,eAAe,SAAS,EAAG;AACpC,QAAK,MAAM,YAAY,KAAK,eAAgB,UAAS,MAAM;;EAE9D;AACD,cAAa,IAAI,UAAU,KAAK;CAIhC,MAAM,OAAO,IAAI,MAAM,UAAU,EAC/B,IAAI,GAAG,GAAG;AACR,SAAO,SAAS;IAEnB,CAAC;AAGF,MAAK,MAAM,CAAC,KAAK,iBAAiB,OAAO,QAAQ,OAAO,MAAM,EAAE;AAC9D,OAAK,UAAU,KAAK,IAAI;EACxB,MAAM,OAAO,IAAI;EACjB,MAAM,YACJ,OAAO,UAAW,QAAoC,OAAO;EAE/D,IAAI;AAEJ,MAAI,WAAW,aAAa,EAAE;GAE5B,MAAM,iBAAiB,eACrB,aAAa,SACZ,aAAyC,EAAE,CAC7C;AACD,YAAS,OAAO,eAAe;AAG/B,WAAQ,iBAAiB,UAAU;AACjC,SAAK,UAAU;KAAE,GAAG;KAAO,MAAM,OAAO,MAAM;KAAM,CAAC;KACrD;QAEF,UAAS,OAAO,cAAc,SAAY,YAAY,aAAa;AASrE,WAAS,OANO,cACd,QACA,OACC,MAAM,KAAK,UAAU,EAAE,QAClB,KAAK,eAAe,OAAO,EAClC;;AAKH,KAAI,OAAO,OAAO;EAChB,MAAM,QAAQ,OAAO,MAAM,KAAK;AAChC,OAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,MAAiC,CACxE,UAAS,OAAO;;AAKpB,KAAI,OAAO,SAAS;EAClB,MAAM,aAAa,OAAO,QAAQ,KAAK;AACvC,OAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,WAAW,CACtD,UAAS,QAAQ,GAAG,SAAoB,UAAU,MAAM,KAAK,UAAU,KAAK;;AAIhF,QAAO;;;;;AC/GT,MAAM,gCAAgB,IAAI,KAAsB;;AAGhD,SAAgB,UAAU,IAAkB;AAC1C,eAAc,OAAO,GAAG;;;AAI1B,SAAgB,gBAAsB;AACpC,eAAc,OAAO;;;;;;AASvB,IAAa,kBAAb,MAIE;;CAEA,CAAU,eAAe;;CAGzB,AAAS;CAET,YAAY,QAA+C;AACzD,OAAK,UAAU;;;;;;;;;CAUjB,OAAO,SAA8E;AACnF,SAAO,eAAe,KAAK,SAAS,WAAW,EAAE,CAAC;;;;;;;;;;;CAYpD,OAAO,IAA2D;AAChE,eAAa;AACX,OAAI,CAAC,cAAc,IAAI,GAAG,CACxB,eAAc,IAAI,IAAI,KAAK,QAAQ,CAAC;AAEtC,UAAO,cAAc,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiClC,SAAgB,MAId,QAA0F;AAC1F,QAAO,IAAI,gBAAgB,OAAO;;;;;;;;;;;;;AC1FpC,SAAgB,YAAuC,UAAoC;CACzF,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,yDAAyD;CAEpF,MAAM,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,KAAK,WAAW;EAChC,MAAM,MAAO,SAA6C;AAC1D,MAAI,CAAC,IAAK;EACV,MAAM,MAAM,IAAI,MAAM;AACtB,MAAI,OAAO,gBAAgB,IAAI,GAAG,YAAY,IAAc,GAAG;;AAEjE,QAAO;;;;;;;;;;AAaT,SAAgB,cACd,UACA,UACM;CACN,MAAM,OAAO,aAAa,IAAI,SAAS;AACvC,KAAI,CAAC,KAAM,OAAM,IAAI,MAAM,2DAA2D;AAEtF,aAAY;AACV,OAAK,MAAM,OAAO,KAAK,WAAW;AAChC,OAAI,EAAE,OAAO,UAAW;GACxB,MAAM,MAAO,SAA6C;AAC1D,OAAI,CAAC,IAAK;GACV,MAAM,MAAO,SAAqC;GAClD,MAAM,UAAU,IAAI,MAAM;AAC1B,OAAI,gBAAgB,QAAQ,CAE1B,eAAc,SAAmB,IAA+B;OAEhE,KAAI,IAAI,IAAI;;GAGhB"}
@@ -41,7 +41,7 @@ type Snapshot<TState extends StateShape> = { [K in keyof TState]: TState[K] exte
41
41
  readonly __pyreonMod: true;
42
42
  } ? Snapshot<ExtractModelState<TState[K]>> : TState[K] };
43
43
  interface Patch {
44
- op: "replace";
44
+ op: 'replace';
45
45
  path: string;
46
46
  value: unknown;
47
47
  }
package/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "@pyreon/state-tree",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "Structured reactive state tree — composable models with snapshots, patches, and middleware",
5
+ "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/state-tree#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/pyreon/pyreon/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "https://github.com/pyreon/pyreon.git",
9
13
  "directory": "packages/fundamentals/state-tree"
10
14
  },
11
- "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/state-tree#readme",
12
- "bugs": {
13
- "url": "https://github.com/pyreon/pyreon/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
15
  "files": [
19
16
  "lib",
20
17
  "src",
@@ -22,6 +19,7 @@
22
19
  "LICENSE"
23
20
  ],
24
21
  "type": "module",
22
+ "sideEffects": false,
25
23
  "main": "./lib/index.js",
26
24
  "module": "./lib/index.js",
27
25
  "types": "./lib/types/index.d.ts",
@@ -37,19 +35,21 @@
37
35
  "types": "./lib/types/devtools.d.ts"
38
36
  }
39
37
  },
40
- "sideEffects": false,
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
41
  "scripts": {
42
42
  "build": "vl_rolldown_build",
43
43
  "dev": "vl_rolldown_build-watch",
44
44
  "test": "vitest run",
45
45
  "typecheck": "tsc --noEmit",
46
- "lint": "biome check ."
47
- },
48
- "peerDependencies": {
49
- "@pyreon/reactivity": "^0.11.5"
46
+ "lint": "oxlint ."
50
47
  },
51
48
  "devDependencies": {
52
49
  "@happy-dom/global-registrator": "^20.8.3",
53
- "@pyreon/reactivity": "^0.11.5"
50
+ "@pyreon/reactivity": "^0.11.6"
51
+ },
52
+ "peerDependencies": {
53
+ "@pyreon/reactivity": "^0.11.6"
54
54
  }
55
55
  }
package/src/devtools.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Import: `import { ... } from "@pyreon/state-tree/devtools"`
4
4
  */
5
5
 
6
- import { getSnapshot } from "./snapshot"
6
+ import { getSnapshot } from './snapshot'
7
7
 
8
8
  // Track active model instances (devtools-only, opt-in)
9
9
  const _activeModels = new Map<string, WeakRef<object>>()
package/src/index.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  // ─── Core ─────────────────────────────────────────────────────────────────────
2
2
 
3
- export type { ModelDefinition } from "./model"
4
- export { model, resetAllHooks, resetHook } from "./model"
3
+ export type { ModelDefinition } from './model'
4
+ export { model, resetAllHooks, resetHook } from './model'
5
5
 
6
6
  // ─── Snapshot ─────────────────────────────────────────────────────────────────
7
7
 
8
- export { applySnapshot, getSnapshot } from "./snapshot"
8
+ export { applySnapshot, getSnapshot } from './snapshot'
9
9
 
10
10
  // ─── Patches ─────────────────────────────────────────────────────────────────
11
11
 
12
- export { applyPatch, onPatch } from "./patch"
12
+ export { applyPatch, onPatch } from './patch'
13
13
 
14
14
  // ─── Middleware ───────────────────────────────────────────────────────────────
15
15
 
16
- export { addMiddleware } from "./middleware"
16
+ export { addMiddleware } from './middleware'
17
17
 
18
18
  // ─── Types ────────────────────────────────────────────────────────────────────
19
19
 
@@ -26,4 +26,4 @@ export type {
26
26
  PatchListener,
27
27
  Snapshot,
28
28
  StateShape,
29
- } from "./types"
29
+ } from './types'
package/src/instance.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { Computed, Signal } from "@pyreon/reactivity"
2
- import { signal } from "@pyreon/reactivity"
3
- import { runAction } from "./middleware"
4
- import { onPatch, trackedSignal } from "./patch"
5
- import { instanceMeta } from "./registry"
6
- import type { InstanceMeta, ModelInstance, Snapshot, StateShape } from "./types"
7
- import { MODEL_BRAND } from "./types"
1
+ import type { Computed, Signal } from '@pyreon/reactivity'
2
+ import { signal } from '@pyreon/reactivity'
3
+ import { runAction } from './middleware'
4
+ import { onPatch, trackedSignal } from './patch'
5
+ import { instanceMeta } from './registry'
6
+ import type { InstanceMeta, ModelInstance, Snapshot, StateShape } from './types'
7
+ import { MODEL_BRAND } from './types'
8
8
 
9
9
  // ─── Model definition detection ───────────────────────────────────────────────
10
10
 
@@ -18,7 +18,7 @@ interface AnyModelDef {
18
18
  }
19
19
 
20
20
  function isModelDef(v: unknown): v is AnyModelDef {
21
- if (v == null || typeof v !== "object") return false
21
+ if (v == null || typeof v !== 'object') return false
22
22
  return (v as Record<string, unknown>)[MODEL_BRAND] === true
23
23
  }
24
24
 
package/src/middleware.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { instanceMeta } from "./registry"
2
- import type { ActionCall, InstanceMeta, MiddlewareFn } from "./types"
1
+ import { instanceMeta } from './registry'
2
+ import type { ActionCall, InstanceMeta, MiddlewareFn } from './types'
3
3
 
4
4
  // ─── Action runner ────────────────────────────────────────────────────────────
5
5
 
@@ -44,7 +44,7 @@ export function runAction(
44
44
  */
45
45
  export function addMiddleware(instance: object, middleware: MiddlewareFn): () => void {
46
46
  const meta = instanceMeta.get(instance)
47
- if (!meta) throw new Error("[@pyreon/state-tree] addMiddleware: not a model instance")
47
+ if (!meta) throw new Error('[@pyreon/state-tree] addMiddleware: not a model instance')
48
48
  meta.middlewares.push(middleware)
49
49
  return () => {
50
50
  const idx = meta.middlewares.indexOf(middleware)
package/src/model.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { Computed, Signal } from "@pyreon/reactivity"
2
- import { createInstance, type ModelConfig } from "./instance"
3
- import type { ModelInstance, Snapshot, StateShape } from "./types"
4
- import { MODEL_BRAND } from "./types"
1
+ import type { Computed, Signal } from '@pyreon/reactivity'
2
+ import { createInstance, type ModelConfig } from './instance'
3
+ import type { ModelInstance, Snapshot, StateShape } from './types'
4
+ import { MODEL_BRAND } from './types'
5
5
 
6
6
  // ─── Hook registry ────────────────────────────────────────────────────────────
7
7