jotai-state-tree 1.2.1 → 1.3.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.
package/README.md CHANGED
@@ -67,6 +67,7 @@ Explore our detailed, exhaustive guides to master `jotai-state-tree`:
67
67
  5. **[React Integration](docs/react-integration.md)** - Observables HOCs, typed context Providers, hooks (`useSnapshot`, `useWatchPath`), and update batching.
68
68
  6. **[Advanced Features](docs/advanced-features.md)** - Undo/Redo managers, Time Travel, Action recorders, dynamic plugins/registry, and middleware pipelines.
69
69
  7. **[Migration from MobX-State-Tree](docs/mst-migration.md)** - Step-by-step replacement guide, performance comparisons, and key differences.
70
+ 8. **[Examples & Templates](docs/examples-and-templates.md)** - 6 pre-configured Vite starter templates, features breakdown, and project scaffolding instructions.
70
71
 
71
72
  ---
72
73
 
@@ -617,6 +617,9 @@ function recordPatches(target) {
617
617
  };
618
618
  }
619
619
  var currentAction = null;
620
+ function getCurrentAction() {
621
+ return currentAction;
622
+ }
620
623
  var actionListeners = /* @__PURE__ */ new Set();
621
624
  var actionRecorderHooks = [];
622
625
  function registerActionRecorderHook(hook) {
@@ -882,6 +885,7 @@ export {
882
885
  onPatch,
883
886
  applyPatch,
884
887
  recordPatches,
888
+ getCurrentAction,
885
889
  registerActionRecorderHook,
886
890
  isActionRunning,
887
891
  trackAction,
package/dist/index.d.mts CHANGED
@@ -362,9 +362,14 @@ interface IMiddlewareHandler {
362
362
  (call: IMiddlewareEvent, next: (call: IMiddlewareEvent, callback?: (value: unknown) => unknown) => unknown, abort: (value: unknown) => unknown): unknown;
363
363
  }
364
364
  /**
365
- * Add middleware to the global stack
365
+ * Add middleware to the stack/node
366
366
  */
367
367
  declare function addMiddleware(target: unknown, handler: IMiddlewareHandler, includeHooks?: boolean): IDisposer;
368
+ /**
369
+ * Creates an async action (generator function) that can be yielded.
370
+ * Compatible with MST's flow().
371
+ */
372
+ declare function flow<Args extends unknown[], R>(generator: (...args: Args) => Generator<Promise<unknown>, R, unknown>): (...args: Args) => Promise<R>;
368
373
  interface ISerializedActionCall {
369
374
  name: string;
370
375
  path: string;
@@ -646,11 +651,6 @@ declare const types: {
646
651
  safeDynamicReference: typeof safeDynamicReference;
647
652
  };
648
653
 
649
- /**
650
- * Creates an async action (generator function) that can be yielded.
651
- * Compatible with MST's flow().
652
- */
653
- declare function flow<Args extends unknown[], R>(generator: (...args: Args) => Generator<Promise<unknown>, R, unknown>): (...args: Args) => Promise<R>;
654
654
  /**
655
655
  * Cast a value to a different type.
656
656
  * Useful for working around TypeScript limitations.
package/dist/index.d.ts CHANGED
@@ -362,9 +362,14 @@ interface IMiddlewareHandler {
362
362
  (call: IMiddlewareEvent, next: (call: IMiddlewareEvent, callback?: (value: unknown) => unknown) => unknown, abort: (value: unknown) => unknown): unknown;
363
363
  }
364
364
  /**
365
- * Add middleware to the global stack
365
+ * Add middleware to the stack/node
366
366
  */
367
367
  declare function addMiddleware(target: unknown, handler: IMiddlewareHandler, includeHooks?: boolean): IDisposer;
368
+ /**
369
+ * Creates an async action (generator function) that can be yielded.
370
+ * Compatible with MST's flow().
371
+ */
372
+ declare function flow<Args extends unknown[], R>(generator: (...args: Args) => Generator<Promise<unknown>, R, unknown>): (...args: Args) => Promise<R>;
368
373
  interface ISerializedActionCall {
369
374
  name: string;
370
375
  path: string;
@@ -646,11 +651,6 @@ declare const types: {
646
651
  safeDynamicReference: typeof safeDynamicReference;
647
652
  };
648
653
 
649
- /**
650
- * Creates an async action (generator function) that can be yielded.
651
- * Compatible with MST's flow().
652
- */
653
- declare function flow<Args extends unknown[], R>(generator: (...args: Args) => Generator<Promise<unknown>, R, unknown>): (...args: Args) => Promise<R>;
654
654
  /**
655
655
  * Cast a value to a different type.
656
656
  * Useful for working around TypeScript limitations.
package/dist/index.js CHANGED
@@ -1048,6 +1048,9 @@ function recordPatches(target) {
1048
1048
  };
1049
1049
  }
1050
1050
  var currentAction = null;
1051
+ function getCurrentAction() {
1052
+ return currentAction;
1053
+ }
1051
1054
  var actionListeners = /* @__PURE__ */ new Set();
1052
1055
  var actionRecorderHooks = [];
1053
1056
  function registerActionRecorderHook(hook) {
@@ -1318,8 +1321,26 @@ function runBeforeDestroy(node) {
1318
1321
  hooks.beforeDestroy();
1319
1322
  }
1320
1323
  }
1324
+ var activeMiddlewareEvent = null;
1325
+ var middlewareIdCounter = 0;
1321
1326
  var middlewareStack = [];
1327
+ var nodeMiddlewares = /* @__PURE__ */ new WeakMap();
1322
1328
  function addMiddleware(target, handler, includeHooks = true) {
1329
+ if (target && isStateTreeNode(target)) {
1330
+ const node = getStateTreeNode(target);
1331
+ let middlewares = nodeMiddlewares.get(node);
1332
+ if (!middlewares) {
1333
+ middlewares = /* @__PURE__ */ new Set();
1334
+ nodeMiddlewares.set(node, middlewares);
1335
+ }
1336
+ middlewares.add(handler);
1337
+ return () => {
1338
+ const current = nodeMiddlewares.get(node);
1339
+ if (current) {
1340
+ current.delete(handler);
1341
+ }
1342
+ };
1343
+ }
1323
1344
  middlewareStack.push(handler);
1324
1345
  return () => {
1325
1346
  const index = middlewareStack.indexOf(handler);
@@ -1328,6 +1349,199 @@ function addMiddleware(target, handler, includeHooks = true) {
1328
1349
  }
1329
1350
  };
1330
1351
  }
1352
+ function getMiddlewaresForNode(node) {
1353
+ const handlers = [];
1354
+ let current = node;
1355
+ while (current) {
1356
+ const middlewares = nodeMiddlewares.get(current);
1357
+ if (middlewares) {
1358
+ handlers.unshift(...Array.from(middlewares));
1359
+ }
1360
+ current = current.$parent;
1361
+ }
1362
+ return [...Array.from(middlewareStack), ...handlers];
1363
+ }
1364
+ function runMiddlewares(node, event, fn) {
1365
+ const handlers = getMiddlewaresForNode(node);
1366
+ if (handlers.length === 0) {
1367
+ const previousEvent = activeMiddlewareEvent;
1368
+ activeMiddlewareEvent = event;
1369
+ try {
1370
+ return fn(event);
1371
+ } finally {
1372
+ activeMiddlewareEvent = previousEvent;
1373
+ }
1374
+ }
1375
+ let index = 0;
1376
+ let aborted = false;
1377
+ let abortValue;
1378
+ const abort = (value) => {
1379
+ aborted = true;
1380
+ abortValue = value;
1381
+ return value;
1382
+ };
1383
+ const next = (call, callback) => {
1384
+ if (aborted) return abortValue;
1385
+ const previousEvent = activeMiddlewareEvent;
1386
+ activeMiddlewareEvent = call;
1387
+ try {
1388
+ if (index >= handlers.length) {
1389
+ const result = fn(call);
1390
+ return callback ? callback(result) : result;
1391
+ }
1392
+ const middleware = handlers[index++];
1393
+ return middleware(call, next, abort);
1394
+ } finally {
1395
+ activeMiddlewareEvent = previousEvent;
1396
+ }
1397
+ };
1398
+ return next(event);
1399
+ }
1400
+ function createMiddlewareRunner(node, actionName, args) {
1401
+ const id = ++middlewareIdCounter;
1402
+ const parentEvent = activeMiddlewareEvent;
1403
+ const event = {
1404
+ type: "action",
1405
+ name: actionName,
1406
+ id,
1407
+ parentId: parentEvent ? parentEvent.id : 0,
1408
+ rootId: parentEvent ? parentEvent.rootId : id,
1409
+ context: node.getInstance(),
1410
+ tree: node.getRoot().getInstance(),
1411
+ args,
1412
+ parentEvent: parentEvent ?? void 0
1413
+ };
1414
+ return (fn) => {
1415
+ return runMiddlewares(node, event, fn);
1416
+ };
1417
+ }
1418
+ function flow(generator) {
1419
+ return function flowAction(...args) {
1420
+ const node = (this && typeof this === "object" && $treenode in this ? this[$treenode] : null) ?? getCurrentAction()?.tree;
1421
+ if (!node) {
1422
+ let step2 = function(nextFn) {
1423
+ let result;
1424
+ try {
1425
+ result = nextFn();
1426
+ } catch (e) {
1427
+ return Promise.reject(e);
1428
+ }
1429
+ if (result.done) {
1430
+ return Promise.resolve(result.value);
1431
+ }
1432
+ return Promise.resolve(result.value).then(
1433
+ (value) => step2(() => gen2.next(value)),
1434
+ (error) => step2(() => gen2.throw(error))
1435
+ );
1436
+ };
1437
+ var step = step2;
1438
+ const gen2 = generator.apply(this, args);
1439
+ return step2(() => gen2.next(void 0));
1440
+ }
1441
+ const actionName = getCurrentAction()?.name ?? "anonymousFlow";
1442
+ const gen = generator.apply(this, args);
1443
+ const actionEvent = activeMiddlewareEvent;
1444
+ const spawnEventId = ++middlewareIdCounter;
1445
+ const spawnEvent = {
1446
+ type: "flow_spawn",
1447
+ name: actionName,
1448
+ id: spawnEventId,
1449
+ parentId: actionEvent ? actionEvent.id : 0,
1450
+ rootId: actionEvent ? actionEvent.rootId : spawnEventId,
1451
+ context: node.getInstance(),
1452
+ tree: node.getRoot().getInstance(),
1453
+ args,
1454
+ parentEvent: actionEvent ?? void 0
1455
+ };
1456
+ return new Promise((resolve, reject) => {
1457
+ runMiddlewares(node, spawnEvent, () => {
1458
+ function step2(nextFn, isResume, resumeError) {
1459
+ let result;
1460
+ if (isResume) {
1461
+ if (!node.$isAlive) {
1462
+ gen.return(void 0);
1463
+ handleThrow(new Error("FLOW_CANCELLED"));
1464
+ return;
1465
+ }
1466
+ const resumeEventId = ++middlewareIdCounter;
1467
+ const resumeEvent = {
1468
+ type: resumeError !== void 0 ? "flow_resume_error" : "flow_resume",
1469
+ name: actionName,
1470
+ id: resumeEventId,
1471
+ parentId: spawnEvent.id,
1472
+ rootId: spawnEvent.rootId,
1473
+ context: node.getInstance(),
1474
+ tree: node.getRoot().getInstance(),
1475
+ args: resumeError !== void 0 ? [resumeError] : [],
1476
+ parentEvent: spawnEvent
1477
+ };
1478
+ runMiddlewares(node, resumeEvent, () => {
1479
+ try {
1480
+ result = trackAction(node, actionName, args, () => {
1481
+ return nextFn();
1482
+ });
1483
+ } catch (e) {
1484
+ handleThrow(e);
1485
+ return;
1486
+ }
1487
+ handleResult(result);
1488
+ });
1489
+ } else {
1490
+ try {
1491
+ result = nextFn();
1492
+ } catch (e) {
1493
+ handleThrow(e);
1494
+ return;
1495
+ }
1496
+ handleResult(result);
1497
+ }
1498
+ }
1499
+ function handleResult(result) {
1500
+ if (result.done) {
1501
+ const returnEventId = ++middlewareIdCounter;
1502
+ const returnEvent = {
1503
+ type: "flow_return",
1504
+ name: actionName,
1505
+ id: returnEventId,
1506
+ parentId: spawnEvent.id,
1507
+ rootId: spawnEvent.rootId,
1508
+ context: node.getInstance(),
1509
+ tree: node.getRoot().getInstance(),
1510
+ args: [result.value],
1511
+ parentEvent: spawnEvent
1512
+ };
1513
+ runMiddlewares(node, returnEvent, () => {
1514
+ resolve(result.value);
1515
+ });
1516
+ } else {
1517
+ Promise.resolve(result.value).then(
1518
+ (value) => step2(() => gen.next(value), true),
1519
+ (error) => step2(() => gen.throw(error), true, error)
1520
+ );
1521
+ }
1522
+ }
1523
+ function handleThrow(error) {
1524
+ const throwEventId = ++middlewareIdCounter;
1525
+ const throwEvent = {
1526
+ type: "flow_throw",
1527
+ name: actionName,
1528
+ id: throwEventId,
1529
+ parentId: spawnEvent.id,
1530
+ rootId: spawnEvent.rootId,
1531
+ context: node.getInstance(),
1532
+ tree: node.getRoot().getInstance(),
1533
+ args: [error],
1534
+ parentEvent: spawnEvent
1535
+ };
1536
+ runMiddlewares(node, throwEvent, () => {
1537
+ reject(error);
1538
+ });
1539
+ }
1540
+ step2(() => gen.next(void 0), false);
1541
+ });
1542
+ });
1543
+ };
1544
+ }
1331
1545
  var actionRecorders = /* @__PURE__ */ new WeakMap();
1332
1546
  function recordActions(target) {
1333
1547
  const node = getStateTreeNode(target);
@@ -1406,6 +1620,9 @@ function isProtected(target) {
1406
1620
  return !isNodeUnprotected(node);
1407
1621
  }
1408
1622
  function canWrite(node) {
1623
+ if (!node.$isAlive) {
1624
+ return false;
1625
+ }
1409
1626
  if (typeof process !== "undefined" && process.env && process.env.NODE_ENV === "production") {
1410
1627
  return true;
1411
1628
  }
@@ -1578,6 +1795,9 @@ var ModelType = class _ModelType {
1578
1795
  if (prop === "$treenode") {
1579
1796
  return node;
1580
1797
  }
1798
+ if (!node.$isAlive) {
1799
+ throw new Error(`[jotai-state-tree] Cannot access '${String(prop)}' - the node is dead.`);
1800
+ }
1581
1801
  const propStr = String(prop);
1582
1802
  if (propertyAtoms.has(propStr)) {
1583
1803
  const childNode = node.getChild(propStr);
@@ -1614,6 +1834,9 @@ var ModelType = class _ModelType {
1614
1834
  set(target, prop, value) {
1615
1835
  const propStr = String(prop);
1616
1836
  if (propertyAtoms.has(propStr)) {
1837
+ if (!node.$isAlive) {
1838
+ throw new Error(`[jotai-state-tree] Cannot modify '${propStr}' - the node is dead.`);
1839
+ }
1617
1840
  if (!canWrite(node)) {
1618
1841
  throw new Error(`Cannot modify '${propStr}' - the object is protected and can only be modified inside an action.`);
1619
1842
  }
@@ -1665,6 +1888,9 @@ var ModelType = class _ModelType {
1665
1888
  return true;
1666
1889
  }
1667
1890
  if (propStr in node.volatileState) {
1891
+ if (!node.$isAlive) {
1892
+ throw new Error(`[jotai-state-tree] Cannot modify volatile property '${propStr}' - the node is dead.`);
1893
+ }
1668
1894
  if (!canWrite(node)) {
1669
1895
  throw new Error(`Cannot modify volatile property '${propStr}' - the object is protected and can only be modified inside an action.`);
1670
1896
  }
@@ -1714,8 +1940,11 @@ var ModelType = class _ModelType {
1714
1940
  for (const [key, value] of Object.entries(actions)) {
1715
1941
  if (typeof value === "function") {
1716
1942
  allActions[key] = (...args) => {
1717
- return trackAction(node, key, args, () => {
1718
- return value.apply(proxy, args);
1943
+ const runner = createMiddlewareRunner(node, key, args);
1944
+ return runner((currEvent) => {
1945
+ return trackAction(node, currEvent.name, currEvent.args, () => {
1946
+ return value.apply(proxy, currEvent.args);
1947
+ });
1719
1948
  });
1720
1949
  };
1721
1950
  if (key === "afterCreate" || key === "afterAttach" || key === "beforeDetach" || key === "beforeDestroy") {
@@ -2016,6 +2245,9 @@ var MSTArray = class _MSTArray extends Array {
2016
2245
  Object.setPrototypeOf(this, _MSTArray.prototype);
2017
2246
  }
2018
2247
  checkWrite() {
2248
+ if (!this.node.$isAlive) {
2249
+ throw new Error("[jotai-state-tree] Cannot modify array - the node is dead.");
2250
+ }
2019
2251
  if (!canWrite(this.node)) {
2020
2252
  throw new Error(
2021
2253
  `Cannot modify the array - the parent object is protected and can only be modified inside an action.`
@@ -2277,12 +2509,18 @@ var ArrayType = class {
2277
2509
  if (prop === $treenode) {
2278
2510
  return node;
2279
2511
  }
2512
+ if (!node.$isAlive) {
2513
+ throw new Error("[jotai-state-tree] Cannot access array - the node is dead.");
2514
+ }
2280
2515
  return Reflect.get(target, prop, receiver);
2281
2516
  },
2282
2517
  set(target, prop, value) {
2283
2518
  const propStr = String(prop);
2284
2519
  const isIndex = /^\d+$/.test(propStr);
2285
2520
  if (isIndex || propStr === "length") {
2521
+ if (!node.$isAlive) {
2522
+ throw new Error("[jotai-state-tree] Cannot modify array - the node is dead.");
2523
+ }
2286
2524
  if (!canWrite(node)) {
2287
2525
  throw new Error(
2288
2526
  `Cannot modify the array - the parent object is protected and can only be modified inside an action.`
@@ -2357,6 +2595,9 @@ var MSTMap = class extends Map {
2357
2595
  }
2358
2596
  }
2359
2597
  checkWrite() {
2598
+ if (!this.node.$isAlive) {
2599
+ throw new Error("[jotai-state-tree] Cannot modify map - the node is dead.");
2600
+ }
2360
2601
  if (!canWrite(this.node)) {
2361
2602
  throw new Error(
2362
2603
  `Cannot modify the map - the parent object is protected and can only be modified inside an action.`
@@ -2403,8 +2644,10 @@ var MSTMap = class extends Map {
2403
2644
  }
2404
2645
  return this;
2405
2646
  }
2406
- // Override get to return the instance from child node for complex types
2407
2647
  get(key) {
2648
+ if (!this.node.$isAlive) {
2649
+ throw new Error("[jotai-state-tree] Cannot access map - the node is dead.");
2650
+ }
2408
2651
  if (this.valueType._kind === "model" || this.valueType._kind === "array" || this.valueType._kind === "map") {
2409
2652
  const childNode = this.node.getChild(key);
2410
2653
  if (childNode) {
@@ -3669,27 +3912,6 @@ var types = {
3669
3912
  dynamicReference,
3670
3913
  safeDynamicReference
3671
3914
  };
3672
- function flow(generator) {
3673
- return function flowAction(...args) {
3674
- const gen = generator(...args);
3675
- function step(nextFn) {
3676
- let result;
3677
- try {
3678
- result = nextFn();
3679
- } catch (e) {
3680
- return Promise.reject(e);
3681
- }
3682
- if (result.done) {
3683
- return Promise.resolve(result.value);
3684
- }
3685
- return Promise.resolve(result.value).then(
3686
- (value) => step(() => gen.next(value)),
3687
- (error) => step(() => gen.throw(error))
3688
- );
3689
- }
3690
- return step(() => gen.next(void 0));
3691
- };
3692
- }
3693
3915
  function cast(value) {
3694
3916
  return value;
3695
3917
  }