@solidjs/signals 2.0.0-beta.22 → 2.0.0-beta.24

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/dist/dev.js CHANGED
@@ -1569,8 +1569,17 @@ function unlinkSubs(link) {
1569
1569
  if (nextSub === null) {
1570
1570
  dep._unobserved?.();
1571
1571
  // No more subscribers; only tear down if CONFIG_AUTO_DISPOSE is set.
1572
+ // A pending node is exempt: its in-flight async work (or the
1573
+ // transition holding it) is an observer — tearing down would orphan
1574
+ // the work and re-execute it on the next read. The settle path runs
1575
+ // this same last-one-out check when that observer releases (the
1576
+ // untracked-read dispose in core.ts guards on pending identically).
1572
1577
  const c = dep;
1573
- c._fn && c._config & CONFIG_AUTO_DISPOSE && !(c._flags & REACTIVE_ZOMBIE) && unobserved(c);
1578
+ c._fn &&
1579
+ c._config & CONFIG_AUTO_DISPOSE &&
1580
+ !(c._flags & REACTIVE_ZOMBIE) &&
1581
+ !(c._statusFlags & STATUS_PENDING) &&
1582
+ unobserved(c);
1574
1583
  }
1575
1584
  }
1576
1585
  return nextDep;
@@ -1847,6 +1856,16 @@ function handleAsync(el, result, setter) {
1847
1856
  flush();
1848
1857
  then?.();
1849
1858
  };
1859
+ // A pending node's in-flight promise is an observer: `unlinkSubs` skips
1860
+ // autodispose while STATUS_PENDING so subscriber churn can't orphan the
1861
+ // work (a lazy async memo would otherwise tear down and re-execute — one
1862
+ // fetch per suspended re-read). Settling is that observer's release, so
1863
+ // it runs the same last-one-out check the other release sites run.
1864
+ const settleAutodispose = () => {
1865
+ if (el._config & CONFIG_AUTO_DISPOSE && !el._subs && !(el._statusFlags & STATUS_PENDING)) {
1866
+ unobserved(el);
1867
+ }
1868
+ };
1850
1869
  if (thenable) {
1851
1870
  let resolved = false,
1852
1871
  rejected = false,
@@ -1857,13 +1876,19 @@ function handleAsync(el, result, setter) {
1857
1876
  if (isSync) {
1858
1877
  syncValue = v;
1859
1878
  resolved = true;
1860
- } else asyncWrite(v);
1879
+ } else {
1880
+ asyncWrite(v);
1881
+ settleAutodispose();
1882
+ }
1861
1883
  },
1862
1884
  e => {
1863
1885
  if (isSync) {
1864
1886
  syncError = e;
1865
1887
  rejected = true;
1866
- } else handleError(e);
1888
+ } else {
1889
+ handleError(e);
1890
+ settleAutodispose();
1891
+ }
1867
1892
  }
1868
1893
  );
1869
1894
  isSync = false;
@@ -2113,7 +2138,10 @@ function clearSnapshots() {
2113
2138
  if (snapshotSources) {
2114
2139
  for (const source of snapshotSources) {
2115
2140
  delete source._snapshotValue;
2116
- delete source[STORE_SNAPSHOT_PROPS];
2141
+ // StoreNode targets share one pre-initialized hidden class (see
2142
+ // createStoreProxy) — assign undefined instead of deleting, and only
2143
+ // when present so signal-node sources don't grow the field.
2144
+ if (source[STORE_SNAPSHOT_PROPS] !== undefined) source[STORE_SNAPSHOT_PROPS] = undefined;
2117
2145
  }
2118
2146
  snapshotSources = null;
2119
2147
  }
@@ -4738,23 +4766,18 @@ function applyStateFast(next, target, keyFn) {
4738
4766
  let tracked;
4739
4767
  if (nodes) {
4740
4768
  tracked = nodes[$TRACK];
4741
- const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4742
- for (let i = 0, len = keys.length; i < len; i++) {
4743
- const key = keys[i];
4744
- const node = nodes[key];
4745
- const previousValue = unwrap(previous[key]);
4746
- let nextValue = unwrap(next[key]);
4747
- if (previousValue === nextValue) continue;
4748
- if (
4749
- !previousValue ||
4750
- !isWrappable(previousValue) ||
4751
- !isWrappable(nextValue) ||
4752
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4753
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4754
- ) {
4755
- tracked && setSignal(tracked, void 0);
4756
- node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
4757
- } else applyState(nextValue, wrap(previousValue, target), keyFn);
4769
+ if (tracked || symbolKeyedRecords.has(nodes)) {
4770
+ const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4771
+ for (let i = 0, len = keys.length; i < len; i++) {
4772
+ diffNodeKey(keys[i], nodes, previous, next, target, tracked, keyFn);
4773
+ }
4774
+ } else {
4775
+ // Untracked, string-only node records (the overwhelmingly common case)
4776
+ // iterate in place — nodeKeys() allocated a fresh key array per object
4777
+ // per pass, which dominates allocation on large-graph reconciles.
4778
+ for (const key in nodes) {
4779
+ diffNodeKey(key, nodes, previous, next, target, tracked, keyFn);
4780
+ }
4758
4781
  }
4759
4782
  }
4760
4783
  if (!tracked && target[STORE_DESC]) applyDescendants(previous, next, target, nodes, keyFn);
@@ -4767,6 +4790,24 @@ function applyStateFast(next, target, keyFn) {
4767
4790
  }
4768
4791
  }
4769
4792
  }
4793
+ // One node-key step of the fast object diff — shared by the array-iterating
4794
+ // (tracked / symbol-keyed) and for-in (plain) loops in applyStateFast.
4795
+ function diffNodeKey(key, nodes, previous, next, target, tracked, keyFn) {
4796
+ const node = nodes[key];
4797
+ const previousValue = unwrap(previous[key]);
4798
+ let nextValue = unwrap(next[key]);
4799
+ if (previousValue === nextValue) return;
4800
+ if (
4801
+ !previousValue ||
4802
+ !isWrappable(previousValue) ||
4803
+ !isWrappable(nextValue) ||
4804
+ Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4805
+ (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4806
+ ) {
4807
+ tracked && setSignal(tracked, void 0);
4808
+ node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
4809
+ } else applyState(nextValue, wrap(previousValue, target), keyFn);
4810
+ }
4770
4811
  function applyStateSlow(next, target, keyFn) {
4771
4812
  const previous = target[STORE_VALUE];
4772
4813
  const override = target[STORE_OVERRIDE];
@@ -4904,17 +4945,26 @@ function applyStateSlow(next, target, keyFn) {
4904
4945
  }
4905
4946
  }
4906
4947
  }
4948
+ // No-key reconcile: every item reports "no key", which routes array diffs to
4949
+ // the positional branch and object descent to plain per-property merging.
4950
+ const NOKEY = () => null;
4907
4951
  /**
4908
4952
  * Returns a draft-mutating function that smart-merges `value` into a store,
4909
- * preserving the identity of items whose `key` field matches between old and
4910
- * new states. Useful when applying server payloads or full-replacement data
4911
- * onto an existing store without losing fine-grained reactivity.
4953
+ * preserving fine-grained reactivity: only changed leaves trigger updates.
4954
+ *
4955
+ * With a `key` (default `"id"`), array items whose key matches between old
4956
+ * and new states keep their identity (updated in place, moves and removals
4957
+ * update the corresponding signals) — the shape for keyed server payloads.
4958
+ * Items without the key field fall back to positional matching.
4912
4959
  *
4913
- * Items with the same key are updated in place (only changed properties
4914
- * trigger updates). Items added or removed update the corresponding signals.
4960
+ * With `key: null`, matching is purely positional: index N of the new array
4961
+ * merges into index N of the old, and object properties merge recursively —
4962
+ * the classic pattern for fixed-shape data that churns in place (dashboards,
4963
+ * monitors), where no keyed diff pass is needed or wanted.
4915
4964
  *
4916
4965
  * @param value the next state to merge in
4917
- * @param key property name (string) or extractor function for stable identity
4966
+ * @param key property name (string) or extractor function for stable
4967
+ * identity (default `"id"`); pass `null` for positional merging
4918
4968
  *
4919
4969
  * @example
4920
4970
  * ```ts
@@ -4922,13 +4972,20 @@ function applyStateSlow(next, target, keyFn) {
4922
4972
  *
4923
4973
  * async function refresh() {
4924
4974
  * const fresh = await api.getTodos();
4925
- * setTodos(reconcile(fresh, "id")); // diff-merge by `id`
4975
+ * setTodos(reconcile(fresh)); // diff-merge by `id`
4926
4976
  * }
4977
+ *
4978
+ * // fixed-shape polling data — positional merge
4979
+ * setStats(reconcile(nextStats, null));
4927
4980
  * ```
4928
4981
  */
4929
- function reconcile(value, key) {
4982
+ function reconcile(value, key = "id") {
4930
4983
  return state => {
4931
4984
  if (state == null) throw new Error("Cannot reconcile null or undefined state");
4985
+ if (key === null) {
4986
+ applyState(value, state, NOKEY);
4987
+ return;
4988
+ }
4932
4989
  const keyFn = typeof key === "string" ? item => item[key] : key;
4933
4990
  const eq = keyFn(state);
4934
4991
  if (eq !== undefined && keyFn(value) !== eq)
@@ -5131,13 +5188,37 @@ const STORE_VALUE = "v",
5131
5188
  STORE_PARENT = "u",
5132
5189
  STORE_DESC = "d";
5133
5190
  const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
5191
+ // Every StoreNode field is initialized up front, in one fixed order, so all
5192
+ // targets share a single hidden class. The traps and reconcile read these
5193
+ // fields on their hottest paths; fields added lazily in varying orders made
5194
+ // those loads megamorphic across a large store graph (dictionary-mode probes
5195
+ // on every trap hit). Assignments elsewhere must never `delete` a field —
5196
+ // write `undefined` instead, or the shape degrades again.
5197
+ function initStoreFields(newTarget) {
5198
+ newTarget[STORE_OVERRIDE] = undefined;
5199
+ newTarget[STORE_OPTIMISTIC_OVERRIDE] = undefined;
5200
+ newTarget[STORE_OPTIMISTIC_OWNERS] = undefined;
5201
+ newTarget[STORE_NODE] = undefined;
5202
+ newTarget[STORE_HAS] = undefined;
5203
+ newTarget[STORE_CUSTOM_PROTO] = undefined;
5204
+ newTarget[STORE_WRAP] = undefined;
5205
+ newTarget[STORE_LOOKUP] = undefined;
5206
+ newTarget[STORE_FIREWALL] = undefined;
5207
+ newTarget[STORE_OPTIMISTIC] = undefined;
5208
+ newTarget[STORE_SNAPSHOT_PROPS] = undefined;
5209
+ newTarget[STORE_PARENT] = undefined;
5210
+ newTarget[STORE_DESC] = undefined;
5211
+ newTarget[$PROXY] = null;
5212
+ }
5134
5213
  function createStoreProxy(value, traps = storeTraps, extend) {
5135
5214
  let newTarget;
5136
5215
  if (Array.isArray(value)) {
5137
5216
  newTarget = [];
5138
- newTarget.v = value;
5217
+ newTarget[STORE_VALUE] = value;
5218
+ initStoreFields(newTarget);
5139
5219
  } else {
5140
- newTarget = { v: value };
5220
+ newTarget = { [STORE_VALUE]: value };
5221
+ initStoreFields(newTarget);
5141
5222
  const unwrapped = value?.[$TARGET]?.[STORE_VALUE] ?? value;
5142
5223
  const proto = Object.getPrototypeOf(unwrapped);
5143
5224
  if (proto !== null && proto !== Object.prototype) {
@@ -5716,6 +5797,28 @@ const storeTraps = {
5716
5797
  trackSelf(target);
5717
5798
  return receiver;
5718
5799
  }
5800
+ // Hot path: an existing node on a plain target — no firewall (so neither
5801
+ // selfRead nor the uninitialized guard can apply), no override layers,
5802
+ // no write scope, and a raw (non-proxy) source. This is the shape of
5803
+ // every effect re-read of a settled store; it pays one node read and the
5804
+ // wrap check, nothing else. Any exotic bit falls through to the full
5805
+ // resolution below, and dev strictRead keeps its warning path.
5806
+ if (
5807
+ !strictRead &&
5808
+ target[STORE_FIREWALL] === undefined &&
5809
+ target[STORE_OVERRIDE] === undefined &&
5810
+ target[STORE_OPTIMISTIC_OVERRIDE] === undefined &&
5811
+ !writeOverride &&
5812
+ (Writing === null || !Writing.has(receiver))
5813
+ ) {
5814
+ const nodes = target[STORE_NODE];
5815
+ const node = nodes && nodes[property];
5816
+ if (node !== undefined && target[STORE_VALUE][$TARGET] === undefined) {
5817
+ let value = read(node);
5818
+ if (value === $DELETED) value = undefined;
5819
+ return isWrappable(value) ? wrap(value, target) : value;
5820
+ }
5821
+ }
5719
5822
  const selfRead = getObserver() === target[STORE_FIREWALL];
5720
5823
  const nodes = getNodes(target, STORE_NODE);
5721
5824
  const tracked = selfRead ? undefined : nodes[property];
@@ -6305,8 +6408,10 @@ function clearOptimisticOverride(target, completing) {
6305
6408
  }
6306
6409
  }
6307
6410
  if (!remaining) {
6308
- delete target[STORE_OPTIMISTIC_OVERRIDE];
6309
- delete target[STORE_OPTIMISTIC_OWNERS];
6411
+ // Assignment, not delete: StoreNode targets share one pre-initialized
6412
+ // hidden class (see createStoreProxy) and a delete would demote it.
6413
+ target[STORE_OPTIMISTIC_OVERRIDE] = undefined;
6414
+ target[STORE_OPTIMISTIC_OWNERS] = undefined;
6310
6415
  }
6311
6416
  // Notify $TRACK
6312
6417
  if (cleared && nodes?.[$TRACK]) {
@@ -6499,6 +6604,18 @@ function snapshotImpl(item, track, map, lookup) {
6499
6604
  if (pendingCheckActive) witnessAffectsMark(target);
6500
6605
  }
6501
6606
  override = mergedOverlay(target);
6607
+ // A derived store's STORE_VALUE is the inner store's live proxy
6608
+ // (store-in-store: createOptimisticStore/createProjection over a store).
6609
+ // Without an overlay of its own, the fast path below would map to — and
6610
+ // could return — that proxy verbatim. Recurse instead so the
6611
+ // inner store's own target branch unwraps it (chains of any depth).
6612
+ // With an overlay, a fresh `result` is always built, so the walk over the
6613
+ // inner proxy already copies plain values.
6614
+ if (!override && target[STORE_VALUE][$TARGET]) {
6615
+ unwrapped = snapshotImpl(target[STORE_VALUE], track, map, lookup);
6616
+ map.set(item, unwrapped);
6617
+ return unwrapped;
6618
+ }
6502
6619
  isArray = Array.isArray(target[STORE_VALUE]);
6503
6620
  map.set(
6504
6621
  item,
@@ -6912,19 +7029,7 @@ function updateKeyedMap() {
6912
7029
  this._items = newItems.slice(0);
6913
7030
  this._len = newLen;
6914
7031
  } else {
6915
- let start,
6916
- end,
6917
- newEnd,
6918
- item,
6919
- key,
6920
- newIndices,
6921
- newIndicesNext,
6922
- removed,
6923
- created,
6924
- temp = new Array(newLen),
6925
- tempNodes = new Array(newLen);
6926
- rows = this._rows ? new Array(newLen) : undefined;
6927
- indexes = this._indexes ? new Array(newLen) : undefined;
7032
+ let start, end, newEnd, item, key, newIndices, newIndicesNext, removed, created;
6928
7033
  // skip common prefix
6929
7034
  for (
6930
7035
  start = 0, end = Math.min(this._len, newLen);
@@ -6935,7 +7040,8 @@ function updateKeyedMap() {
6935
7040
  ) {
6936
7041
  if (this._rows) setSignal(this._rows[start], newItems[start]);
6937
7042
  }
6938
- // common suffix
7043
+ // skip common suffix — counted only; retained entries land in one pass
7044
+ // at commit instead of being staged and copied twice
6939
7045
  for (
6940
7046
  end = this._len - 1, newEnd = newLen - 1;
6941
7047
  end >= start &&
@@ -6943,13 +7049,21 @@ function updateKeyedMap() {
6943
7049
  (this._items[end] === newItems[newEnd] ||
6944
7050
  (this._rows && compare(this._key, this._items[end], newItems[newEnd])));
6945
7051
  end--, newEnd--
6946
- ) {
6947
- temp[newEnd] = this._mappings[end];
6948
- tempNodes[newEnd] = this._nodes[end];
6949
- rows && (rows[newEnd] = this._rows[end]);
6950
- indexes && (indexes[newEnd] = this._indexes[end]);
7052
+ );
7053
+ // no structural change (every position matched in place at equal
7054
+ // length — the common post-reconcile shape): keep the same mapped
7055
+ // array identity so downstream consumers don't re-run at all
7056
+ if (start === newLen && this._len === newLen) {
7057
+ this._items = newItems.slice(0);
7058
+ return;
6951
7059
  }
6952
- // 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
7060
+ const dif = newLen - this._len;
7061
+ const temp = new Array(newLen);
7062
+ const tempNodes = new Array(newLen);
7063
+ rows = this._rows ? new Array(newLen) : undefined;
7064
+ indexes = this._indexes ? new Array(newLen) : undefined;
7065
+ // 0) prepare a map of all indices in the changed window of newItems,
7066
+ // scanning backwards so we encounter them in natural order
6953
7067
  newIndices = new Map();
6954
7068
  newIndicesNext = new Array(newEnd + 1);
6955
7069
  for (j = newEnd; j >= start; j--) {
@@ -6959,7 +7073,9 @@ function updateKeyedMap() {
6959
7073
  newIndicesNext[j] = i === undefined ? -1 : i;
6960
7074
  newIndices.set(key, j);
6961
7075
  }
6962
- // 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not, queue them for disposal at commit
7076
+ // 1) step through the old changed window and see if items can be found
7077
+ // in the new set; if so, stage them at their new positions; if not,
7078
+ // queue them for disposal at commit
6963
7079
  for (i = start; i <= end; i++) {
6964
7080
  item = this._items[i];
6965
7081
  key = this._key ? this._key(item) : item;
@@ -6975,8 +7091,8 @@ function updateKeyedMap() {
6975
7091
  }
6976
7092
  // 2) create new rows into the temp arrays; an abort disposes only these
6977
7093
  try {
6978
- for (j = start; j < newLen; j++) {
6979
- if (j in temp) continue;
7094
+ for (j = start; j <= newEnd; j++) {
7095
+ if (tempNodes[j] !== undefined) continue;
6980
7096
  (created ??= []).push((tempNodes[j] = createOwner()));
6981
7097
  temp[j] = runWithOwner(tempNodes[j], mapper);
6982
7098
  }
@@ -6984,24 +7100,39 @@ function updateKeyedMap() {
6984
7100
  if (created) for (i = 0; i < created.length; i++) created[i].dispose();
6985
7101
  throw err;
6986
7102
  }
6987
- // 3) commit: land positions, then dispose exited rows
6988
- for (j = start; j < newLen; j++) {
6989
- this._mappings[j] = temp[j];
6990
- this._nodes[j] = tempNodes[j];
7103
+ // 3) commit: land the retained prefix and suffix plus the staged window
7104
+ // into the fresh arrays, swap them in (new identity for downstream
7105
+ // change propagation), then dispose exited rows
7106
+ for (i = 0; i < start; i++) {
7107
+ temp[i] = this._mappings[i];
7108
+ tempNodes[i] = this._nodes[i];
7109
+ rows && (rows[i] = this._rows[i]);
7110
+ indexes && (indexes[i] = this._indexes[i]);
7111
+ }
7112
+ for (j = start; j <= newEnd; j++) {
7113
+ if (rows) setSignal(rows[j], newItems[j]);
7114
+ if (indexes) setSignal(indexes[j], j);
7115
+ }
7116
+ for (j = newEnd + 1; j < newLen; j++) {
7117
+ temp[j] = this._mappings[j - dif];
7118
+ tempNodes[j] = this._nodes[j - dif];
6991
7119
  if (rows) {
6992
- this._rows[j] = rows[j];
6993
- setSignal(this._rows[j], newItems[j]);
7120
+ rows[j] = this._rows[j - dif];
7121
+ setSignal(rows[j], newItems[j]);
6994
7122
  }
6995
7123
  if (indexes) {
6996
- this._indexes[j] = indexes[j];
6997
- setSignal(this._indexes[j], j);
7124
+ indexes[j] = this._indexes[j - dif];
7125
+ if (dif !== 0) setSignal(indexes[j], j);
6998
7126
  }
6999
7127
  }
7000
- if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
7001
- // 4) in case the new set is shorter than the old, set the length of the mapped array
7002
- this._mappings = this._mappings.slice(0, (this._len = newLen));
7003
- // 5) save a copy of the mapped items for the next update
7128
+ this._mappings = temp;
7129
+ this._nodes = tempNodes;
7130
+ rows && (this._rows = rows);
7131
+ indexes && (this._indexes = indexes);
7132
+ this._len = newLen;
7133
+ // save a copy of the mapped items for the next update
7004
7134
  this._items = newItems.slice(0);
7135
+ if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
7005
7136
  }
7006
7137
  });
7007
7138
  return this._mappings;