@legendapp/state 1.3.6 → 1.4.0-next.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/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.4.0
2
+
3
+ - Feat: Returning an observable in a computed creates a two-way link to the target observable.
4
+ - Feat: `computed` is supported as a child of an observable
5
+ - Feat: Functions and computeds in the hierarchy of the constructing object in an observable are extracted into observable metadata so that setting the observable does not delete them.
6
+
7
+ ## 1.3.6
8
+
9
+ - Fix: Setting a primitive observable to the same value was still notifying listeners
10
+
1
11
  ## 1.3.5
2
12
 
3
13
  - Fix: array.find was returning `[]` instead of `undefined` when it found no matches
package/index.d.ts CHANGED
@@ -5,12 +5,13 @@ export { batch, beginBatch, endBatch, afterBatch } from './src/batching';
5
5
  export { computed } from './src/computed';
6
6
  export { event } from './src/event';
7
7
  export { observe } from './src/observe';
8
+ export { proxy } from './src/proxy';
8
9
  export { when, whenReady } from './src/when';
9
10
  export { configureLegendState } from './src/config';
10
11
  export * from './src/observableInterfaces';
11
12
  export { isEmpty, isArray, isBoolean, isFunction, isObject, isPrimitive, isPromise, isString, isSymbol, } from './src/is';
12
- import { setAtPath, getNode } from './src/helpers';
13
- import { ensureNodeValue, get, peek } from './src/globals';
13
+ import { setAtPath } from './src/helpers';
14
+ import { ensureNodeValue, get, getNode, peek } from './src/globals';
14
15
  import { getProxy, set } from './src/ObservableObject';
15
16
  export declare const internal: {
16
17
  ensureNodeValue: typeof ensureNodeValue;
package/index.js CHANGED
@@ -10,7 +10,6 @@ function isString(obj) {
10
10
  function isObject(obj) {
11
11
  return !!obj && typeof obj === 'object' && !isArray(obj);
12
12
  }
13
- // eslint-disable-next-line @typescript-eslint/ban-types
14
13
  function isFunction(obj) {
15
14
  return typeof obj === 'function';
16
15
  }
@@ -92,14 +91,13 @@ function updateTracking(node, track) {
92
91
  }
93
92
 
94
93
  const symbolToPrimitive = Symbol.toPrimitive;
95
- const symbolIsObservable = Symbol('isObservable');
96
- const symbolIsEvent = Symbol('isEvent');
97
94
  const symbolGetNode = Symbol('getNode');
98
95
  const symbolDelete = /* @__PURE__ */ Symbol('delete');
99
96
  const symbolOpaque = Symbol('opaque');
100
97
  const optimized = Symbol('optimized');
101
98
  const extraPrimitiveActivators = new Map();
102
99
  const extraPrimitiveProps = new Map();
100
+ const __devExtractFunctionsAndComputedsNodes = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ? new Set() : undefined;
103
101
  function checkActivate(node) {
104
102
  const root = node.root;
105
103
  const activate = root.activate;
@@ -108,6 +106,9 @@ function checkActivate(node) {
108
106
  activate();
109
107
  }
110
108
  }
109
+ function getNode(obs) {
110
+ return obs && obs[symbolGetNode];
111
+ }
111
112
  function get(node, track) {
112
113
  // Track by default
113
114
  updateTracking(node, track);
@@ -183,6 +184,36 @@ function findIDKey(obj, node) {
183
184
  }
184
185
  return idKey;
185
186
  }
187
+ function extractFunction(node, value, key) {
188
+ if (!node.functions) {
189
+ node.functions = new Map();
190
+ }
191
+ node.functions.set(key, value[key]);
192
+ }
193
+ function extractFunctionsAndComputeds(obj, node) {
194
+ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
195
+ if (__devExtractFunctionsAndComputedsNodes.has(obj)) {
196
+ console.error('[legend-state] Circular reference detected in object. You may want to use opaqueObject to stop traversing child nodes.', obj);
197
+ return false;
198
+ }
199
+ __devExtractFunctionsAndComputedsNodes.add(obj);
200
+ }
201
+ for (const k in obj) {
202
+ const v = obj[k];
203
+ if (typeof v === 'function') {
204
+ extractFunction(node, obj, k);
205
+ }
206
+ else if (typeof v == 'object' && v !== null && v !== undefined) {
207
+ const childNode = getNode(v);
208
+ if (childNode === null || childNode === void 0 ? void 0 : childNode.isComputed) {
209
+ extractFunction(node, obj, k);
210
+ }
211
+ else {
212
+ extractFunctionsAndComputeds(obj[k], getChildNode(node, k));
213
+ }
214
+ }
215
+ }
216
+ }
186
217
 
187
218
  let timeout;
188
219
  let numInBatch = 0;
@@ -272,6 +303,11 @@ function computeChangesAtNode(changesInBatch, node, value, path, pathTypes, valu
272
303
  function computeChangesRecursive(changesInBatch, node, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf) {
273
304
  // Do the compute at this node
274
305
  computeChangesAtNode(changesInBatch, node, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf);
306
+ if (node.linkedFromNodes) {
307
+ for (const linkedFromNode of node.linkedFromNodes) {
308
+ computeChangesAtNode(changesInBatch, linkedFromNode, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf);
309
+ }
310
+ }
275
311
  // If not root notify up through parents
276
312
  if (node.parent) {
277
313
  const parent = node.parent;
@@ -395,17 +431,18 @@ function afterBatch(fn) {
395
431
  }
396
432
 
397
433
  function isObservable(obs) {
398
- return obs && !!obs[symbolIsObservable];
434
+ return obs && !!obs[symbolGetNode];
435
+ }
436
+ function isEvent(obs) {
437
+ var _a;
438
+ return obs && ((_a = obs[symbolGetNode]) === null || _a === void 0 ? void 0 : _a.isEvent);
399
439
  }
400
- function computeSelector(selector, e) {
440
+ function computeSelector(selector, e, retainObservable) {
401
441
  let c = selector;
402
442
  if (isFunction(c)) {
403
443
  c = e ? c(e) : c();
404
444
  }
405
- return isObservable(c) ? c.get() : c;
406
- }
407
- function getNode(obs) {
408
- return obs[symbolGetNode];
445
+ return isObservable(c) && !retainObservable ? c.get() : c;
409
446
  }
410
447
  function getObservableIndex(obs) {
411
448
  const node = getNode(obs);
@@ -640,7 +677,7 @@ function trackSelector(selector, update, observeEvent, observeOptions, createRes
640
677
  else {
641
678
  // Compute the selector inside a tracking context
642
679
  beginTracking();
643
- value = selector ? computeSelector(selector, observeEvent) : selector;
680
+ value = selector ? computeSelector(selector, observeEvent, observeOptions === null || observeOptions === void 0 ? void 0 : observeOptions.retainObservable) : selector;
644
681
  tracker = tracking.current;
645
682
  nodes = tracker.nodes;
646
683
  endTracking();
@@ -689,9 +726,7 @@ const ArrayLoopers = new Set([
689
726
  'some',
690
727
  ]);
691
728
  const ArrayLoopersReturn = new Set(['filter', 'find']);
692
- // eslint-disable-next-line @typescript-eslint/ban-types
693
729
  const observableProperties = new Map();
694
- // eslint-disable-next-line @typescript-eslint/ban-types
695
730
  const observableFns = new Map([
696
731
  ['get', get],
697
732
  ['set', set],
@@ -708,7 +743,6 @@ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
708
743
  function collectionSetter(node, target, prop, ...args) {
709
744
  var _a;
710
745
  const prevValue = (isArray(target) && target.slice()) || target;
711
- // eslint-disable-next-line @typescript-eslint/ban-types
712
746
  const ret = target[prop].apply(target, args);
713
747
  if (node) {
714
748
  const hasParent = isChildNodeValue(node);
@@ -902,22 +936,29 @@ function getProxy(node, p) {
902
936
  return node.proxy || (node.proxy = new Proxy(node, proxyHandler));
903
937
  }
904
938
  const proxyHandler = {
905
- get(node, p) {
939
+ get(node, p, receiver) {
940
+ var _a;
906
941
  if (p === symbolToPrimitive) {
907
942
  throw new Error(process.env.NODE_ENV === 'development'
908
943
  ? '[legend-state] observable should not be used as a primitive. You may have forgotten to use .get() or .peek() to get the value of the observable.'
909
944
  : '[legend-state] observable is not a primitive.');
910
945
  }
911
- if (p === symbolIsObservable) {
912
- // Return true if called by isObservable()
913
- return true;
914
- }
915
- if (p === symbolIsEvent) {
916
- return false;
917
- }
918
946
  if (p === symbolGetNode) {
919
947
  return node;
920
948
  }
949
+ if (node.isComputed) {
950
+ if (node.proxyFn) {
951
+ return node.proxyFn(p);
952
+ }
953
+ else {
954
+ checkActivate(node);
955
+ }
956
+ }
957
+ // If this node is linked to another observable then forward to the target's handler.
958
+ // The exception is onChange because it needs to listen to this node for changes.
959
+ if (node.linkedToNode && p !== 'onChange') {
960
+ return proxyHandler.get(node.linkedToNode, p, receiver);
961
+ }
921
962
  const value = peek(node);
922
963
  if (value instanceof Map || value instanceof WeakMap || value instanceof Set || value instanceof WeakSet) {
923
964
  const ret = handlerMapSet(node, p, value);
@@ -1016,6 +1057,10 @@ const proxyHandler = {
1016
1057
  return vProp;
1017
1058
  }
1018
1059
  }
1060
+ const fnOrComputed = (_a = node.functions) === null || _a === void 0 ? void 0 : _a.get(p);
1061
+ if (fnOrComputed) {
1062
+ return fnOrComputed;
1063
+ }
1019
1064
  // Return an observable proxy to the property
1020
1065
  return getProxy(node, p);
1021
1066
  },
@@ -1125,7 +1170,7 @@ function setKey(node, key, newValue, level) {
1125
1170
  newValue =
1126
1171
  !node.isAssigning && isFunc
1127
1172
  ? newValue(prevValue)
1128
- : isObject(newValue) && (newValue === null || newValue === void 0 ? void 0 : newValue[symbolIsObservable])
1173
+ : isObject(newValue) && (newValue === null || newValue === void 0 ? void 0 : newValue[symbolGetNode])
1129
1174
  ? newValue.peek()
1130
1175
  : newValue;
1131
1176
  const isPrim = isPrimitive(newValue) || newValue instanceof Date;
@@ -1270,11 +1315,25 @@ function updateNodesAndNotify(node, newValue, prevValue, childNode, isPrim, isRo
1270
1315
  endBatch();
1271
1316
  }
1272
1317
 
1318
+ const fns = ['get', 'set', 'peek', 'onChange', 'toggle'];
1273
1319
  function ObservablePrimitiveClass(node) {
1274
1320
  this._node = node;
1275
- this.set = this.set.bind(this);
1276
- this.toggle = this.toggle.bind(this);
1321
+ // Bind to this
1322
+ for (let i = 0; i < fns.length; i++) {
1323
+ const key = fns[i];
1324
+ this[key] = this[key].bind(this);
1325
+ }
1277
1326
  }
1327
+ // Add observable functions to prototype
1328
+ function proto(key, fn) {
1329
+ ObservablePrimitiveClass.prototype[key] = function (...args) {
1330
+ return fn.call(this, this._node, ...args);
1331
+ };
1332
+ }
1333
+ proto('peek', peek);
1334
+ proto('get', get);
1335
+ proto('set', set);
1336
+ proto('onChange', onChange);
1278
1337
  // Getters
1279
1338
  Object.defineProperty(ObservablePrimitiveClass.prototype, symbolGetNode, {
1280
1339
  configurable: true,
@@ -1282,24 +1341,6 @@ Object.defineProperty(ObservablePrimitiveClass.prototype, symbolGetNode, {
1282
1341
  return this._node;
1283
1342
  },
1284
1343
  });
1285
- Object.defineProperty(ObservablePrimitiveClass.prototype, symbolIsObservable, {
1286
- configurable: true,
1287
- value: true,
1288
- });
1289
- Object.defineProperty(ObservablePrimitiveClass.prototype, symbolIsEvent, {
1290
- configurable: true,
1291
- value: false,
1292
- });
1293
- ObservablePrimitiveClass.prototype.peek = function () {
1294
- return peek(this._node);
1295
- };
1296
- ObservablePrimitiveClass.prototype.get = function () {
1297
- return get(this._node);
1298
- };
1299
- // Setters
1300
- ObservablePrimitiveClass.prototype.set = function (value) {
1301
- return set(this._node, value);
1302
- };
1303
1344
  ObservablePrimitiveClass.prototype.toggle = function () {
1304
1345
  const value = this.peek();
1305
1346
  if (value === undefined || isBoolean(value)) {
@@ -1314,10 +1355,6 @@ ObservablePrimitiveClass.prototype.delete = function () {
1314
1355
  this.set(undefined);
1315
1356
  return this;
1316
1357
  };
1317
- // Listener
1318
- ObservablePrimitiveClass.prototype.onChange = function (cb, options) {
1319
- return onChange(this._node, cb, options);
1320
- };
1321
1358
 
1322
1359
  function createObservable(value, makePrimitive) {
1323
1360
  const valueIsPromise = isPromise(value);
@@ -1327,7 +1364,8 @@ function createObservable(value, makePrimitive) {
1327
1364
  const node = {
1328
1365
  root,
1329
1366
  };
1330
- const obs = makePrimitive || isActualPrimitive(value)
1367
+ const prim = makePrimitive || isActualPrimitive(value);
1368
+ const obs = prim
1331
1369
  ? new ObservablePrimitiveClass(node)
1332
1370
  : getProxy(node);
1333
1371
  if (valueIsPromise) {
@@ -1338,6 +1376,12 @@ function createObservable(value, makePrimitive) {
1338
1376
  obs.set(value);
1339
1377
  });
1340
1378
  }
1379
+ else if (!prim) {
1380
+ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
1381
+ __devExtractFunctionsAndComputedsNodes.clear();
1382
+ }
1383
+ extractFunctionsAndComputeds(value, node);
1384
+ }
1341
1385
  return obs;
1342
1386
  }
1343
1387
  function observable(value) {
@@ -1377,7 +1421,7 @@ function observe(selectorOrRun, reactionOrOptions, options) {
1377
1421
  e.onCleanupReaction = undefined;
1378
1422
  }
1379
1423
  // Call the reaction if there is one and the value changed
1380
- if (reaction && (e.num > 0 || !(selectorOrRun === null || selectorOrRun === void 0 ? void 0 : selectorOrRun[symbolIsEvent])) && e.previous !== e.value) {
1424
+ if (reaction && (e.num > 0 || !isEvent(selectorOrRun)) && e.previous !== e.value) {
1381
1425
  reaction(e);
1382
1426
  }
1383
1427
  // Update the previous value
@@ -1402,16 +1446,40 @@ function computed(compute, set$1) {
1402
1446
  // Create an observable for this computed variable
1403
1447
  const obs = observable();
1404
1448
  lockObservable(obs, true);
1405
- // Lazily activate the observable when get is called
1406
1449
  const node = getNode(obs);
1450
+ node.isComputed = true;
1407
1451
  const setInner = function (val) {
1408
- if (val !== obs.peek()) {
1452
+ const prevNode = node.linkedToNode;
1453
+ // If it was previously linked to a node remove self
1454
+ // from its linkedFromNodes
1455
+ if (prevNode) {
1456
+ node.linkedToNode.linkedFromNodes.delete(node);
1457
+ node.linkedToNode = undefined;
1458
+ }
1459
+ if (isObservable(val)) {
1460
+ // If the computed is a proxy to another observable
1461
+ // link it to the target observable
1462
+ const linkedNode = getNode(val);
1463
+ node.linkedToNode = linkedNode;
1464
+ if (!linkedNode.linkedFromNodes) {
1465
+ linkedNode.linkedFromNodes = new Set();
1466
+ }
1467
+ linkedNode.linkedFromNodes.add(node);
1468
+ // If the target observable is different then notify for the change
1469
+ if (prevNode) {
1470
+ const value = getNodeValue(linkedNode);
1471
+ const prevValue = getNodeValue(prevNode);
1472
+ notify(node, value, prevValue, 0);
1473
+ }
1474
+ }
1475
+ else if (val !== obs.peek()) {
1409
1476
  // Update the computed value
1410
1477
  lockObservable(obs, false);
1411
1478
  set(node, val);
1412
1479
  lockObservable(obs, true);
1413
1480
  }
1414
1481
  };
1482
+ // Lazily activate the observable when get is called
1415
1483
  node.root.activate = () => {
1416
1484
  observe(compute, ({ value }) => {
1417
1485
  if (isPromise(value)) {
@@ -1420,7 +1488,7 @@ function computed(compute, set$1) {
1420
1488
  else {
1421
1489
  setInner(value);
1422
1490
  }
1423
- }, { immediate: true });
1491
+ }, { immediate: true, retainObservable: true });
1424
1492
  };
1425
1493
  if (set$1) {
1426
1494
  node.root.set = (value) => {
@@ -1434,6 +1502,8 @@ function event() {
1434
1502
  // event simply wraps around a number observable
1435
1503
  // which increments its value to dispatch change events
1436
1504
  const obs = observable(0);
1505
+ const node = getNode(obs);
1506
+ node.isEvent = true;
1437
1507
  return {
1438
1508
  fire: function () {
1439
1509
  // Notify increments the value so that the observable changes
@@ -1442,13 +1512,33 @@ function event() {
1442
1512
  on: function (cb) {
1443
1513
  return obs.onChange(cb);
1444
1514
  },
1445
- get: () => obs.get(),
1515
+ get: function () {
1516
+ // Need to return undefined
1517
+ obs.get();
1518
+ },
1446
1519
  // @ts-expect-error eslint doesn't like adding symbols to the object but this does work
1447
- [symbolIsObservable]: true,
1448
- [symbolIsEvent]: true,
1520
+ [symbolGetNode]: node,
1449
1521
  };
1450
1522
  }
1451
1523
 
1524
+ function proxy(get) {
1525
+ // Create an observable for this computed variable
1526
+ const obs = observable();
1527
+ lockObservable(obs, true);
1528
+ const mapTargets = new Map();
1529
+ const node = getNode(obs);
1530
+ node.isComputed = true;
1531
+ node.proxyFn = (key) => {
1532
+ let target = mapTargets.get(key);
1533
+ if (!target) {
1534
+ target = get(key);
1535
+ mapTargets.set(key, target);
1536
+ }
1537
+ return target;
1538
+ };
1539
+ return obs;
1540
+ }
1541
+
1452
1542
  function _when(predicate, effect, checkReady) {
1453
1543
  let value;
1454
1544
  // Create a wrapping fn that calls the effect if predicate returns true
@@ -1570,12 +1660,11 @@ exports.observablePrimitive = observablePrimitive;
1570
1660
  exports.observe = observe;
1571
1661
  exports.opaqueObject = opaqueObject;
1572
1662
  exports.optimized = optimized;
1663
+ exports.proxy = proxy;
1573
1664
  exports.setAtPath = setAtPath;
1574
1665
  exports.setInObservableAtPath = setInObservableAtPath;
1575
1666
  exports.setupTracking = setupTracking;
1576
1667
  exports.symbolDelete = symbolDelete;
1577
- exports.symbolIsEvent = symbolIsEvent;
1578
- exports.symbolIsObservable = symbolIsObservable;
1579
1668
  exports.trackSelector = trackSelector;
1580
1669
  exports.tracking = tracking;
1581
1670
  exports.updateTracking = updateTracking;