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