@legendapp/state 1.3.5 → 1.4.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -9,9 +9,9 @@ export { when, whenReady } from './src/when';
9
9
  export { configureLegendState } from './src/config';
10
10
  export * from './src/observableInterfaces';
11
11
  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';
14
- import { getProxy, set } from './src/observable';
12
+ import { setAtPath } from './src/helpers';
13
+ import { ensureNodeValue, get, getNode, peek } from './src/globals';
14
+ import { getProxy, set } from './src/ObservableObject';
15
15
  export declare const internal: {
16
16
  ensureNodeValue: typeof ensureNodeValue;
17
17
  get: typeof get;
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();
@@ -665,70 +702,6 @@ function trackSelector(selector, update, observeEvent, observeOptions, createRes
665
702
  return { value, dispose, resubscribe };
666
703
  }
667
704
 
668
- function ObservablePrimitiveClass(node) {
669
- this._node = node;
670
- this.set = this.set.bind(this);
671
- this.toggle = this.toggle.bind(this);
672
- }
673
- // Getters
674
- Object.defineProperty(ObservablePrimitiveClass.prototype, symbolGetNode, {
675
- configurable: true,
676
- get() {
677
- return this._node;
678
- },
679
- });
680
- Object.defineProperty(ObservablePrimitiveClass.prototype, symbolIsObservable, {
681
- configurable: true,
682
- value: true,
683
- });
684
- Object.defineProperty(ObservablePrimitiveClass.prototype, symbolIsEvent, {
685
- configurable: true,
686
- value: false,
687
- });
688
- ObservablePrimitiveClass.prototype.peek = function () {
689
- checkActivate(this._node);
690
- return this._node.root._;
691
- };
692
- ObservablePrimitiveClass.prototype.get = function () {
693
- const node = this._node;
694
- updateTracking(node);
695
- return this.peek();
696
- };
697
- // Setters
698
- ObservablePrimitiveClass.prototype.set = function (value) {
699
- if (isFunction(value)) {
700
- value = value(this._node.root._);
701
- }
702
- if (this._node.root.locked) {
703
- throw new Error(process.env.NODE_ENV === 'development'
704
- ? '[legend-state] Cannot modify an observable while it is locked. Please make sure that you unlock the observable before making changes.'
705
- : '[legend-state] Modified locked observable');
706
- }
707
- const root = this._node.root;
708
- const prev = root._;
709
- root._ = value;
710
- notify(this._node, value, prev, 0);
711
- return this;
712
- };
713
- ObservablePrimitiveClass.prototype.toggle = function () {
714
- const value = this.peek();
715
- if (value === undefined || isBoolean(value)) {
716
- this.set(!value);
717
- }
718
- else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
719
- throw new Error('[legend-state] Cannot toggle a non-boolean value');
720
- }
721
- return !value;
722
- };
723
- ObservablePrimitiveClass.prototype.delete = function () {
724
- this.set(undefined);
725
- return this;
726
- };
727
- // Listener
728
- ObservablePrimitiveClass.prototype.onChange = function (cb, options) {
729
- return onChange(this._node, cb, options);
730
- };
731
-
732
705
  const ArrayModifiers = new Set([
733
706
  'copyWithin',
734
707
  'fill',
@@ -753,9 +726,7 @@ const ArrayLoopers = new Set([
753
726
  'some',
754
727
  ]);
755
728
  const ArrayLoopersReturn = new Set(['filter', 'find']);
756
- // eslint-disable-next-line @typescript-eslint/ban-types
757
729
  const observableProperties = new Map();
758
- // eslint-disable-next-line @typescript-eslint/ban-types
759
730
  const observableFns = new Map([
760
731
  ['get', get],
761
732
  ['set', set],
@@ -772,7 +743,6 @@ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
772
743
  function collectionSetter(node, target, prop, ...args) {
773
744
  var _a;
774
745
  const prevValue = (isArray(target) && target.slice()) || target;
775
- // eslint-disable-next-line @typescript-eslint/ban-types
776
746
  const ret = target[prop].apply(target, args);
777
747
  if (node) {
778
748
  const hasParent = isChildNodeValue(node);
@@ -966,22 +936,24 @@ function getProxy(node, p) {
966
936
  return node.proxy || (node.proxy = new Proxy(node, proxyHandler));
967
937
  }
968
938
  const proxyHandler = {
969
- get(node, p) {
939
+ get(node, p, receiver) {
940
+ var _a;
970
941
  if (p === symbolToPrimitive) {
971
942
  throw new Error(process.env.NODE_ENV === 'development'
972
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.'
973
944
  : '[legend-state] observable is not a primitive.');
974
945
  }
975
- if (p === symbolIsObservable) {
976
- // Return true if called by isObservable()
977
- return true;
978
- }
979
- if (p === symbolIsEvent) {
980
- return false;
981
- }
982
946
  if (p === symbolGetNode) {
983
947
  return node;
984
948
  }
949
+ if (node.isComputed) {
950
+ checkActivate(node);
951
+ }
952
+ // If this node is linked to another observable then forward to the target's handler.
953
+ // The exception is onChange because it needs to listen to this node for changes.
954
+ if (node.linkedToNode && p !== 'onChange') {
955
+ return proxyHandler.get(node.linkedToNode, p, receiver);
956
+ }
985
957
  const value = peek(node);
986
958
  if (value instanceof Map || value instanceof WeakMap || value instanceof Set || value instanceof WeakSet) {
987
959
  const ret = handlerMapSet(node, p, value);
@@ -1080,6 +1052,10 @@ const proxyHandler = {
1080
1052
  return vProp;
1081
1053
  }
1082
1054
  }
1055
+ const fnOrComputed = (_a = node.functions) === null || _a === void 0 ? void 0 : _a.get(p);
1056
+ if (fnOrComputed) {
1057
+ return fnOrComputed;
1058
+ }
1083
1059
  // Return an observable proxy to the property
1084
1060
  return getProxy(node, p);
1085
1061
  },
@@ -1143,11 +1119,11 @@ const proxyHandler = {
1143
1119
  },
1144
1120
  };
1145
1121
  function set(node, newValue) {
1146
- if (!node.parent) {
1147
- return setKey(node, undefined, newValue);
1122
+ if (node.parent) {
1123
+ return setKey(node.parent, node.key, newValue);
1148
1124
  }
1149
1125
  else {
1150
- return setKey(node.parent, node.key, newValue);
1126
+ return setKey(node, undefined, newValue);
1151
1127
  }
1152
1128
  }
1153
1129
  function toggle(node) {
@@ -1189,12 +1165,12 @@ function setKey(node, key, newValue, level) {
1189
1165
  newValue =
1190
1166
  !node.isAssigning && isFunc
1191
1167
  ? newValue(prevValue)
1192
- : isObject(newValue) && (newValue === null || newValue === void 0 ? void 0 : newValue[symbolIsObservable])
1168
+ : isObject(newValue) && (newValue === null || newValue === void 0 ? void 0 : newValue[symbolGetNode])
1193
1169
  ? newValue.peek()
1194
1170
  : newValue;
1195
1171
  const isPrim = isPrimitive(newValue) || newValue instanceof Date;
1196
1172
  try {
1197
- node.isSetting++;
1173
+ node.isSetting = (node.isSetting || 0) + 1;
1198
1174
  // Save the new value
1199
1175
  if (isDelete) {
1200
1176
  delete parentValue[key];
@@ -1209,7 +1185,9 @@ function setKey(node, key, newValue, level) {
1209
1185
  if (node.root.locked && node.root.set) {
1210
1186
  node.root.set(node.root._);
1211
1187
  }
1212
- updateNodesAndNotify(node, newValue, prevValue, childNode, isPrim, isRoot, level);
1188
+ if (newValue !== prevValue) {
1189
+ updateNodesAndNotify(node, newValue, prevValue, childNode, isPrim, isRoot, level);
1190
+ }
1213
1191
  return isFunc ? newValue : isRoot ? getProxy(node) : getProxy(node, key);
1214
1192
  }
1215
1193
  function assign(node, value) {
@@ -1237,33 +1215,6 @@ function deleteFn(node, key) {
1237
1215
  }
1238
1216
  setKey(node, key, symbolDelete, /*level*/ -1);
1239
1217
  }
1240
- function createObservable(value, makePrimitive) {
1241
- const valueIsPromise = isPromise(value);
1242
- const root = {
1243
- _: valueIsPromise ? undefined : value,
1244
- };
1245
- const node = {
1246
- root,
1247
- };
1248
- const obs = makePrimitive || isActualPrimitive(value)
1249
- ? new ObservablePrimitiveClass(node)
1250
- : getProxy(node);
1251
- if (valueIsPromise) {
1252
- value.catch((error) => {
1253
- obs.set({ error });
1254
- });
1255
- value.then((value) => {
1256
- obs.set(value);
1257
- });
1258
- }
1259
- return obs;
1260
- }
1261
- function observable(value) {
1262
- return createObservable(value);
1263
- }
1264
- function observablePrimitive(value) {
1265
- return createObservable(value, /*makePrimitive*/ true);
1266
- }
1267
1218
  function handlerMapSet(node, p, value) {
1268
1219
  const vProp = value === null || value === void 0 ? void 0 : value[p];
1269
1220
  if (isFunction(vProp)) {
@@ -1359,6 +1310,82 @@ function updateNodesAndNotify(node, newValue, prevValue, childNode, isPrim, isRo
1359
1310
  endBatch();
1360
1311
  }
1361
1312
 
1313
+ const fns = ['get', 'set', 'peek', 'onChange', 'toggle'];
1314
+ function ObservablePrimitiveClass(node) {
1315
+ this._node = node;
1316
+ // Bind to this
1317
+ for (let i = 0; i < fns.length; i++) {
1318
+ const key = fns[i];
1319
+ this[key] = this[key].bind(this);
1320
+ }
1321
+ }
1322
+ // Add observable functions to prototype
1323
+ function proto(key, fn) {
1324
+ ObservablePrimitiveClass.prototype[key] = function (...args) {
1325
+ return fn.call(this, this._node, ...args);
1326
+ };
1327
+ }
1328
+ proto('peek', peek);
1329
+ proto('get', get);
1330
+ proto('set', set);
1331
+ proto('onChange', onChange);
1332
+ // Getters
1333
+ Object.defineProperty(ObservablePrimitiveClass.prototype, symbolGetNode, {
1334
+ configurable: true,
1335
+ get() {
1336
+ return this._node;
1337
+ },
1338
+ });
1339
+ ObservablePrimitiveClass.prototype.toggle = function () {
1340
+ const value = this.peek();
1341
+ if (value === undefined || isBoolean(value)) {
1342
+ this.set(!value);
1343
+ }
1344
+ else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
1345
+ throw new Error('[legend-state] Cannot toggle a non-boolean value');
1346
+ }
1347
+ return !value;
1348
+ };
1349
+ ObservablePrimitiveClass.prototype.delete = function () {
1350
+ this.set(undefined);
1351
+ return this;
1352
+ };
1353
+
1354
+ function createObservable(value, makePrimitive) {
1355
+ const valueIsPromise = isPromise(value);
1356
+ const root = {
1357
+ _: valueIsPromise ? undefined : value,
1358
+ };
1359
+ const node = {
1360
+ root,
1361
+ };
1362
+ const prim = makePrimitive || isActualPrimitive(value);
1363
+ const obs = prim
1364
+ ? new ObservablePrimitiveClass(node)
1365
+ : getProxy(node);
1366
+ if (valueIsPromise) {
1367
+ value.catch((error) => {
1368
+ obs.set({ error });
1369
+ });
1370
+ value.then((value) => {
1371
+ obs.set(value);
1372
+ });
1373
+ }
1374
+ else if (!prim) {
1375
+ if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
1376
+ __devExtractFunctionsAndComputedsNodes.clear();
1377
+ }
1378
+ extractFunctionsAndComputeds(value, node);
1379
+ }
1380
+ return obs;
1381
+ }
1382
+ function observable(value) {
1383
+ return createObservable(value);
1384
+ }
1385
+ function observablePrimitive(value) {
1386
+ return createObservable(value, /*makePrimitive*/ true);
1387
+ }
1388
+
1362
1389
  function observe(selectorOrRun, reactionOrOptions, options) {
1363
1390
  let reaction;
1364
1391
  if (isFunction(reactionOrOptions)) {
@@ -1389,7 +1416,7 @@ function observe(selectorOrRun, reactionOrOptions, options) {
1389
1416
  e.onCleanupReaction = undefined;
1390
1417
  }
1391
1418
  // Call the reaction if there is one and the value changed
1392
- if (reaction && (e.num > 0 || !(selectorOrRun === null || selectorOrRun === void 0 ? void 0 : selectorOrRun[symbolIsEvent])) && e.previous !== e.value) {
1419
+ if (reaction && (e.num > 0 || !isEvent(selectorOrRun)) && e.previous !== e.value) {
1393
1420
  reaction(e);
1394
1421
  }
1395
1422
  // Update the previous value
@@ -1414,16 +1441,40 @@ function computed(compute, set$1) {
1414
1441
  // Create an observable for this computed variable
1415
1442
  const obs = observable();
1416
1443
  lockObservable(obs, true);
1417
- // Lazily activate the observable when get is called
1418
1444
  const node = getNode(obs);
1445
+ node.isComputed = true;
1419
1446
  const setInner = function (val) {
1420
- if (val !== obs.peek()) {
1447
+ const prevNode = node.linkedToNode;
1448
+ // If it was previously linked to a node remove self
1449
+ // from its linkedFromNodes
1450
+ if (prevNode) {
1451
+ node.linkedToNode.linkedFromNodes.delete(node);
1452
+ node.linkedToNode = undefined;
1453
+ }
1454
+ if (isObservable(val)) {
1455
+ // If the computed is a proxy to another observable
1456
+ // link it to the target observable
1457
+ const linkedNode = getNode(val);
1458
+ node.linkedToNode = linkedNode;
1459
+ if (!linkedNode.linkedFromNodes) {
1460
+ linkedNode.linkedFromNodes = new Set();
1461
+ }
1462
+ linkedNode.linkedFromNodes.add(node);
1463
+ // If the target observable is different then notify for the change
1464
+ if (prevNode) {
1465
+ const value = getNodeValue(linkedNode);
1466
+ const prevValue = getNodeValue(prevNode);
1467
+ notify(node, value, prevValue, 0);
1468
+ }
1469
+ }
1470
+ else if (val !== obs.peek()) {
1421
1471
  // Update the computed value
1422
1472
  lockObservable(obs, false);
1423
1473
  set(node, val);
1424
1474
  lockObservable(obs, true);
1425
1475
  }
1426
1476
  };
1477
+ // Lazily activate the observable when get is called
1427
1478
  node.root.activate = () => {
1428
1479
  observe(compute, ({ value }) => {
1429
1480
  if (isPromise(value)) {
@@ -1432,7 +1483,7 @@ function computed(compute, set$1) {
1432
1483
  else {
1433
1484
  setInner(value);
1434
1485
  }
1435
- }, { immediate: true });
1486
+ }, { immediate: true, retainObservable: true });
1436
1487
  };
1437
1488
  if (set$1) {
1438
1489
  node.root.set = (value) => {
@@ -1446,6 +1497,8 @@ function event() {
1446
1497
  // event simply wraps around a number observable
1447
1498
  // which increments its value to dispatch change events
1448
1499
  const obs = observable(0);
1500
+ const node = getNode(obs);
1501
+ node.isEvent = true;
1449
1502
  return {
1450
1503
  fire: function () {
1451
1504
  // Notify increments the value so that the observable changes
@@ -1454,10 +1507,12 @@ function event() {
1454
1507
  on: function (cb) {
1455
1508
  return obs.onChange(cb);
1456
1509
  },
1457
- get: () => obs.get(),
1510
+ get: function () {
1511
+ // Need to return undefined
1512
+ obs.get();
1513
+ },
1458
1514
  // @ts-expect-error eslint doesn't like adding symbols to the object but this does work
1459
- [symbolIsObservable]: true,
1460
- [symbolIsEvent]: true,
1515
+ [symbolGetNode]: node,
1461
1516
  };
1462
1517
  }
1463
1518
 
@@ -1586,8 +1641,6 @@ exports.setAtPath = setAtPath;
1586
1641
  exports.setInObservableAtPath = setInObservableAtPath;
1587
1642
  exports.setupTracking = setupTracking;
1588
1643
  exports.symbolDelete = symbolDelete;
1589
- exports.symbolIsEvent = symbolIsEvent;
1590
- exports.symbolIsObservable = symbolIsObservable;
1591
1644
  exports.trackSelector = trackSelector;
1592
1645
  exports.tracking = tracking;
1593
1646
  exports.updateTracking = updateTracking;