r-state-tree 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,7 +84,15 @@ function makeChildDecorator(typeObj) {
84
84
  };
85
85
  }
86
86
  const child = makeChildDecorator(childType);
87
- const modelRef = makeChildDecorator(modelRefType);
87
+ const modelRefDecorator = makeChildDecorator(modelRefType);
88
+ function modelRef(childCtor, context) {
89
+ if (context !== void 0) {
90
+ throw new Error(
91
+ "r-state-tree: @modelRef requires a model constructor, for example `@modelRef(User)`"
92
+ );
93
+ }
94
+ return modelRefDecorator(childCtor);
95
+ }
88
96
  const model = makeDecorator(modelType);
89
97
  const id = makeDecorator(idType);
90
98
  const state = makeDecorator(stateType);
@@ -102,7 +110,11 @@ function normalizeEntry(entry) {
102
110
  if (entry === state) return stateType;
103
111
  if (entry === model) return modelType;
104
112
  if (entry === child) return childType;
105
- if (entry === modelRef) return modelRefType;
113
+ if (entry === modelRef) {
114
+ throw new Error(
115
+ "r-state-tree: modelRef requires a model constructor, for example `modelRef(User)`"
116
+ );
117
+ }
106
118
  return void 0;
107
119
  }
108
120
  return void 0;
@@ -668,9 +680,10 @@ function reaction(fn, callback) {
668
680
  if (Object.is(currentValue, nextValue)) {
669
681
  return;
670
682
  }
683
+ const previousValue = currentValue;
671
684
  currentValue = nextValue;
672
685
  untracked(() => {
673
- callback(nextValue);
686
+ callback(nextValue, previousValue);
674
687
  });
675
688
  });
676
689
  }
@@ -1925,7 +1938,7 @@ function createCircularMountError(frame) {
1925
1938
  const chain = formatMountChain(frame);
1926
1939
  const models = frame.modelsKeys.length === 0 ? "no models provided" : `models: ${frame.modelsKeys.join(", ")}`;
1927
1940
  return new Error(
1928
- `r-state-tree: detected circular store/model creation while mounting ${chain} (using ${models}). Passing models into child stores during mount can create recursive wiring. Move child store/model creation into storeDidMount or break the cycle.`
1941
+ `r-state-tree: detected circular store/model creation while mounting ${chain} (using ${models}). Passing models into child stores during mount can create recursive wiring. Break the ownership cycle.`
1929
1942
  );
1930
1943
  }
1931
1944
  function updateProps(props, newProps) {
@@ -1984,8 +1997,8 @@ class StoreAdministration extends PreactObjectAdministration {
1984
1997
  PreactObjectAdministration.proxyTraps,
1985
1998
  {
1986
1999
  get(target, name) {
1987
- if (name === "key") {
1988
- return target.key;
2000
+ if (name === "key" || name === Symbol.dispose) {
2001
+ return Reflect.get(target, name);
1989
2002
  }
1990
2003
  const adm = getAdministration(target);
1991
2004
  switch (getConfigType(adm.configuration[name])) {
@@ -2018,10 +2031,11 @@ class StoreAdministration extends PreactObjectAdministration {
2018
2031
  }
2019
2032
  );
2020
2033
  parent = null;
2021
- mounted = false;
2034
+ mountedSignal = createSignal(false);
2035
+ disposed = false;
2022
2036
  contextCache = /* @__PURE__ */ new Map();
2023
2037
  childStoreDataMap = /* @__PURE__ */ new Map();
2024
- reactionsUnsub = [];
2038
+ ownedDisposers = /* @__PURE__ */ new Set();
2025
2039
  configurationGetter;
2026
2040
  setConfiguration(configurationGetter) {
2027
2041
  this.configurationGetter = configurationGetter;
@@ -2095,7 +2109,7 @@ class StoreAdministration extends PreactObjectAdministration {
2095
2109
  if (newStores.size || removedStores.size || keyedIndexChanged) {
2096
2110
  batch(() => childStoreData.value.set(stores));
2097
2111
  }
2098
- removedStores.forEach((s) => getStoreAdm(s).unmount());
2112
+ removedStores.forEach((s) => getStoreAdm(s).dispose(true));
2099
2113
  newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2100
2114
  return stores;
2101
2115
  }
@@ -2106,12 +2120,12 @@ class StoreAdministration extends PreactObjectAdministration {
2106
2120
  );
2107
2121
  const { key, Type, props } = element || {};
2108
2122
  if (!element) {
2109
- oldStore && getStoreAdm(oldStore).unmount();
2123
+ oldStore && getStoreAdm(oldStore).dispose(true);
2110
2124
  batch(() => childStoreData.value.set(null));
2111
2125
  return null;
2112
2126
  } else if (!oldStore || oldStore.props.key !== key || !(oldStore instanceof Type)) {
2113
2127
  if (oldStore) {
2114
- getStoreAdm(oldStore).unmount();
2128
+ getStoreAdm(oldStore).dispose(true);
2115
2129
  }
2116
2130
  const childStore = this.createChildStore(element);
2117
2131
  batch(() => childStoreData.value.set(childStore));
@@ -2155,7 +2169,8 @@ class StoreAdministration extends PreactObjectAdministration {
2155
2169
  getStore(name) {
2156
2170
  const childStoreData = this.childStoreDataMap.get(name);
2157
2171
  if (!childStoreData) {
2158
- return this.initializeStore(name);
2172
+ untracked(() => this.initializeStore(name));
2173
+ return this.childStoreDataMap.get(name).value.get();
2159
2174
  } else {
2160
2175
  const storeElement = untracked(() => childStoreData.computed.get());
2161
2176
  validateStoreChildValue(storeElement, name);
@@ -2171,6 +2186,9 @@ class StoreAdministration extends PreactObjectAdministration {
2171
2186
  isRoot() {
2172
2187
  return !this.parent;
2173
2188
  }
2189
+ get isMounted() {
2190
+ return this.mountedSignal.get();
2191
+ }
2174
2192
  getContextValue(contextId, provideSymbol, defaultValue, hasDefault) {
2175
2193
  let computed2 = this.contextCache.get(contextId);
2176
2194
  if (!computed2) {
@@ -2204,12 +2222,37 @@ class StoreAdministration extends PreactObjectAdministration {
2204
2222
  }
2205
2223
  return void 0;
2206
2224
  }
2225
+ own(disposer) {
2226
+ if (this.disposed) {
2227
+ disposer();
2228
+ throw new Error(
2229
+ "r-state-tree: cannot register reactive resources on a disposed store"
2230
+ );
2231
+ }
2232
+ let active = true;
2233
+ const wrapped = () => {
2234
+ if (!active) return;
2235
+ active = false;
2236
+ this.ownedDisposers.delete(wrapped);
2237
+ disposer();
2238
+ };
2239
+ this.ownedDisposers.add(wrapped);
2240
+ return wrapped;
2241
+ }
2207
2242
  reaction(track, callback) {
2208
2243
  const unsub = reaction(track, callback);
2209
- this.reactionsUnsub.push(unsub);
2210
- return unsub;
2244
+ return this.own(unsub);
2245
+ }
2246
+ effect(callback) {
2247
+ return this.own(effect(callback));
2211
2248
  }
2212
2249
  mount(parent = null, childName) {
2250
+ if (this.disposed) {
2251
+ throw new Error("r-state-tree: cannot mount a disposed store");
2252
+ }
2253
+ if (this.isMounted) {
2254
+ throw new Error("r-state-tree: store is already mounted");
2255
+ }
2213
2256
  const frame = {
2214
2257
  storeName: this.proxy.constructor.name || "Store",
2215
2258
  childName,
@@ -2220,19 +2263,20 @@ class StoreAdministration extends PreactObjectAdministration {
2220
2263
  }
2221
2264
  mountingStack.push(frame);
2222
2265
  try {
2223
- this.parent = parent || null;
2224
- this.childStoreDataMap.forEach(({ value }, name) => {
2225
- const stores = value.get();
2226
- if (Array.isArray(stores)) {
2227
- stores?.forEach((s) => getStoreAdm(s)?.mount(this, name));
2228
- } else if (stores) {
2229
- getStoreAdm(stores)?.mount(this, name);
2230
- }
2266
+ batch(() => {
2267
+ this.parent = parent || null;
2268
+ this.childStoreDataMap.forEach(({ value }, name) => {
2269
+ const stores = value.get();
2270
+ if (Array.isArray(stores)) {
2271
+ stores?.forEach((s) => getStoreAdm(s)?.mount(this, name));
2272
+ } else if (stores) {
2273
+ getStoreAdm(stores)?.mount(this, name);
2274
+ }
2275
+ });
2276
+ this.mountedSignal.set(true);
2231
2277
  });
2232
- this.mounted = true;
2233
- batch(() => this.proxy.storeDidMount?.());
2234
2278
  } catch (error) {
2235
- if (error instanceof RangeError && /call stack/i.test(error.message)) {
2279
+ if (error instanceof Error && (error instanceof RangeError && /call stack/i.test(error.message) || /cycle detected/i.test(error.message))) {
2236
2280
  throw createCircularMountError(frame);
2237
2281
  }
2238
2282
  throw error;
@@ -2240,25 +2284,31 @@ class StoreAdministration extends PreactObjectAdministration {
2240
2284
  mountingStack.pop();
2241
2285
  }
2242
2286
  }
2243
- unmount() {
2244
- this.proxy.storeWillUnmount?.();
2245
- this.mounted = false;
2246
- this.childStoreDataMap.forEach((data) => {
2247
- const { value, computed: computed2, listener } = data;
2248
- const stores = value.get();
2249
- if (Array.isArray(stores)) {
2250
- stores?.forEach((s) => getStoreAdm(s)?.unmount());
2251
- } else if (stores) {
2252
- getStoreAdm(stores)?.unmount();
2253
- }
2254
- computed2.clear();
2255
- listener.dispose();
2287
+ dispose(internal = false) {
2288
+ if (this.disposed) return;
2289
+ if (!internal && !this.isRoot()) {
2290
+ throw new Error("r-state-tree: can only dispose root stores");
2291
+ }
2292
+ batch(() => {
2293
+ this.childStoreDataMap.forEach((data) => {
2294
+ const { value, computed: computed2, listener } = data;
2295
+ const stores = value.get();
2296
+ if (Array.isArray(stores)) {
2297
+ stores?.forEach((s) => getStoreAdm(s)?.dispose(true));
2298
+ } else if (stores) {
2299
+ getStoreAdm(stores)?.dispose(true);
2300
+ }
2301
+ computed2.clear();
2302
+ listener.dispose();
2303
+ });
2304
+ this.childStoreDataMap.clear();
2305
+ this.contextCache.forEach((computed2) => computed2.clear());
2306
+ this.contextCache.clear();
2307
+ this.parent = null;
2256
2308
  });
2257
- this.childStoreDataMap.clear();
2258
- this.contextCache.forEach((computed2) => computed2.clear());
2259
- this.contextCache.clear();
2260
- this.reactionsUnsub.forEach((u) => u());
2261
- this.parent = null;
2309
+ this.mountedSignal.set(false);
2310
+ this.disposed = true;
2311
+ Array.from(this.ownedDisposers).forEach((dispose) => dispose());
2262
2312
  }
2263
2313
  }
2264
2314
  let initEnabled$1 = false;
@@ -2304,141 +2354,109 @@ class Store {
2304
2354
  get key() {
2305
2355
  return this.props.key;
2306
2356
  }
2357
+ get isMounted() {
2358
+ return getStoreAdm(this).isMounted;
2359
+ }
2307
2360
  reaction(track, callback) {
2308
2361
  return getStoreAdm(this).reaction(track, callback);
2309
2362
  }
2310
- // eslint-disable-next-line @typescript-eslint/no-empty-function
2311
- storeDidMount() {
2363
+ effect(callback) {
2364
+ return getStoreAdm(this).effect(callback);
2312
2365
  }
2313
- // eslint-disable-next-line @typescript-eslint/no-empty-function
2314
- storeWillUnmount() {
2366
+ [Symbol.dispose]() {
2367
+ getStoreAdm(this).dispose();
2315
2368
  }
2316
2369
  }
2317
2370
  const attachedIdMap = /* @__PURE__ */ new WeakMap();
2318
2371
  const idMap = /* @__PURE__ */ new WeakMap();
2319
- let loadingSnapshot = false;
2320
- const potentialDups = /* @__PURE__ */ new Set();
2321
- function onSnapshotLoad(fn) {
2322
- const wasLoadingSnapshot = loadingSnapshot;
2323
- loadingSnapshot = true;
2324
- try {
2325
- return fn();
2326
- } finally {
2327
- if (!wasLoadingSnapshot) {
2328
- loadingSnapshot = false;
2329
- const rootMap = /* @__PURE__ */ new Map();
2330
- try {
2331
- potentialDups.forEach((model2) => {
2332
- const root = getModelAdm(model2).root.proxy;
2333
- let set = rootMap.get(root);
2334
- if (!set) {
2335
- set = /* @__PURE__ */ new Set();
2336
- rootMap.set(root, set);
2337
- }
2338
- if (idMap.has(model2)) {
2339
- const id2 = idMap.get(model2);
2340
- if (set.has(id2)) {
2341
- throw new Error(
2342
- "r-state-tree duplicate ids detected after snapshot was loaded"
2343
- );
2344
- }
2345
- set.add(id2);
2346
- }
2347
- });
2348
- } finally {
2349
- potentialDups.clear();
2350
- }
2372
+ function getModelType(model2) {
2373
+ return Object.getPrototypeOf(model2).constructor;
2374
+ }
2375
+ function entriesFor(model2) {
2376
+ const entries = [];
2377
+ attachedIdMap.get(model2)?.forEach((ids, Type) => {
2378
+ ids.forEach((value, id22) => {
2379
+ if (value) entries.push([Type, id22, value]);
2380
+ });
2381
+ });
2382
+ const id2 = idMap.get(model2);
2383
+ if (id2 != null) entries.push([getModelType(model2), id2, model2]);
2384
+ return entries;
2385
+ }
2386
+ function assertAvailable(node, entries) {
2387
+ const map = attachedIdMap.get(node);
2388
+ for (const [Type, id2, model2] of entries) {
2389
+ const existing = map?.get(Type)?.get(id2);
2390
+ if (existing && existing !== model2) {
2391
+ throw new Error(
2392
+ `r-state-tree: id: ${id2} is already assigned to another model`
2393
+ );
2351
2394
  }
2352
2395
  }
2353
2396
  }
2397
+ function bucket(node, Type) {
2398
+ let map = attachedIdMap.get(node);
2399
+ if (!map) {
2400
+ map = observable(/* @__PURE__ */ new Map());
2401
+ attachedIdMap.set(node, map);
2402
+ }
2403
+ let ids = map.get(Type);
2404
+ if (!ids) {
2405
+ ids = observable(/* @__PURE__ */ new Map());
2406
+ map.set(Type, ids);
2407
+ }
2408
+ return ids;
2409
+ }
2354
2410
  function setIdentifier(model2, id2) {
2355
- const prevId = idMap.get(model2);
2356
- idMap.set(model2, id2);
2357
- if (prevId == null) {
2358
- if (model2.parent) {
2359
- onModelAttached(model2);
2360
- }
2361
- } else if (prevId !== id2) {
2362
- updateIdentifier(model2);
2411
+ const previousId = idMap.get(model2);
2412
+ if (previousId === id2) return;
2413
+ const Type = getModelType(model2);
2414
+ const ancestors = [];
2415
+ let node = model2.parent;
2416
+ while (node) {
2417
+ assertAvailable(node, [[Type, id2, model2]]);
2418
+ ancestors.push(node);
2419
+ node = node.parent;
2420
+ }
2421
+ for (const ancestor of ancestors) {
2422
+ const ids = bucket(ancestor, Type);
2423
+ if (previousId != null && ids.get(previousId) === model2)
2424
+ ids.delete(previousId);
2425
+ ids.set(id2, model2);
2363
2426
  }
2427
+ idMap.set(model2, id2);
2364
2428
  }
2365
2429
  function getIdentifier(model2) {
2366
2430
  return idMap.get(model2);
2367
2431
  }
2368
- function getModelById(root, id2) {
2369
- const map = attachedIdMap.get(root);
2370
- return map?.get(id2);
2432
+ function getModelById(root, Type, id2) {
2433
+ return attachedIdMap.get(root)?.get(Type)?.get(id2);
2371
2434
  }
2372
- function updateIdentifier(model2) {
2373
- const id2 = idMap.get(model2);
2435
+ function onModelAttached(model2) {
2436
+ const entries = entriesFor(model2);
2437
+ if (!entries.length) return;
2438
+ const ancestors = [];
2374
2439
  let node = model2.parent;
2375
2440
  while (node) {
2376
- const map = attachedIdMap.get(node);
2377
- if (map) {
2378
- map.set(id2, model2);
2379
- }
2441
+ assertAvailable(node, entries);
2442
+ ancestors.push(node);
2380
2443
  node = node.parent;
2381
2444
  }
2382
- }
2383
- function onModelAttached(model2) {
2384
- const attachedMap = attachedIdMap.get(model2);
2385
- const id2 = idMap.get(model2);
2386
- if (attachedMap || id2 != null) {
2387
- const id22 = idMap.get(model2);
2388
- let node = model2.parent;
2389
- while (node) {
2390
- let map = attachedIdMap.get(node);
2391
- if (!map) {
2392
- map = observable(/* @__PURE__ */ new Map());
2393
- attachedIdMap.set(node, map);
2394
- }
2395
- attachedMap?.forEach((value, key) => {
2396
- if (map.has(key)) {
2397
- if (loadingSnapshot) {
2398
- potentialDups.add(model2);
2399
- potentialDups.add(map.get(key));
2400
- } else {
2401
- throw new Error(
2402
- `r-state-tree: id: ${key} is already assigned to another model`
2403
- );
2404
- }
2405
- }
2406
- map.set(key, value);
2407
- });
2408
- if (id22 != null) {
2409
- if (map.has(id22)) {
2410
- if (loadingSnapshot) {
2411
- potentialDups.add(model2);
2412
- potentialDups.add(map.get(id22));
2413
- } else {
2414
- throw new Error(
2415
- `r-state-tree: id: ${id22} is already assigned to another model`
2416
- );
2417
- }
2418
- }
2419
- map.set(id22, model2);
2420
- }
2421
- node = node.parent;
2422
- }
2445
+ for (const ancestor of ancestors) {
2446
+ for (const [Type, id2, value] of entries)
2447
+ bucket(ancestor, Type).set(id2, value);
2423
2448
  }
2424
2449
  }
2425
2450
  function onModelDetached(model2) {
2426
- const id2 = idMap.get(model2);
2427
- if (attachedIdMap.has(model2) || id2 != null) {
2428
- const attachedMap = attachedIdMap.get(model2);
2429
- let node = model2.parent;
2430
- while (node) {
2431
- const map = attachedIdMap.get(node);
2432
- if (map) {
2433
- attachedMap?.forEach((value, key) => {
2434
- map.delete(key);
2435
- });
2436
- if (id2 != null) {
2437
- map.delete(id2);
2438
- }
2439
- }
2440
- node = node.parent;
2451
+ const entries = entriesFor(model2);
2452
+ let node = model2.parent;
2453
+ while (node) {
2454
+ const map = attachedIdMap.get(node);
2455
+ for (const [Type, id2, value] of entries) {
2456
+ const ids = map?.get(Type);
2457
+ if (ids?.get(id2) === value) ids.delete(id2);
2441
2458
  }
2459
+ node = node.parent;
2442
2460
  }
2443
2461
  }
2444
2462
  const listenerMap = /* @__PURE__ */ new WeakMap();
@@ -2494,19 +2512,28 @@ class ObservableListener {
2494
2512
  notify(ev) {
2495
2513
  if (!this.listeners) return;
2496
2514
  this.notifying = true;
2497
- for (let i = 0; i < this.listeners.length; i++) {
2498
- this.listeners[i](ev);
2515
+ try {
2516
+ for (let i = 0; i < this.listeners.length; i++) {
2517
+ this.listeners[i](ev);
2518
+ }
2519
+ } finally {
2520
+ this.notifying = false;
2499
2521
  }
2500
- this.notifying = false;
2501
2522
  }
2502
2523
  }
2503
2524
  class ChildModelsAdministration extends ArrayAdministration {
2504
2525
  set(index, newValue) {
2505
2526
  return batch(() => {
2527
+ const oldValue = this.source[index];
2506
2528
  const result = super.set(index, newValue);
2507
2529
  const sourceValue = getSource(newValue);
2508
- if (this.source[index] !== sourceValue) {
2509
- notifyArrayUpdate(this.proxy, index, this.source[index], sourceValue);
2530
+ if (oldValue !== sourceValue) {
2531
+ try {
2532
+ notifyArrayUpdate(this.proxy, index, oldValue, sourceValue);
2533
+ } catch (error) {
2534
+ super.set(index, oldValue);
2535
+ throw error;
2536
+ }
2510
2537
  }
2511
2538
  return result;
2512
2539
  });
@@ -2515,7 +2542,12 @@ class ChildModelsAdministration extends ArrayAdministration {
2515
2542
  return batch(() => {
2516
2543
  const deleted = super.spliceWithArray(index, deleteCount, newItems);
2517
2544
  if (deleteCount || newItems?.length) {
2518
- notifySpliceArray(this.proxy, index, newItems ?? [], deleted);
2545
+ try {
2546
+ notifySpliceArray(this.proxy, index, newItems ?? [], deleted);
2547
+ } catch (error) {
2548
+ super.spliceWithArray(index, newItems?.length ?? 0, deleted);
2549
+ throw error;
2550
+ }
2519
2551
  }
2520
2552
  return deleted;
2521
2553
  });
@@ -2594,6 +2626,9 @@ class ModelAdministration extends PreactObjectAdministration {
2594
2626
  {
2595
2627
  get(target, prop, proxy) {
2596
2628
  const adm = getAdministration(target);
2629
+ if (prop === Symbol.dispose) {
2630
+ return Reflect.get(target, prop, proxy);
2631
+ }
2597
2632
  if (prop === "parent") {
2598
2633
  return target.parent;
2599
2634
  }
@@ -2612,6 +2647,7 @@ class ModelAdministration extends PreactObjectAdministration {
2612
2647
  },
2613
2648
  set(target, name, value) {
2614
2649
  const adm = getAdministration(target);
2650
+ adm.assertUsable();
2615
2651
  adm.writeInProgress.add(name);
2616
2652
  try {
2617
2653
  switch (adm.getCfgType(name)) {
@@ -2674,6 +2710,8 @@ class ModelAdministration extends PreactObjectAdministration {
2674
2710
  computedSnapshot;
2675
2711
  snapshotMap = /* @__PURE__ */ new Map();
2676
2712
  contextCache = /* @__PURE__ */ new Map();
2713
+ ownedDisposers = /* @__PURE__ */ new Set();
2714
+ disposed = false;
2677
2715
  parentName = null;
2678
2716
  get parent() {
2679
2717
  this.parentAtom.reportObserved();
@@ -2688,6 +2726,29 @@ class ModelAdministration extends PreactObjectAdministration {
2688
2726
  setConfiguration(configurationGetter) {
2689
2727
  this.configurationGetter = configurationGetter;
2690
2728
  }
2729
+ assertUsable() {
2730
+ if (this.disposed) {
2731
+ throw new Error("r-state-tree: cannot use a disposed model");
2732
+ }
2733
+ }
2734
+ own(disposer) {
2735
+ this.assertUsable();
2736
+ let active = true;
2737
+ const wrapped = () => {
2738
+ if (!active) return;
2739
+ active = false;
2740
+ this.ownedDisposers.delete(wrapped);
2741
+ disposer();
2742
+ };
2743
+ this.ownedDisposers.add(wrapped);
2744
+ return wrapped;
2745
+ }
2746
+ reaction(track, callback) {
2747
+ return this.own(reaction(track, callback));
2748
+ }
2749
+ effect(callback) {
2750
+ return this.own(effect(callback));
2751
+ }
2691
2752
  get configuration() {
2692
2753
  return this.configurationGetter?.() ?? {};
2693
2754
  }
@@ -2757,8 +2818,8 @@ class ModelAdministration extends PreactObjectAdministration {
2757
2818
  );
2758
2819
  }
2759
2820
  if (v !== void 0) {
2760
- this.source[name] = v;
2761
2821
  setIdentifier(this.proxy, v);
2822
+ this.source[name] = v;
2762
2823
  }
2763
2824
  }
2764
2825
  setModel(name, newModel) {
@@ -2813,11 +2874,30 @@ class ModelAdministration extends PreactObjectAdministration {
2813
2874
  name,
2814
2875
  observe(this.proxy[name], (event) => {
2815
2876
  if (event.type === "updateArray") {
2816
- getModelAdm(event.oldValue).detach();
2817
- getModelAdm(event.newValue).attach(this, name);
2877
+ const oldAdm = getModelAdm(event.oldValue);
2878
+ const newAdm = getModelAdm(event.newValue);
2879
+ oldAdm.detach();
2880
+ try {
2881
+ newAdm.attach(this, name);
2882
+ } catch (error) {
2883
+ oldAdm.attach(this, name);
2884
+ throw error;
2885
+ }
2818
2886
  } else if (event.type === "spliceArray") {
2887
+ const attached = [];
2819
2888
  event.removed.forEach((model2) => getModelAdm(model2).detach());
2820
- event.added.forEach((model2) => getModelAdm(model2).attach(this, name));
2889
+ try {
2890
+ event.added.forEach((model2) => {
2891
+ getModelAdm(model2).attach(this, name);
2892
+ attached.push(model2);
2893
+ });
2894
+ } catch (error) {
2895
+ attached.forEach((model2) => getModelAdm(model2).detach());
2896
+ event.removed.forEach(
2897
+ (model2) => getModelAdm(model2).attach(this, name)
2898
+ );
2899
+ throw error;
2900
+ }
2821
2901
  }
2822
2902
  })
2823
2903
  );
@@ -2832,7 +2912,9 @@ class ModelAdministration extends PreactObjectAdministration {
2832
2912
  getModelRef(name) {
2833
2913
  const a = this.getReferencedAtom(name);
2834
2914
  a.reportObserved();
2835
- return this.source[name] != null ? getModelById(this.root.proxy, this.source[name]) : void 0;
2915
+ const Type = this.getRequiredModelRefType(name);
2916
+ const root = this.getReactiveRoot().proxy;
2917
+ return this.source[name] != null ? getModelById(root, Type, this.source[name]) : void 0;
2836
2918
  }
2837
2919
  getModelRefs(name) {
2838
2920
  const a = this.getReferencedAtom(name);
@@ -2841,7 +2923,9 @@ class ModelAdministration extends PreactObjectAdministration {
2841
2923
  if (!this.referencedModels) this.referencedModels = /* @__PURE__ */ new Map();
2842
2924
  c = createComputed(() => {
2843
2925
  a.reportObserved();
2844
- const models = (this.source[name] || []).map((id2) => getModelById(this.root.proxy, id2)).filter((m) => !!m);
2926
+ const Type = this.getRequiredModelRefType(name);
2927
+ const root = this.getReactiveRoot().proxy;
2928
+ const models = (this.source[name] || []).map((id2) => getModelById(root, Type, id2)).filter((m) => !!m);
2845
2929
  return models;
2846
2930
  });
2847
2931
  this.referencedModels.set(name, c);
@@ -2851,6 +2935,7 @@ class ModelAdministration extends PreactObjectAdministration {
2851
2935
  setModelRef(name, modelValue) {
2852
2936
  let id2 = void 0;
2853
2937
  if (modelValue) {
2938
+ this.assertModelRefType(name, modelValue);
2854
2939
  id2 = getIdentifier(modelValue);
2855
2940
  if (id2 == null) {
2856
2941
  throw new Error(
@@ -2863,6 +2948,7 @@ class ModelAdministration extends PreactObjectAdministration {
2863
2948
  }
2864
2949
  setModelRefs(name, modelValue) {
2865
2950
  const ids = modelValue.map((model2) => {
2951
+ this.assertModelRefType(name, model2);
2866
2952
  const id2 = getIdentifier(model2);
2867
2953
  if (id2 == null) {
2868
2954
  throw new Error(
@@ -2874,31 +2960,87 @@ class ModelAdministration extends PreactObjectAdministration {
2874
2960
  this.source[name] = ids;
2875
2961
  this.referencedAtoms?.get(name)?.reportChanged();
2876
2962
  }
2963
+ getRequiredModelRefType(name) {
2964
+ const Type = this.getCfgChildType(name);
2965
+ if (!Type) {
2966
+ throw new Error(
2967
+ `r-state-tree: modelRef '${String(name)}' requires a model constructor`
2968
+ );
2969
+ }
2970
+ return Type;
2971
+ }
2972
+ assertModelRefType(name, model2) {
2973
+ const Type = this.getRequiredModelRefType(name);
2974
+ if (!(model2 instanceof Type)) {
2975
+ throw new Error(
2976
+ `r-state-tree: modelRef '${String(name)}' must reference ${Type.name}`
2977
+ );
2978
+ }
2979
+ }
2877
2980
  attach(parent = null, parentName = null) {
2981
+ this.assertUsable();
2878
2982
  if (this.parent) {
2879
2983
  throw new Error(
2880
2984
  "r-state-tree: child model already attached to a parent. Did you mean to use modelRef?"
2881
2985
  );
2882
2986
  }
2883
- if (parent) {
2884
- this.parent = parent;
2885
- this.root = parent.root;
2886
- this.parentName = parentName;
2887
- }
2888
2987
  batch(() => {
2889
- onModelAttached(this.proxy);
2890
- this.proxy.modelDidAttach();
2988
+ if (parent) {
2989
+ this.parent = parent;
2990
+ this.root = parent.root;
2991
+ this.parentName = parentName;
2992
+ }
2993
+ try {
2994
+ onModelAttached(this.proxy);
2995
+ } catch (error) {
2996
+ this.parent = null;
2997
+ this.root = this;
2998
+ this.parentName = null;
2999
+ throw error;
3000
+ }
2891
3001
  });
2892
3002
  }
3003
+ getReactiveRoot() {
3004
+ const parent = this.parent;
3005
+ return parent ? parent.getReactiveRoot() : this;
3006
+ }
2893
3007
  detach() {
2894
3008
  batch(() => {
2895
- this.proxy.modelWillDetach();
2896
3009
  onModelDetached(this.proxy);
3010
+ this.contextCache.forEach((computed2) => computed2.clear());
3011
+ this.contextCache.clear();
3012
+ this.parent = null;
3013
+ this.parentName = null;
3014
+ this.root = this;
3015
+ });
3016
+ }
3017
+ dispose(internal = false) {
3018
+ if (this.disposed) return;
3019
+ if (!internal && this.parent) {
3020
+ throw new Error(
3021
+ "r-state-tree: cannot directly dispose an attached child model"
3022
+ );
3023
+ }
3024
+ batch(() => {
3025
+ this.activeModels.forEach((name) => {
3026
+ const child2 = this.proxy[name];
3027
+ if (Array.isArray(child2)) {
3028
+ child2.forEach((model2) => getModelAdm(model2).dispose(true));
3029
+ } else if (child2) {
3030
+ getModelAdm(child2).dispose(true);
3031
+ }
3032
+ });
3033
+ if (this.parent) onModelDetached(this.proxy);
3034
+ this.parent = null;
3035
+ this.parentName = null;
3036
+ this.root = this;
2897
3037
  });
3038
+ this.disposed = true;
3039
+ this.modelsTraceUnsub.forEach((dispose) => dispose());
3040
+ this.modelsTraceUnsub.clear();
2898
3041
  this.contextCache.forEach((computed2) => computed2.clear());
2899
3042
  this.contextCache.clear();
2900
- this.parent = null;
2901
- this.root = this;
3043
+ Array.from(this.ownedDisposers).forEach((dispose) => dispose());
2902
3044
  }
2903
3045
  getContextValue(contextId, provideSymbol, defaultValue, hasDefault) {
2904
3046
  let computed2 = this.contextCache.get(contextId);
@@ -3001,85 +3143,94 @@ class ModelAdministration extends PreactObjectAdministration {
3001
3143
  }
3002
3144
  return true;
3003
3145
  };
3004
- onSnapshotLoad(() => {
3005
- untracked(() => {
3006
- batch(() => {
3007
- Object.keys(snapshot).forEach((key) => {
3008
- const type = this.getCfgType(key);
3009
- const childType2 = this.getCfgChildType(key);
3010
- const value = snapshot[key];
3011
- switch (type) {
3012
- case ModelCfgTypes.state:
3013
- this.proxy[key] = this.hydrateStateValue(this.proxy[key], value);
3014
- break;
3015
- case ModelCfgTypes.modelRef:
3016
- if (Array.isArray(value)) {
3017
- if (value?.[0] instanceof Model) {
3018
- this.proxy[key] = value;
3019
- } else {
3020
- this.source[key] = value.map(
3021
- (snapshot2) => getSnapshotRefId(snapshot2)
3022
- );
3023
- this.referencedAtoms?.get(key)?.reportChanged();
3024
- }
3025
- break;
3026
- } else if (value instanceof Model) {
3146
+ untracked(() => {
3147
+ batch(() => {
3148
+ Object.keys(snapshot).forEach((key) => {
3149
+ const type = this.getCfgType(key);
3150
+ const childType2 = this.getCfgChildType(key);
3151
+ const value = snapshot[key];
3152
+ switch (type) {
3153
+ case ModelCfgTypes.state:
3154
+ this.proxy[key] = this.hydrateStateValue(this.proxy[key], value);
3155
+ break;
3156
+ case ModelCfgTypes.modelRef:
3157
+ if (Array.isArray(value)) {
3158
+ if (value?.[0] instanceof Model) {
3027
3159
  this.proxy[key] = value;
3028
3160
  } else {
3029
- this.source[key] = getSnapshotRefId(value);
3161
+ this.source[key] = value.map(
3162
+ (snapshot2) => getSnapshotRefId(snapshot2)
3163
+ );
3030
3164
  this.referencedAtoms?.get(key)?.reportChanged();
3031
3165
  }
3032
3166
  break;
3033
- case ModelCfgTypes.id:
3034
- this.setId(key, value);
3035
- break;
3036
- case CommonCfgTypes.child:
3037
- let model2;
3038
- if (Array.isArray(value)) {
3039
- const Ctor = childType2;
3040
- this.proxy[key] = value?.map(
3041
- (snapshot2, index) => {
3042
- snapshot2 = snapshot2 ?? {};
3043
- let model22;
3044
- if (snapshot2 instanceof Model) {
3045
- model22 = snapshot2;
3167
+ } else if (value instanceof Model) {
3168
+ this.proxy[key] = value;
3169
+ } else {
3170
+ this.source[key] = getSnapshotRefId(value);
3171
+ this.referencedAtoms?.get(key)?.reportChanged();
3172
+ }
3173
+ break;
3174
+ case ModelCfgTypes.id:
3175
+ this.setId(key, value);
3176
+ break;
3177
+ case CommonCfgTypes.child:
3178
+ let model2;
3179
+ if (Array.isArray(value)) {
3180
+ const Ctor = childType2;
3181
+ const snapshotIds = /* @__PURE__ */ new Set();
3182
+ for (const childSnapshot of value) {
3183
+ if (childSnapshot instanceof Model) continue;
3184
+ const snapshotId = childType2 ? getSnapshotId(childSnapshot ?? {}, Ctor) : null;
3185
+ if (snapshotId != null && snapshotIds.has(snapshotId)) {
3186
+ throw new Error(
3187
+ "r-state-tree duplicate ids detected after snapshot was loaded"
3188
+ );
3189
+ }
3190
+ if (snapshotId != null) snapshotIds.add(snapshotId);
3191
+ }
3192
+ this.proxy[key] = value?.map(
3193
+ (snapshot2, index) => {
3194
+ snapshot2 = snapshot2 ?? {};
3195
+ let model22;
3196
+ if (snapshot2 instanceof Model) {
3197
+ model22 = snapshot2;
3198
+ } else {
3199
+ ensureChildTypes(key);
3200
+ const id2 = childType2 && getSnapshotId(snapshot2, Ctor);
3201
+ const foundModel = id2 != null ? getModelById(this.root.proxy, Ctor, id2) : this.proxy[key][index];
3202
+ const adm = foundModel && getModelAdm(foundModel);
3203
+ if (adm && foundModel.parent === this.proxy && adm?.parentName === key) {
3204
+ adm.loadSnapshot(snapshot2);
3205
+ model22 = foundModel;
3046
3206
  } else {
3047
- ensureChildTypes(key);
3048
- const id2 = childType2 && getSnapshotId(snapshot2, Ctor);
3049
- const foundModel = id2 != null ? getModelById(this.root.proxy, id2) : this.proxy[key][index];
3050
- const adm = foundModel && getModelAdm(foundModel);
3051
- if (adm && foundModel.parent === this.proxy && adm?.parentName === key) {
3052
- adm.loadSnapshot(snapshot2);
3053
- model22 = foundModel;
3054
- } else {
3055
- model22 = Ctor.create(snapshot2);
3056
- }
3207
+ model22 = Ctor.create(snapshot2);
3057
3208
  }
3058
- return model22;
3059
3209
  }
3060
- );
3061
- break;
3062
- } else if (value instanceof Model) {
3063
- model2 = value;
3064
- } else {
3065
- ensureChildTypes(key);
3066
- const id2 = childType2 && getSnapshotId(value, childType2);
3067
- if (id2 != null && this.proxy[key] && id2 === getIdentifier(this.proxy[key])) {
3068
- const adm = getModelAdm(this.proxy[key]);
3069
- adm.loadSnapshot(value);
3070
- model2 = this.proxy[key];
3071
- } else {
3072
- model2 = childType2.create(value);
3210
+ return model22;
3073
3211
  }
3074
- }
3075
- this.proxy[key] = model2;
3076
- break;
3077
- default:
3078
- console.warn(
3079
- `r-state-tree: invalid key '${key}' found in snapshot, ignored.`
3080
3212
  );
3081
- }
3082
- });
3213
+ break;
3214
+ } else if (value instanceof Model) {
3215
+ model2 = value;
3216
+ } else {
3217
+ ensureChildTypes(key);
3218
+ const id2 = childType2 && getSnapshotId(value, childType2);
3219
+ if (id2 != null && this.proxy[key] && this.proxy[key] instanceof childType2 && id2 === getIdentifier(this.proxy[key])) {
3220
+ const adm = getModelAdm(this.proxy[key]);
3221
+ adm.loadSnapshot(value);
3222
+ model2 = this.proxy[key];
3223
+ } else {
3224
+ model2 = childType2.create(value);
3225
+ }
3226
+ }
3227
+ this.proxy[key] = model2;
3228
+ break;
3229
+ default:
3230
+ console.warn(
3231
+ `r-state-tree: invalid key '${key}' found in snapshot, ignored.`
3232
+ );
3233
+ }
3083
3234
  });
3084
3235
  });
3085
3236
  });
@@ -3094,11 +3245,14 @@ class ModelAdministration extends PreactObjectAdministration {
3094
3245
  }
3095
3246
  return this.computedSnapshot.get();
3096
3247
  }
3248
+ getSnapshotForRollback() {
3249
+ return this.toJSON();
3250
+ }
3097
3251
  }
3098
3252
  let initEnabled = false;
3099
3253
  class Model {
3100
3254
  static childTypes = {};
3101
- static create(snapshot, ...args) {
3255
+ static create(snapshot) {
3102
3256
  let instance;
3103
3257
  try {
3104
3258
  initEnabled = true;
@@ -3107,8 +3261,12 @@ class Model {
3107
3261
  initEnabled = false;
3108
3262
  }
3109
3263
  const adm = getModelAdm(instance);
3110
- snapshot && adm.loadSnapshot(snapshot);
3111
- instance.modelDidInit(snapshot, ...args);
3264
+ try {
3265
+ snapshot && adm.loadSnapshot(snapshot);
3266
+ } catch (error) {
3267
+ adm.dispose(true);
3268
+ throw error;
3269
+ }
3112
3270
  return instance;
3113
3271
  }
3114
3272
  constructor() {
@@ -3132,31 +3290,33 @@ class Model {
3132
3290
  get parent() {
3133
3291
  return getModelAdm(this).parent?.proxy ?? null;
3134
3292
  }
3135
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3136
- modelDidInit(snapshot, ...args) {
3293
+ reaction(track, callback) {
3294
+ return getModelAdm(this).reaction(track, callback);
3137
3295
  }
3138
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3139
- modelDidAttach() {
3296
+ effect(callback) {
3297
+ return getModelAdm(this).effect(callback);
3140
3298
  }
3141
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3142
- modelWillDetach() {
3299
+ [Symbol.dispose]() {
3300
+ getModelAdm(this).dispose();
3143
3301
  }
3144
3302
  }
3303
+ function findModelById(root, ModelType, id2) {
3304
+ return getModelById(getModelAdm(root).root.proxy, ModelType, id2);
3305
+ }
3145
3306
  function mount(container) {
3146
3307
  return allowNewStore(() => {
3147
3308
  const element = container;
3148
3309
  const s = new element.Type(element.props);
3149
- getStoreAdm(s).mount();
3310
+ const adm = getStoreAdm(s);
3311
+ try {
3312
+ adm.mount();
3313
+ } catch (error) {
3314
+ adm.dispose(true);
3315
+ throw error;
3316
+ }
3150
3317
  return s;
3151
3318
  });
3152
3319
  }
3153
- function unmount(container) {
3154
- const internalStore = getStoreAdm(container);
3155
- if (!internalStore.isRoot()) {
3156
- throw new Error("r-state-tree: can only unmount root stores");
3157
- }
3158
- internalStore.unmount();
3159
- }
3160
3320
  function toSnapshot(model2) {
3161
3321
  return getModelAdm(model2).getSnapshot();
3162
3322
  }
@@ -3176,7 +3336,13 @@ function onSnapshotDiff(model2, callback) {
3176
3336
  }
3177
3337
  function applySnapshot(model2, snapshot) {
3178
3338
  const adm = getModelAdm(model2);
3179
- adm.loadSnapshot(snapshot);
3339
+ const previousSnapshot = adm.getSnapshotForRollback();
3340
+ try {
3341
+ adm.loadSnapshot(snapshot);
3342
+ } catch (error) {
3343
+ adm.loadSnapshot(previousSnapshot);
3344
+ throw error;
3345
+ }
3180
3346
  return model2;
3181
3347
  }
3182
3348
  const CONTEXT_SYMBOL = Symbol("context");
@@ -3283,6 +3449,7 @@ export {
3283
3449
  createContext,
3284
3450
  createStore,
3285
3451
  effect2 as effect,
3452
+ findModelById,
3286
3453
  getSignal,
3287
3454
  id,
3288
3455
  isObservable,
@@ -3300,7 +3467,6 @@ export {
3300
3467
  state,
3301
3468
  toObservableTree,
3302
3469
  toSnapshot,
3303
- unmount,
3304
3470
  untracked2 as untracked,
3305
3471
  updateStore
3306
3472
  };