r-state-tree 0.7.0 → 0.8.1

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
+ reactiveRegistrations = /* @__PURE__ */ new Set();
2025
2039
  configurationGetter;
2026
2040
  setConfiguration(configurationGetter) {
2027
2041
  this.configurationGetter = configurationGetter;
@@ -2045,7 +2059,9 @@ class StoreAdministration extends PreactObjectAdministration {
2045
2059
  }
2046
2060
  });
2047
2061
  childStoreData.value.set(stores);
2048
- stores.forEach((s) => getStoreAdm(s).mount(this, name));
2062
+ if (this.isMounted) {
2063
+ stores.forEach((s) => getStoreAdm(s).mount(this, name));
2064
+ }
2049
2065
  return stores;
2050
2066
  }
2051
2067
  const newStores = /* @__PURE__ */ new Set();
@@ -2095,8 +2111,10 @@ class StoreAdministration extends PreactObjectAdministration {
2095
2111
  if (newStores.size || removedStores.size || keyedIndexChanged) {
2096
2112
  batch(() => childStoreData.value.set(stores));
2097
2113
  }
2098
- removedStores.forEach((s) => getStoreAdm(s).unmount());
2099
- newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2114
+ removedStores.forEach((s) => getStoreAdm(s).dispose(true));
2115
+ if (this.isMounted) {
2116
+ newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2117
+ }
2100
2118
  return stores;
2101
2119
  }
2102
2120
  setSingleStore(name, element) {
@@ -2106,16 +2124,18 @@ class StoreAdministration extends PreactObjectAdministration {
2106
2124
  );
2107
2125
  const { key, Type, props } = element || {};
2108
2126
  if (!element) {
2109
- oldStore && getStoreAdm(oldStore).unmount();
2127
+ oldStore && getStoreAdm(oldStore).dispose(true);
2110
2128
  batch(() => childStoreData.value.set(null));
2111
2129
  return null;
2112
2130
  } else if (!oldStore || oldStore.props.key !== key || !(oldStore instanceof Type)) {
2113
2131
  if (oldStore) {
2114
- getStoreAdm(oldStore).unmount();
2132
+ getStoreAdm(oldStore).dispose(true);
2115
2133
  }
2116
2134
  const childStore = this.createChildStore(element);
2117
2135
  batch(() => childStoreData.value.set(childStore));
2118
- getStoreAdm(childStore).mount(this, name);
2136
+ if (this.isMounted) {
2137
+ getStoreAdm(childStore).mount(this, name);
2138
+ }
2119
2139
  return childStore;
2120
2140
  } else {
2121
2141
  batch(() => updateProps(oldStore.props, props));
@@ -2155,7 +2175,8 @@ class StoreAdministration extends PreactObjectAdministration {
2155
2175
  getStore(name) {
2156
2176
  const childStoreData = this.childStoreDataMap.get(name);
2157
2177
  if (!childStoreData) {
2158
- return this.initializeStore(name);
2178
+ untracked(() => this.initializeStore(name));
2179
+ return this.childStoreDataMap.get(name).value.get();
2159
2180
  } else {
2160
2181
  const storeElement = untracked(() => childStoreData.computed.get());
2161
2182
  validateStoreChildValue(storeElement, name);
@@ -2171,6 +2192,9 @@ class StoreAdministration extends PreactObjectAdministration {
2171
2192
  isRoot() {
2172
2193
  return !this.parent;
2173
2194
  }
2195
+ get isMounted() {
2196
+ return this.mountedSignal.get();
2197
+ }
2174
2198
  getContextValue(contextId, provideSymbol, defaultValue, hasDefault) {
2175
2199
  let computed2 = this.contextCache.get(contextId);
2176
2200
  if (!computed2) {
@@ -2204,12 +2228,43 @@ class StoreAdministration extends PreactObjectAdministration {
2204
2228
  }
2205
2229
  return void 0;
2206
2230
  }
2231
+ register(start) {
2232
+ if (this.disposed) {
2233
+ throw new Error(
2234
+ "r-state-tree: cannot register reactive resources on a disposed store"
2235
+ );
2236
+ }
2237
+ const registration = { start, disposed: false };
2238
+ this.reactiveRegistrations.add(registration);
2239
+ if (this.isMounted) registration.stop = registration.start();
2240
+ return () => {
2241
+ if (registration.disposed) return;
2242
+ registration.disposed = true;
2243
+ this.reactiveRegistrations.delete(registration);
2244
+ registration.stop?.();
2245
+ registration.stop = void 0;
2246
+ };
2247
+ }
2248
+ startReactiveRegistrations() {
2249
+ this.reactiveRegistrations.forEach((registration) => {
2250
+ if (!registration.disposed && !registration.stop) {
2251
+ registration.stop = registration.start();
2252
+ }
2253
+ });
2254
+ }
2207
2255
  reaction(track, callback) {
2208
- const unsub = reaction(track, callback);
2209
- this.reactionsUnsub.push(unsub);
2210
- return unsub;
2256
+ return this.register(() => reaction(track, callback));
2257
+ }
2258
+ effect(callback) {
2259
+ return this.register(() => effect(callback));
2211
2260
  }
2212
- mount(parent = null, childName) {
2261
+ mount(parent = null, childName, activationQueue) {
2262
+ if (this.disposed) {
2263
+ throw new Error("r-state-tree: cannot mount a disposed store");
2264
+ }
2265
+ if (this.isMounted) {
2266
+ throw new Error("r-state-tree: store is already mounted");
2267
+ }
2213
2268
  const frame = {
2214
2269
  storeName: this.proxy.constructor.name || "Store",
2215
2270
  childName,
@@ -2219,20 +2274,31 @@ class StoreAdministration extends PreactObjectAdministration {
2219
2274
  throw createCircularMountError(frame);
2220
2275
  }
2221
2276
  mountingStack.push(frame);
2277
+ const isOutermostMount = activationQueue === void 0;
2278
+ const registrationsToStart = activationQueue ?? [];
2222
2279
  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
- }
2280
+ batch(() => {
2281
+ this.parent = parent || null;
2282
+ this.childStoreDataMap.forEach(({ value }, name) => {
2283
+ const stores = value.get();
2284
+ if (Array.isArray(stores)) {
2285
+ stores?.forEach(
2286
+ (s) => getStoreAdm(s)?.mount(this, name, registrationsToStart)
2287
+ );
2288
+ } else if (stores) {
2289
+ getStoreAdm(stores)?.mount(this, name, registrationsToStart);
2290
+ }
2291
+ });
2292
+ this.mountedSignal.set(true);
2293
+ registrationsToStart.push(this);
2231
2294
  });
2232
- this.mounted = true;
2233
- batch(() => this.proxy.storeDidMount?.());
2295
+ if (isOutermostMount) {
2296
+ registrationsToStart.forEach(
2297
+ (store) => store.startReactiveRegistrations()
2298
+ );
2299
+ }
2234
2300
  } catch (error) {
2235
- if (error instanceof RangeError && /call stack/i.test(error.message)) {
2301
+ if (error instanceof Error && (error instanceof RangeError && /call stack/i.test(error.message) || /cycle detected/i.test(error.message))) {
2236
2302
  throw createCircularMountError(frame);
2237
2303
  }
2238
2304
  throw error;
@@ -2240,25 +2306,36 @@ class StoreAdministration extends PreactObjectAdministration {
2240
2306
  mountingStack.pop();
2241
2307
  }
2242
2308
  }
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();
2309
+ dispose(internal = false) {
2310
+ if (this.disposed) return;
2311
+ if (!internal && !this.isRoot()) {
2312
+ throw new Error("r-state-tree: can only dispose root stores");
2313
+ }
2314
+ batch(() => {
2315
+ this.childStoreDataMap.forEach((data) => {
2316
+ const { value, computed: computed2, listener } = data;
2317
+ const stores = value.get();
2318
+ if (Array.isArray(stores)) {
2319
+ stores?.forEach((s) => getStoreAdm(s)?.dispose(true));
2320
+ } else if (stores) {
2321
+ getStoreAdm(stores)?.dispose(true);
2322
+ }
2323
+ computed2.clear();
2324
+ listener.dispose();
2325
+ });
2326
+ this.childStoreDataMap.clear();
2327
+ this.contextCache.forEach((computed2) => computed2.clear());
2328
+ this.contextCache.clear();
2329
+ this.parent = null;
2256
2330
  });
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;
2331
+ this.disposed = true;
2332
+ this.reactiveRegistrations.forEach((registration) => {
2333
+ registration.disposed = true;
2334
+ registration.stop?.();
2335
+ registration.stop = void 0;
2336
+ });
2337
+ this.reactiveRegistrations.clear();
2338
+ this.mountedSignal.set(false);
2262
2339
  }
2263
2340
  }
2264
2341
  let initEnabled$1 = false;
@@ -2304,141 +2381,109 @@ class Store {
2304
2381
  get key() {
2305
2382
  return this.props.key;
2306
2383
  }
2384
+ get isMounted() {
2385
+ return getStoreAdm(this).isMounted;
2386
+ }
2307
2387
  reaction(track, callback) {
2308
2388
  return getStoreAdm(this).reaction(track, callback);
2309
2389
  }
2310
- // eslint-disable-next-line @typescript-eslint/no-empty-function
2311
- storeDidMount() {
2390
+ effect(callback) {
2391
+ return getStoreAdm(this).effect(callback);
2312
2392
  }
2313
- // eslint-disable-next-line @typescript-eslint/no-empty-function
2314
- storeWillUnmount() {
2393
+ [Symbol.dispose]() {
2394
+ getStoreAdm(this).dispose();
2315
2395
  }
2316
2396
  }
2317
2397
  const attachedIdMap = /* @__PURE__ */ new WeakMap();
2318
2398
  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
- }
2399
+ function getModelType(model2) {
2400
+ return Object.getPrototypeOf(model2).constructor;
2401
+ }
2402
+ function entriesFor(model2) {
2403
+ const entries = [];
2404
+ attachedIdMap.get(model2)?.forEach((ids, Type) => {
2405
+ ids.forEach((value, id22) => {
2406
+ if (value) entries.push([Type, id22, value]);
2407
+ });
2408
+ });
2409
+ const id2 = idMap.get(model2);
2410
+ if (id2 != null) entries.push([getModelType(model2), id2, model2]);
2411
+ return entries;
2412
+ }
2413
+ function assertAvailable(node, entries) {
2414
+ const map = attachedIdMap.get(node);
2415
+ for (const [Type, id2, model2] of entries) {
2416
+ const existing = map?.get(Type)?.get(id2);
2417
+ if (existing && existing !== model2) {
2418
+ throw new Error(
2419
+ `r-state-tree: id: ${id2} is already assigned to another model`
2420
+ );
2351
2421
  }
2352
2422
  }
2353
2423
  }
2424
+ function bucket(node, Type) {
2425
+ let map = attachedIdMap.get(node);
2426
+ if (!map) {
2427
+ map = observable(/* @__PURE__ */ new Map());
2428
+ attachedIdMap.set(node, map);
2429
+ }
2430
+ let ids = map.get(Type);
2431
+ if (!ids) {
2432
+ ids = observable(/* @__PURE__ */ new Map());
2433
+ map.set(Type, ids);
2434
+ }
2435
+ return ids;
2436
+ }
2354
2437
  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);
2438
+ const previousId = idMap.get(model2);
2439
+ if (previousId === id2) return;
2440
+ const Type = getModelType(model2);
2441
+ const ancestors = [];
2442
+ let node = model2.parent;
2443
+ while (node) {
2444
+ assertAvailable(node, [[Type, id2, model2]]);
2445
+ ancestors.push(node);
2446
+ node = node.parent;
2363
2447
  }
2448
+ for (const ancestor of ancestors) {
2449
+ const ids = bucket(ancestor, Type);
2450
+ if (previousId != null && ids.get(previousId) === model2)
2451
+ ids.delete(previousId);
2452
+ ids.set(id2, model2);
2453
+ }
2454
+ idMap.set(model2, id2);
2364
2455
  }
2365
2456
  function getIdentifier(model2) {
2366
2457
  return idMap.get(model2);
2367
2458
  }
2368
- function getModelById(root, id2) {
2369
- const map = attachedIdMap.get(root);
2370
- return map?.get(id2);
2459
+ function getModelById(root, Type, id2) {
2460
+ return attachedIdMap.get(root)?.get(Type)?.get(id2);
2371
2461
  }
2372
- function updateIdentifier(model2) {
2373
- const id2 = idMap.get(model2);
2462
+ function onModelAttached(model2) {
2463
+ const entries = entriesFor(model2);
2464
+ if (!entries.length) return;
2465
+ const ancestors = [];
2374
2466
  let node = model2.parent;
2375
2467
  while (node) {
2376
- const map = attachedIdMap.get(node);
2377
- if (map) {
2378
- map.set(id2, model2);
2379
- }
2468
+ assertAvailable(node, entries);
2469
+ ancestors.push(node);
2380
2470
  node = node.parent;
2381
2471
  }
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
- }
2472
+ for (const ancestor of ancestors) {
2473
+ for (const [Type, id2, value] of entries)
2474
+ bucket(ancestor, Type).set(id2, value);
2423
2475
  }
2424
2476
  }
2425
2477
  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;
2478
+ const entries = entriesFor(model2);
2479
+ let node = model2.parent;
2480
+ while (node) {
2481
+ const map = attachedIdMap.get(node);
2482
+ for (const [Type, id2, value] of entries) {
2483
+ const ids = map?.get(Type);
2484
+ if (ids?.get(id2) === value) ids.delete(id2);
2441
2485
  }
2486
+ node = node.parent;
2442
2487
  }
2443
2488
  }
2444
2489
  const listenerMap = /* @__PURE__ */ new WeakMap();
@@ -2494,19 +2539,28 @@ class ObservableListener {
2494
2539
  notify(ev) {
2495
2540
  if (!this.listeners) return;
2496
2541
  this.notifying = true;
2497
- for (let i = 0; i < this.listeners.length; i++) {
2498
- this.listeners[i](ev);
2542
+ try {
2543
+ for (let i = 0; i < this.listeners.length; i++) {
2544
+ this.listeners[i](ev);
2545
+ }
2546
+ } finally {
2547
+ this.notifying = false;
2499
2548
  }
2500
- this.notifying = false;
2501
2549
  }
2502
2550
  }
2503
2551
  class ChildModelsAdministration extends ArrayAdministration {
2504
2552
  set(index, newValue) {
2505
2553
  return batch(() => {
2554
+ const oldValue = this.source[index];
2506
2555
  const result = super.set(index, newValue);
2507
2556
  const sourceValue = getSource(newValue);
2508
- if (this.source[index] !== sourceValue) {
2509
- notifyArrayUpdate(this.proxy, index, this.source[index], sourceValue);
2557
+ if (oldValue !== sourceValue) {
2558
+ try {
2559
+ notifyArrayUpdate(this.proxy, index, oldValue, sourceValue);
2560
+ } catch (error) {
2561
+ super.set(index, oldValue);
2562
+ throw error;
2563
+ }
2510
2564
  }
2511
2565
  return result;
2512
2566
  });
@@ -2515,7 +2569,12 @@ class ChildModelsAdministration extends ArrayAdministration {
2515
2569
  return batch(() => {
2516
2570
  const deleted = super.spliceWithArray(index, deleteCount, newItems);
2517
2571
  if (deleteCount || newItems?.length) {
2518
- notifySpliceArray(this.proxy, index, newItems ?? [], deleted);
2572
+ try {
2573
+ notifySpliceArray(this.proxy, index, newItems ?? [], deleted);
2574
+ } catch (error) {
2575
+ super.spliceWithArray(index, newItems?.length ?? 0, deleted);
2576
+ throw error;
2577
+ }
2519
2578
  }
2520
2579
  return deleted;
2521
2580
  });
@@ -2594,6 +2653,9 @@ class ModelAdministration extends PreactObjectAdministration {
2594
2653
  {
2595
2654
  get(target, prop, proxy) {
2596
2655
  const adm = getAdministration(target);
2656
+ if (prop === Symbol.dispose) {
2657
+ return Reflect.get(target, prop, proxy);
2658
+ }
2597
2659
  if (prop === "parent") {
2598
2660
  return target.parent;
2599
2661
  }
@@ -2612,6 +2674,7 @@ class ModelAdministration extends PreactObjectAdministration {
2612
2674
  },
2613
2675
  set(target, name, value) {
2614
2676
  const adm = getAdministration(target);
2677
+ adm.assertUsable();
2615
2678
  adm.writeInProgress.add(name);
2616
2679
  try {
2617
2680
  switch (adm.getCfgType(name)) {
@@ -2674,6 +2737,8 @@ class ModelAdministration extends PreactObjectAdministration {
2674
2737
  computedSnapshot;
2675
2738
  snapshotMap = /* @__PURE__ */ new Map();
2676
2739
  contextCache = /* @__PURE__ */ new Map();
2740
+ ownedDisposers = /* @__PURE__ */ new Set();
2741
+ disposed = false;
2677
2742
  parentName = null;
2678
2743
  get parent() {
2679
2744
  this.parentAtom.reportObserved();
@@ -2688,6 +2753,29 @@ class ModelAdministration extends PreactObjectAdministration {
2688
2753
  setConfiguration(configurationGetter) {
2689
2754
  this.configurationGetter = configurationGetter;
2690
2755
  }
2756
+ assertUsable() {
2757
+ if (this.disposed) {
2758
+ throw new Error("r-state-tree: cannot use a disposed model");
2759
+ }
2760
+ }
2761
+ own(disposer) {
2762
+ this.assertUsable();
2763
+ let active = true;
2764
+ const wrapped = () => {
2765
+ if (!active) return;
2766
+ active = false;
2767
+ this.ownedDisposers.delete(wrapped);
2768
+ disposer();
2769
+ };
2770
+ this.ownedDisposers.add(wrapped);
2771
+ return wrapped;
2772
+ }
2773
+ reaction(track, callback) {
2774
+ return this.own(reaction(track, callback));
2775
+ }
2776
+ effect(callback) {
2777
+ return this.own(effect(callback));
2778
+ }
2691
2779
  get configuration() {
2692
2780
  return this.configurationGetter?.() ?? {};
2693
2781
  }
@@ -2757,8 +2845,8 @@ class ModelAdministration extends PreactObjectAdministration {
2757
2845
  );
2758
2846
  }
2759
2847
  if (v !== void 0) {
2760
- this.source[name] = v;
2761
2848
  setIdentifier(this.proxy, v);
2849
+ this.source[name] = v;
2762
2850
  }
2763
2851
  }
2764
2852
  setModel(name, newModel) {
@@ -2813,11 +2901,30 @@ class ModelAdministration extends PreactObjectAdministration {
2813
2901
  name,
2814
2902
  observe(this.proxy[name], (event) => {
2815
2903
  if (event.type === "updateArray") {
2816
- getModelAdm(event.oldValue).detach();
2817
- getModelAdm(event.newValue).attach(this, name);
2904
+ const oldAdm = getModelAdm(event.oldValue);
2905
+ const newAdm = getModelAdm(event.newValue);
2906
+ oldAdm.detach();
2907
+ try {
2908
+ newAdm.attach(this, name);
2909
+ } catch (error) {
2910
+ oldAdm.attach(this, name);
2911
+ throw error;
2912
+ }
2818
2913
  } else if (event.type === "spliceArray") {
2914
+ const attached = [];
2819
2915
  event.removed.forEach((model2) => getModelAdm(model2).detach());
2820
- event.added.forEach((model2) => getModelAdm(model2).attach(this, name));
2916
+ try {
2917
+ event.added.forEach((model2) => {
2918
+ getModelAdm(model2).attach(this, name);
2919
+ attached.push(model2);
2920
+ });
2921
+ } catch (error) {
2922
+ attached.forEach((model2) => getModelAdm(model2).detach());
2923
+ event.removed.forEach(
2924
+ (model2) => getModelAdm(model2).attach(this, name)
2925
+ );
2926
+ throw error;
2927
+ }
2821
2928
  }
2822
2929
  })
2823
2930
  );
@@ -2832,7 +2939,9 @@ class ModelAdministration extends PreactObjectAdministration {
2832
2939
  getModelRef(name) {
2833
2940
  const a = this.getReferencedAtom(name);
2834
2941
  a.reportObserved();
2835
- return this.source[name] != null ? getModelById(this.root.proxy, this.source[name]) : void 0;
2942
+ const Type = this.getRequiredModelRefType(name);
2943
+ const root = this.getReactiveRoot().proxy;
2944
+ return this.source[name] != null ? getModelById(root, Type, this.source[name]) : void 0;
2836
2945
  }
2837
2946
  getModelRefs(name) {
2838
2947
  const a = this.getReferencedAtom(name);
@@ -2841,7 +2950,9 @@ class ModelAdministration extends PreactObjectAdministration {
2841
2950
  if (!this.referencedModels) this.referencedModels = /* @__PURE__ */ new Map();
2842
2951
  c = createComputed(() => {
2843
2952
  a.reportObserved();
2844
- const models = (this.source[name] || []).map((id2) => getModelById(this.root.proxy, id2)).filter((m) => !!m);
2953
+ const Type = this.getRequiredModelRefType(name);
2954
+ const root = this.getReactiveRoot().proxy;
2955
+ const models = (this.source[name] || []).map((id2) => getModelById(root, Type, id2)).filter((m) => !!m);
2845
2956
  return models;
2846
2957
  });
2847
2958
  this.referencedModels.set(name, c);
@@ -2851,6 +2962,7 @@ class ModelAdministration extends PreactObjectAdministration {
2851
2962
  setModelRef(name, modelValue) {
2852
2963
  let id2 = void 0;
2853
2964
  if (modelValue) {
2965
+ this.assertModelRefType(name, modelValue);
2854
2966
  id2 = getIdentifier(modelValue);
2855
2967
  if (id2 == null) {
2856
2968
  throw new Error(
@@ -2863,6 +2975,7 @@ class ModelAdministration extends PreactObjectAdministration {
2863
2975
  }
2864
2976
  setModelRefs(name, modelValue) {
2865
2977
  const ids = modelValue.map((model2) => {
2978
+ this.assertModelRefType(name, model2);
2866
2979
  const id2 = getIdentifier(model2);
2867
2980
  if (id2 == null) {
2868
2981
  throw new Error(
@@ -2874,31 +2987,87 @@ class ModelAdministration extends PreactObjectAdministration {
2874
2987
  this.source[name] = ids;
2875
2988
  this.referencedAtoms?.get(name)?.reportChanged();
2876
2989
  }
2990
+ getRequiredModelRefType(name) {
2991
+ const Type = this.getCfgChildType(name);
2992
+ if (!Type) {
2993
+ throw new Error(
2994
+ `r-state-tree: modelRef '${String(name)}' requires a model constructor`
2995
+ );
2996
+ }
2997
+ return Type;
2998
+ }
2999
+ assertModelRefType(name, model2) {
3000
+ const Type = this.getRequiredModelRefType(name);
3001
+ if (!(model2 instanceof Type)) {
3002
+ throw new Error(
3003
+ `r-state-tree: modelRef '${String(name)}' must reference ${Type.name}`
3004
+ );
3005
+ }
3006
+ }
2877
3007
  attach(parent = null, parentName = null) {
3008
+ this.assertUsable();
2878
3009
  if (this.parent) {
2879
3010
  throw new Error(
2880
3011
  "r-state-tree: child model already attached to a parent. Did you mean to use modelRef?"
2881
3012
  );
2882
3013
  }
2883
- if (parent) {
2884
- this.parent = parent;
2885
- this.root = parent.root;
2886
- this.parentName = parentName;
2887
- }
2888
3014
  batch(() => {
2889
- onModelAttached(this.proxy);
2890
- this.proxy.modelDidAttach();
3015
+ if (parent) {
3016
+ this.parent = parent;
3017
+ this.root = parent.root;
3018
+ this.parentName = parentName;
3019
+ }
3020
+ try {
3021
+ onModelAttached(this.proxy);
3022
+ } catch (error) {
3023
+ this.parent = null;
3024
+ this.root = this;
3025
+ this.parentName = null;
3026
+ throw error;
3027
+ }
2891
3028
  });
2892
3029
  }
3030
+ getReactiveRoot() {
3031
+ const parent = this.parent;
3032
+ return parent ? parent.getReactiveRoot() : this;
3033
+ }
2893
3034
  detach() {
2894
3035
  batch(() => {
2895
- this.proxy.modelWillDetach();
2896
3036
  onModelDetached(this.proxy);
3037
+ this.contextCache.forEach((computed2) => computed2.clear());
3038
+ this.contextCache.clear();
3039
+ this.parent = null;
3040
+ this.parentName = null;
3041
+ this.root = this;
2897
3042
  });
3043
+ }
3044
+ dispose(internal = false) {
3045
+ if (this.disposed) return;
3046
+ if (!internal && this.parent) {
3047
+ throw new Error(
3048
+ "r-state-tree: cannot directly dispose an attached child model"
3049
+ );
3050
+ }
3051
+ batch(() => {
3052
+ this.activeModels.forEach((name) => {
3053
+ const child2 = this.proxy[name];
3054
+ if (Array.isArray(child2)) {
3055
+ child2.forEach((model2) => getModelAdm(model2).dispose(true));
3056
+ } else if (child2) {
3057
+ getModelAdm(child2).dispose(true);
3058
+ }
3059
+ });
3060
+ if (this.parent) onModelDetached(this.proxy);
3061
+ this.parent = null;
3062
+ this.parentName = null;
3063
+ this.root = this;
3064
+ });
3065
+ this.disposed = true;
3066
+ this.modelsTraceUnsub.forEach((dispose) => dispose());
3067
+ this.modelsTraceUnsub.clear();
2898
3068
  this.contextCache.forEach((computed2) => computed2.clear());
2899
3069
  this.contextCache.clear();
2900
- this.parent = null;
2901
- this.root = this;
3070
+ Array.from(this.ownedDisposers).forEach((dispose) => dispose());
2902
3071
  }
2903
3072
  getContextValue(contextId, provideSymbol, defaultValue, hasDefault) {
2904
3073
  let computed2 = this.contextCache.get(contextId);
@@ -3001,85 +3170,94 @@ class ModelAdministration extends PreactObjectAdministration {
3001
3170
  }
3002
3171
  return true;
3003
3172
  };
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) {
3173
+ untracked(() => {
3174
+ batch(() => {
3175
+ Object.keys(snapshot).forEach((key) => {
3176
+ const type = this.getCfgType(key);
3177
+ const childType2 = this.getCfgChildType(key);
3178
+ const value = snapshot[key];
3179
+ switch (type) {
3180
+ case ModelCfgTypes.state:
3181
+ this.proxy[key] = this.hydrateStateValue(this.proxy[key], value);
3182
+ break;
3183
+ case ModelCfgTypes.modelRef:
3184
+ if (Array.isArray(value)) {
3185
+ if (value?.[0] instanceof Model) {
3027
3186
  this.proxy[key] = value;
3028
3187
  } else {
3029
- this.source[key] = getSnapshotRefId(value);
3188
+ this.source[key] = value.map(
3189
+ (snapshot2) => getSnapshotRefId(snapshot2)
3190
+ );
3030
3191
  this.referencedAtoms?.get(key)?.reportChanged();
3031
3192
  }
3032
3193
  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;
3194
+ } else if (value instanceof Model) {
3195
+ this.proxy[key] = value;
3196
+ } else {
3197
+ this.source[key] = getSnapshotRefId(value);
3198
+ this.referencedAtoms?.get(key)?.reportChanged();
3199
+ }
3200
+ break;
3201
+ case ModelCfgTypes.id:
3202
+ this.setId(key, value);
3203
+ break;
3204
+ case CommonCfgTypes.child:
3205
+ let model2;
3206
+ if (Array.isArray(value)) {
3207
+ const Ctor = childType2;
3208
+ const snapshotIds = /* @__PURE__ */ new Set();
3209
+ for (const childSnapshot of value) {
3210
+ if (childSnapshot instanceof Model) continue;
3211
+ const snapshotId = childType2 ? getSnapshotId(childSnapshot ?? {}, Ctor) : null;
3212
+ if (snapshotId != null && snapshotIds.has(snapshotId)) {
3213
+ throw new Error(
3214
+ "r-state-tree duplicate ids detected after snapshot was loaded"
3215
+ );
3216
+ }
3217
+ if (snapshotId != null) snapshotIds.add(snapshotId);
3218
+ }
3219
+ this.proxy[key] = value?.map(
3220
+ (snapshot2, index) => {
3221
+ snapshot2 = snapshot2 ?? {};
3222
+ let model22;
3223
+ if (snapshot2 instanceof Model) {
3224
+ model22 = snapshot2;
3225
+ } else {
3226
+ ensureChildTypes(key);
3227
+ const id2 = childType2 && getSnapshotId(snapshot2, Ctor);
3228
+ const foundModel = id2 != null ? getModelById(this.root.proxy, Ctor, id2) : this.proxy[key][index];
3229
+ const adm = foundModel && getModelAdm(foundModel);
3230
+ if (adm && foundModel.parent === this.proxy && adm?.parentName === key) {
3231
+ adm.loadSnapshot(snapshot2);
3232
+ model22 = foundModel;
3046
3233
  } 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
- }
3234
+ model22 = Ctor.create(snapshot2);
3057
3235
  }
3058
- return model22;
3059
3236
  }
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);
3237
+ return model22;
3073
3238
  }
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
3239
  );
3081
- }
3082
- });
3240
+ break;
3241
+ } else if (value instanceof Model) {
3242
+ model2 = value;
3243
+ } else {
3244
+ ensureChildTypes(key);
3245
+ const id2 = childType2 && getSnapshotId(value, childType2);
3246
+ if (id2 != null && this.proxy[key] && this.proxy[key] instanceof childType2 && id2 === getIdentifier(this.proxy[key])) {
3247
+ const adm = getModelAdm(this.proxy[key]);
3248
+ adm.loadSnapshot(value);
3249
+ model2 = this.proxy[key];
3250
+ } else {
3251
+ model2 = childType2.create(value);
3252
+ }
3253
+ }
3254
+ this.proxy[key] = model2;
3255
+ break;
3256
+ default:
3257
+ console.warn(
3258
+ `r-state-tree: invalid key '${key}' found in snapshot, ignored.`
3259
+ );
3260
+ }
3083
3261
  });
3084
3262
  });
3085
3263
  });
@@ -3094,11 +3272,14 @@ class ModelAdministration extends PreactObjectAdministration {
3094
3272
  }
3095
3273
  return this.computedSnapshot.get();
3096
3274
  }
3275
+ getSnapshotForRollback() {
3276
+ return this.toJSON();
3277
+ }
3097
3278
  }
3098
3279
  let initEnabled = false;
3099
3280
  class Model {
3100
3281
  static childTypes = {};
3101
- static create(snapshot, ...args) {
3282
+ static create(snapshot) {
3102
3283
  let instance;
3103
3284
  try {
3104
3285
  initEnabled = true;
@@ -3107,8 +3288,12 @@ class Model {
3107
3288
  initEnabled = false;
3108
3289
  }
3109
3290
  const adm = getModelAdm(instance);
3110
- snapshot && adm.loadSnapshot(snapshot);
3111
- instance.modelDidInit(snapshot, ...args);
3291
+ try {
3292
+ snapshot && adm.loadSnapshot(snapshot);
3293
+ } catch (error) {
3294
+ adm.dispose(true);
3295
+ throw error;
3296
+ }
3112
3297
  return instance;
3113
3298
  }
3114
3299
  constructor() {
@@ -3132,31 +3317,33 @@ class Model {
3132
3317
  get parent() {
3133
3318
  return getModelAdm(this).parent?.proxy ?? null;
3134
3319
  }
3135
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3136
- modelDidInit(snapshot, ...args) {
3320
+ reaction(track, callback) {
3321
+ return getModelAdm(this).reaction(track, callback);
3137
3322
  }
3138
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3139
- modelDidAttach() {
3323
+ effect(callback) {
3324
+ return getModelAdm(this).effect(callback);
3140
3325
  }
3141
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3142
- modelWillDetach() {
3326
+ [Symbol.dispose]() {
3327
+ getModelAdm(this).dispose();
3143
3328
  }
3144
3329
  }
3330
+ function findModelById(root, ModelType, id2) {
3331
+ return getModelById(getModelAdm(root).root.proxy, ModelType, id2);
3332
+ }
3145
3333
  function mount(container) {
3146
3334
  return allowNewStore(() => {
3147
3335
  const element = container;
3148
3336
  const s = new element.Type(element.props);
3149
- getStoreAdm(s).mount();
3337
+ const adm = getStoreAdm(s);
3338
+ try {
3339
+ adm.mount();
3340
+ } catch (error) {
3341
+ adm.dispose(true);
3342
+ throw error;
3343
+ }
3150
3344
  return s;
3151
3345
  });
3152
3346
  }
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
3347
  function toSnapshot(model2) {
3161
3348
  return getModelAdm(model2).getSnapshot();
3162
3349
  }
@@ -3176,7 +3363,13 @@ function onSnapshotDiff(model2, callback) {
3176
3363
  }
3177
3364
  function applySnapshot(model2, snapshot) {
3178
3365
  const adm = getModelAdm(model2);
3179
- adm.loadSnapshot(snapshot);
3366
+ const previousSnapshot = adm.getSnapshotForRollback();
3367
+ try {
3368
+ adm.loadSnapshot(snapshot);
3369
+ } catch (error) {
3370
+ adm.loadSnapshot(previousSnapshot);
3371
+ throw error;
3372
+ }
3180
3373
  return model2;
3181
3374
  }
3182
3375
  const CONTEXT_SYMBOL = Symbol("context");
@@ -3283,6 +3476,7 @@ export {
3283
3476
  createContext,
3284
3477
  createStore,
3285
3478
  effect2 as effect,
3479
+ findModelById,
3286
3480
  getSignal,
3287
3481
  id,
3288
3482
  isObservable,
@@ -3300,7 +3494,6 @@ export {
3300
3494
  state,
3301
3495
  toObservableTree,
3302
3496
  toSnapshot,
3303
- unmount,
3304
3497
  untracked2 as untracked,
3305
3498
  updateStore
3306
3499
  };