@solidjs/signals 2.0.0-beta.23 → 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
@@ -2138,7 +2138,10 @@ function clearSnapshots() {
2138
2138
  if (snapshotSources) {
2139
2139
  for (const source of snapshotSources) {
2140
2140
  delete source._snapshotValue;
2141
- 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;
2142
2145
  }
2143
2146
  snapshotSources = null;
2144
2147
  }
@@ -4763,23 +4766,18 @@ function applyStateFast(next, target, keyFn) {
4763
4766
  let tracked;
4764
4767
  if (nodes) {
4765
4768
  tracked = nodes[$TRACK];
4766
- const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
4767
- for (let i = 0, len = keys.length; i < len; i++) {
4768
- const key = keys[i];
4769
- const node = nodes[key];
4770
- const previousValue = unwrap(previous[key]);
4771
- let nextValue = unwrap(next[key]);
4772
- if (previousValue === nextValue) continue;
4773
- if (
4774
- !previousValue ||
4775
- !isWrappable(previousValue) ||
4776
- !isWrappable(nextValue) ||
4777
- Array.isArray(previousValue) !== Array.isArray(nextValue) ||
4778
- (keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
4779
- ) {
4780
- tracked && setSignal(tracked, void 0);
4781
- node && setSignal(node, isWrappable(nextValue) ? wrap(nextValue, target) : nextValue);
4782
- } 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
+ }
4783
4781
  }
4784
4782
  }
4785
4783
  if (!tracked && target[STORE_DESC]) applyDescendants(previous, next, target, nodes, keyFn);
@@ -4792,6 +4790,24 @@ function applyStateFast(next, target, keyFn) {
4792
4790
  }
4793
4791
  }
4794
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
+ }
4795
4811
  function applyStateSlow(next, target, keyFn) {
4796
4812
  const previous = target[STORE_VALUE];
4797
4813
  const override = target[STORE_OVERRIDE];
@@ -4929,17 +4945,26 @@ function applyStateSlow(next, target, keyFn) {
4929
4945
  }
4930
4946
  }
4931
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;
4932
4951
  /**
4933
4952
  * Returns a draft-mutating function that smart-merges `value` into a store,
4934
- * preserving the identity of items whose `key` field matches between old and
4935
- * new states. Useful when applying server payloads or full-replacement data
4936
- * onto an existing store without losing fine-grained reactivity.
4953
+ * preserving fine-grained reactivity: only changed leaves trigger updates.
4937
4954
  *
4938
- * Items with the same key are updated in place (only changed properties
4939
- * trigger updates). Items added or removed update the corresponding signals.
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.
4959
+ *
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.
4940
4964
  *
4941
4965
  * @param value the next state to merge in
4942
- * @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
4943
4968
  *
4944
4969
  * @example
4945
4970
  * ```ts
@@ -4947,13 +4972,20 @@ function applyStateSlow(next, target, keyFn) {
4947
4972
  *
4948
4973
  * async function refresh() {
4949
4974
  * const fresh = await api.getTodos();
4950
- * setTodos(reconcile(fresh, "id")); // diff-merge by `id`
4975
+ * setTodos(reconcile(fresh)); // diff-merge by `id`
4951
4976
  * }
4977
+ *
4978
+ * // fixed-shape polling data — positional merge
4979
+ * setStats(reconcile(nextStats, null));
4952
4980
  * ```
4953
4981
  */
4954
- function reconcile(value, key) {
4982
+ function reconcile(value, key = "id") {
4955
4983
  return state => {
4956
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
+ }
4957
4989
  const keyFn = typeof key === "string" ? item => item[key] : key;
4958
4990
  const eq = keyFn(state);
4959
4991
  if (eq !== undefined && keyFn(value) !== eq)
@@ -5156,13 +5188,37 @@ const STORE_VALUE = "v",
5156
5188
  STORE_PARENT = "u",
5157
5189
  STORE_DESC = "d";
5158
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
+ }
5159
5213
  function createStoreProxy(value, traps = storeTraps, extend) {
5160
5214
  let newTarget;
5161
5215
  if (Array.isArray(value)) {
5162
5216
  newTarget = [];
5163
- newTarget.v = value;
5217
+ newTarget[STORE_VALUE] = value;
5218
+ initStoreFields(newTarget);
5164
5219
  } else {
5165
- newTarget = { v: value };
5220
+ newTarget = { [STORE_VALUE]: value };
5221
+ initStoreFields(newTarget);
5166
5222
  const unwrapped = value?.[$TARGET]?.[STORE_VALUE] ?? value;
5167
5223
  const proto = Object.getPrototypeOf(unwrapped);
5168
5224
  if (proto !== null && proto !== Object.prototype) {
@@ -5741,6 +5797,28 @@ const storeTraps = {
5741
5797
  trackSelf(target);
5742
5798
  return receiver;
5743
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
+ }
5744
5822
  const selfRead = getObserver() === target[STORE_FIREWALL];
5745
5823
  const nodes = getNodes(target, STORE_NODE);
5746
5824
  const tracked = selfRead ? undefined : nodes[property];
@@ -6330,8 +6408,10 @@ function clearOptimisticOverride(target, completing) {
6330
6408
  }
6331
6409
  }
6332
6410
  if (!remaining) {
6333
- delete target[STORE_OPTIMISTIC_OVERRIDE];
6334
- 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;
6335
6415
  }
6336
6416
  // Notify $TRACK
6337
6417
  if (cleared && nodes?.[$TRACK]) {
@@ -6524,6 +6604,18 @@ function snapshotImpl(item, track, map, lookup) {
6524
6604
  if (pendingCheckActive) witnessAffectsMark(target);
6525
6605
  }
6526
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
+ }
6527
6619
  isArray = Array.isArray(target[STORE_VALUE]);
6528
6620
  map.set(
6529
6621
  item,
@@ -6937,19 +7029,7 @@ function updateKeyedMap() {
6937
7029
  this._items = newItems.slice(0);
6938
7030
  this._len = newLen;
6939
7031
  } else {
6940
- let start,
6941
- end,
6942
- newEnd,
6943
- item,
6944
- key,
6945
- newIndices,
6946
- newIndicesNext,
6947
- removed,
6948
- created,
6949
- temp = new Array(newLen),
6950
- tempNodes = new Array(newLen);
6951
- rows = this._rows ? new Array(newLen) : undefined;
6952
- indexes = this._indexes ? new Array(newLen) : undefined;
7032
+ let start, end, newEnd, item, key, newIndices, newIndicesNext, removed, created;
6953
7033
  // skip common prefix
6954
7034
  for (
6955
7035
  start = 0, end = Math.min(this._len, newLen);
@@ -6960,7 +7040,8 @@ function updateKeyedMap() {
6960
7040
  ) {
6961
7041
  if (this._rows) setSignal(this._rows[start], newItems[start]);
6962
7042
  }
6963
- // common suffix
7043
+ // skip common suffix — counted only; retained entries land in one pass
7044
+ // at commit instead of being staged and copied twice
6964
7045
  for (
6965
7046
  end = this._len - 1, newEnd = newLen - 1;
6966
7047
  end >= start &&
@@ -6968,13 +7049,21 @@ function updateKeyedMap() {
6968
7049
  (this._items[end] === newItems[newEnd] ||
6969
7050
  (this._rows && compare(this._key, this._items[end], newItems[newEnd])));
6970
7051
  end--, newEnd--
6971
- ) {
6972
- temp[newEnd] = this._mappings[end];
6973
- tempNodes[newEnd] = this._nodes[end];
6974
- rows && (rows[newEnd] = this._rows[end]);
6975
- 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;
6976
7059
  }
6977
- // 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
6978
7067
  newIndices = new Map();
6979
7068
  newIndicesNext = new Array(newEnd + 1);
6980
7069
  for (j = newEnd; j >= start; j--) {
@@ -6984,7 +7073,9 @@ function updateKeyedMap() {
6984
7073
  newIndicesNext[j] = i === undefined ? -1 : i;
6985
7074
  newIndices.set(key, j);
6986
7075
  }
6987
- // 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
6988
7079
  for (i = start; i <= end; i++) {
6989
7080
  item = this._items[i];
6990
7081
  key = this._key ? this._key(item) : item;
@@ -7000,8 +7091,8 @@ function updateKeyedMap() {
7000
7091
  }
7001
7092
  // 2) create new rows into the temp arrays; an abort disposes only these
7002
7093
  try {
7003
- for (j = start; j < newLen; j++) {
7004
- if (j in temp) continue;
7094
+ for (j = start; j <= newEnd; j++) {
7095
+ if (tempNodes[j] !== undefined) continue;
7005
7096
  (created ??= []).push((tempNodes[j] = createOwner()));
7006
7097
  temp[j] = runWithOwner(tempNodes[j], mapper);
7007
7098
  }
@@ -7009,24 +7100,39 @@ function updateKeyedMap() {
7009
7100
  if (created) for (i = 0; i < created.length; i++) created[i].dispose();
7010
7101
  throw err;
7011
7102
  }
7012
- // 3) commit: land positions, then dispose exited rows
7013
- for (j = start; j < newLen; j++) {
7014
- this._mappings[j] = temp[j];
7015
- 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];
7016
7119
  if (rows) {
7017
- this._rows[j] = rows[j];
7018
- setSignal(this._rows[j], newItems[j]);
7120
+ rows[j] = this._rows[j - dif];
7121
+ setSignal(rows[j], newItems[j]);
7019
7122
  }
7020
7123
  if (indexes) {
7021
- this._indexes[j] = indexes[j];
7022
- setSignal(this._indexes[j], j);
7124
+ indexes[j] = this._indexes[j - dif];
7125
+ if (dif !== 0) setSignal(indexes[j], j);
7023
7126
  }
7024
7127
  }
7025
- if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
7026
- // 4) in case the new set is shorter than the old, set the length of the mapped array
7027
- this._mappings = this._mappings.slice(0, (this._len = newLen));
7028
- // 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
7029
7134
  this._items = newItems.slice(0);
7135
+ if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
7030
7136
  }
7031
7137
  });
7032
7138
  return this._mappings;