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