@solidjs/signals 2.0.0-beta.25 → 2.0.0-beta.27

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.
@@ -1,4 +1,4 @@
1
- import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js";
1
+ import { $REFRESH, STORE_SNAPSHOT_PROPS, NOT_PENDING, unwrapOverride, STATUS_ERROR, STATUS_UNINITIALIZED, STATUS_PENDING, NO_SNAPSHOT } from "../core/constants.js";
2
2
 
3
3
  import { suppressComputedRecompute, isEqual, signal, pendingCheckActive, untrack, setSignal, read, readNodeFast, READ_SLOW, snapshotCaptureActive, snapshotSources } from "../core/core.js";
4
4
 
@@ -122,6 +122,14 @@ function isRawValue(e) {
122
122
 
123
123
  function markRawOne(e) {
124
124
  if (isWrappable(e)) {
125
+ // A store proxy is already tracked elsewhere: the shallow boundary passes
126
+ // it through by reference (replaced, never edited — same slot semantics
127
+ // as a raw) instead of claiming it raw. The sticky mark is global, so
128
+ // marking a live proxy would make wrap() serve it verbatim through every
129
+ // OTHER store too — downstream deep stores then captured it instead of
130
+ // wrapping it in their own family, and their writes landed in the
131
+ // upstream store's override layer (#2932).
132
+ if (e[$TARGET] !== undefined) return;
125
133
  rawValuesUsed = true;
126
134
  rawValues.add(e);
127
135
  }
@@ -173,11 +181,39 @@ function wrapShallow(e) {
173
181
  return r;
174
182
  }
175
183
 
184
+ const OBJECT_PROTO = Object.prototype;
185
+
186
+ // Per-prototype memo for the custom-proto branch of isWrappable: the verdict
187
+ // is fully determined by the prototype (tag and Node lineage both live on
188
+ // the chain), so each class pays the tag call once — not per read.
189
+ const wrappableProtos = new WeakMap;
190
+
176
191
  function isWrappable(e) {
177
192
  if (e == null || typeof e !== "object" || Object.isFrozen(e)) return false;
178
- // Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node`
179
- // are observed at call time).
180
- return typeof Node === "undefined" || !(e instanceof Node);
193
+ // Plain data and user class instances wrap; platform objects never do
194
+ // (#2952). Native code brand-checks internal slots and throws through a
195
+ // proxy (`Map.prototype.size`, `Date.prototype.getTime`, ...), so
196
+ // collections and other built-ins can't honestly be stores — they get the
197
+ // markRaw-children contract automatically: served raw, mutations land raw,
198
+ // the property holding them still tracks (reassignment notifies). The tag
199
+ // check separates them structurally: user classes stringify as
200
+ // `[object Object]` while every native/host object carries its own brand
201
+ // (`[object Map]`, `[object Date]`, `[object Headers]`, ...), including
202
+ // subclasses, which inherit the tag. getPrototypeOf keeps the hot path
203
+ // (plain and null-proto objects) intrinsic-only — no property lookup.
204
+ const t = Object.getPrototypeOf(e);
205
+ if (t === OBJECT_PROTO || t === null) return true;
206
+ if (Array.isArray(e)) return true;
207
+ let r = wrappableProtos.get(t);
208
+ if (r === undefined) {
209
+ r = Object.prototype.toString.call(e) === "[object Object]" && (
210
+ // Dynamic Node check (kept dynamic so test/SSR overrides of
211
+ // `globalThis.Node` are observed at call time): shimmed DOMs implement
212
+ // nodes as plain user classes, which pass the tag check.
213
+ typeof Node === "undefined" || !(e instanceof Node));
214
+ wrappableProtos.set(t, r);
215
+ }
216
+ return r;
181
217
  }
182
218
 
183
219
  let writeOverride = false;
@@ -255,7 +291,7 @@ function ownEnumerableKeysPlain(e) {
255
291
  * The value a store leaf's backing signal currently shows to readers: active
256
292
  * override, else held pending value, else committed value.
257
293
  */ function visibleNodeValue(e) {
258
- return e.Ee !== undefined && e.Ee !== NOT_PENDING ? unwrapOverride(e.Ee) : e.Ne !== NOT_PENDING ? e.Ne : e.Pe;
294
+ return e.Ae !== undefined && e.Ae !== NOT_PENDING ? unwrapOverride(e.Ae) : e.De !== NOT_PENDING ? e.De : e.Ue;
259
295
  }
260
296
 
261
297
  function hasOwnStoreProperty(e, t) {
@@ -306,11 +342,11 @@ function getNode(e, t, r, n, o = isEqual, i) {
306
342
  }
307
343
  }, e[STORE_FIREWALL]);
308
344
  if (e[STORE_OPTIMISTIC]) {
309
- O.Ee = NOT_PENDING;
345
+ O.Ae = NOT_PENDING;
310
346
  }
311
347
  if (i && r in i) {
312
348
  const e = i[r];
313
- O.Ve = e === undefined ? NO_SNAPSHOT : e;
349
+ O.xe = e === undefined ? NO_SNAPSHOT : e;
314
350
  snapshotSources?.add(O);
315
351
  }
316
352
  if (typeof r === "symbol" && r !== $TRACK && r !== $AFFECTS) symbolKeyedRecords.add(t);
@@ -574,8 +610,8 @@ function getPropertyDescriptor(e, t, r) {
574
610
  function prepareStoreWrite(e, t, r) {
575
611
  if (e[STORE_OPTIMISTIC]) {
576
612
  const t = e[STORE_FIREWALL];
577
- if (t?.Ue) {
578
- globalQueue.initTransition(t.Ue);
613
+ if (t?.Ie) {
614
+ globalQueue.initTransition(t.Ie);
579
615
  }
580
616
  }
581
617
  const n = e[STORE_VALUE];
@@ -618,8 +654,11 @@ function prepareStoreWrite(e, t, r) {
618
654
  * concurrent actions writing disjoint keys must revert independently, exactly
619
655
  * like optimistic signal nodes do via the transition's _optimisticNodes.
620
656
  * `activeTransition` is the write's transaction (action() opens it before the
621
- * body runs); null marks an ambient write that clears at plain flush end.
622
- * Same-key writes across actions keep last-write-wins layer semantics.
657
+ * body runs); null marks an ambient write, which clears at plain flush end
658
+ * unless its flush's transition is blocked on the store's own in-flight truth
659
+ * (pending firewall, #2951), in which case it rides that transaction to
660
+ * settle. Same-key writes across actions keep last-write-wins layer
661
+ * semantics.
623
662
  */ function stampOptimisticOwner(e, t, r) {
624
663
  if (t === STORE_OPTIMISTIC_OVERRIDE) (e[STORE_OPTIMISTIC_OWNERS] ??= Object.create(null))[r] = activeTransition;
625
664
  }
@@ -672,16 +711,36 @@ function notifyStoreProperty(e, t, r, n, o, i) {
672
711
  let Writing = null;
673
712
 
674
713
  /**
675
- * A derived store's seed is a draft for the derive function, never an
676
- * observable value (#2897): until the firewall first resolves there is
677
- * nothing to read, so every consumer path throws NotReady tracked reads
678
- * through their node (core read()), and the untracked fall-throughs in the
679
- * traps through this guard. Returning the seed leaked it; returning
680
- * `undefined` would break non-nullable types. Callers exempt the firewall
681
- * itself (the derive function works its own draft while uninitialized).
682
- */ function throwIfUninitialized(e) {
714
+ * A derived store follows async memo rules (#2897 ruling): its seed is a
715
+ * draft for the derive function, never an observable value, and an errored
716
+ * derive is an error state, never a silent stale/seed serve. Until the
717
+ * firewall first resolves there is nothing to read, so every consumer path
718
+ * throws NotReady tracked reads through their node (core read()), and the
719
+ * untracked fall-throughs in the traps through this guard. Returning the
720
+ * seed leaked it; returning `undefined` would break non-nullable types.
721
+ * Callers exempt the firewall itself (the derive function works its own
722
+ * draft while uninitialized).
723
+ *
724
+ * Error rail: a firewall carrying STATUS_ERROR throws its error for every
725
+ * late reader — memo parity, where read()'s error branch does the same for
726
+ * plain computeds. Rejection clears STATUS_UNINITIALIZED at commit, so
727
+ * without this check late readers silently got the seed while settle-time
728
+ * subscribers saw the error.
729
+ *
730
+ * Loading rail: the veto requires the firewall to still be in flight, not
731
+ * just flagged: STATUS_UNINITIALIZED's clear is deferred to batch commit,
732
+ * so during the settle flush a firewall that has already recomputed — and
733
+ * reconciled real values into STORE_VALUE — still carries the stale flag.
734
+ * STATUS_PENDING is the live bit (it clears eagerly at settle, mirroring
735
+ * core read()'s verdict), so gating on it stops the guard from throwing a
736
+ * fresh NotReadyError that nothing would ever sweep. #2944: mapArray's
737
+ * keyed diff reads items inside its internal owner (untracked by design)
738
+ * in exactly this window, and the stale throw wedged <For> permanently.
739
+ */ function throwIfUnreadable(e) {
683
740
  const t = e[STORE_FIREWALL];
684
- if (t && t.S & STATUS_UNINITIALIZED) throw t._ ?? new NotReadyError(t);
741
+ if (!t) return;
742
+ const r = t.S;
743
+ if (r & STATUS_ERROR || r & STATUS_UNINITIALIZED && r & STATUS_PENDING) throw t._ ?? new NotReadyError(t);
685
744
  }
686
745
 
687
746
  const storeTraps = {
@@ -731,20 +790,20 @@ const storeTraps = {
731
790
  const s = getOverlayLayer(e, t);
732
791
  const E = !!s;
733
792
  const S = !!e[STORE_VALUE][$TARGET];
734
- const f = s ?? e[STORE_VALUE];
793
+ const T = s ?? e[STORE_VALUE];
735
794
  if (!i) {
736
- const n = Object.getOwnPropertyDescriptor(f, t);
795
+ const n = Object.getOwnPropertyDescriptor(T, t);
737
796
  if (n && n.get) return n.get.call(r);
738
797
  if (!n && !E && e[STORE_CUSTOM_PROTO]) {
739
- const e = unwrapStoreValue(f);
798
+ const e = unwrapStoreValue(T);
740
799
  if (hasInheritedAccessor(e, t)) {
741
- return Reflect.get(f, t, r);
800
+ return Reflect.get(T, t, r);
742
801
  }
743
802
  }
744
803
  }
745
804
  if (writeOnly(r)) {
746
805
  if (isPrototypePollutionKey(t) && !hasOwnStoreProperty(e, t)) return undefined;
747
- let r = i && (E || !S) ? visibleNodeValue(i) : f[t];
806
+ let r = i && (E || !S) ? visibleNodeValue(i) : T[t];
748
807
  r === $DELETED && (r = undefined);
749
808
  if (!isWrappable(r)) return r;
750
809
  // Shallow boundary: records are replaced, never edited in place. Reads
@@ -756,19 +815,26 @@ const storeTraps = {
756
815
  Writing?.add(n);
757
816
  return n;
758
817
  }
759
- let c = i ? E || !S ? read(o[t]) : (read(o[t]), f[t]) : f[t];
818
+ let c = i ? E || !S ? read(o[t]) : (read(o[t]), T[t]) : T[t];
760
819
  c === $DELETED && (c = undefined);
761
820
  if (!i) {
762
- if (!E && typeof c === "function" && !Object.prototype.hasOwnProperty.call(f, t)) {
821
+ if (!E && typeof c === "function" && !Object.prototype.hasOwnProperty.call(T, t)) {
763
822
  let t;
764
- return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? c.bind(f) : c;
823
+ return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? c.bind(T) : c;
765
824
  } else if (getObserver() && !n) {
766
825
  return read(getNode(e, o, t, isWrappable(c) ? wrap(c, e) : c, isEqual, e[STORE_SNAPSHOT_PROPS]));
767
826
  }
768
827
  }
769
828
  // Untracked fall-through (tracked reads already threw via their node in
770
829
  // read(); the dev strictRead error above wins first for memo parity).
771
- if (!n) throwIfUninitialized(e);
830
+ // Observer-present reads must NOT re-consult the flag here: during the
831
+ // settle flush the firewall has recomputed (first values live on the
832
+ // pending rail, served by read() above) but its UNINITIALIZED clear is
833
+ // deferred to batch commit — vetoing read()'s verdict with the stale flag
834
+ // threw a fresh NotReadyError for an already-settled source, which no
835
+ // sweep would ever release (#2938: projection over an async store wedged
836
+ // its Loading boundary on `undefined`).
837
+ if (!n && !getObserver()) throwIfUnreadable(e);
772
838
  return isWrappable(c) ? wrap(c, e) : c;
773
839
  },
774
840
  has(e, t) {
@@ -788,7 +854,7 @@ const storeTraps = {
788
854
  if (getObserver()) {
789
855
  return read(getNode(e, o, t, n));
790
856
  }
791
- throwIfUninitialized(e);
857
+ throwIfUnreadable(e);
792
858
  return n;
793
859
  },
794
860
  set(e, t, r) {
@@ -800,37 +866,48 @@ const storeTraps = {
800
866
  const s = getOverlayLayer(e, t);
801
867
  const E = s ? s[t] : o;
802
868
  const S = s ? s[t] !== $DELETED : t in e[STORE_VALUE];
803
- const f = unwrapStoreValue(r);
804
- if (e[STORE_SHALLOW] && isWrappable(f)) rawValues.add(f);
869
+ // Shallow slots hold store proxies verbatim (pass-through reference,
870
+ // never raw-marked see markRawOne/#2932); everything else unwraps
871
+ // and marks as usual.
872
+ const T = !!e[STORE_SHALLOW] && r?.[$TARGET] !== undefined;
873
+ const c = T ? r : unwrapStoreValue(r);
874
+ if (e[STORE_SHALLOW] && !T && isWrappable(c)) {
875
+ // Flip the live gate too: a bare add was inert unless something else
876
+ // had already marked a raw somewhere (wrap() checks rawValuesUsed
877
+ // first), so the documented set-trap ingest mark silently no-oped in
878
+ // apps whose only shallow data arrived through writes.
879
+ rawValuesUsed = true;
880
+ rawValues.add(c);
881
+ }
805
882
  // Symbol-keyed writes on arrays are metadata, not index writes — never run
806
883
  // them through the numeric index/length machinery (`parseInt` on a symbol
807
884
  // throws). #2769
808
- const c = typeof t === "string" ? Number(t) : -1;
809
- const R = Array.isArray(O) && Number.isInteger(c) && c >= 0 && c < 4294967295 && String(c) === t;
810
- const T = R ? c + 1 : 0;
811
- const u = R && (getOverlayLayer(e, "length") ?? O).length;
812
- const a = R && T > u ? T : undefined;
813
- if (E === f && a === undefined) return true;
885
+ const f = typeof t === "string" ? Number(t) : -1;
886
+ const R = Array.isArray(O) && Number.isInteger(f) && f >= 0 && f < 4294967295 && String(f) === t;
887
+ const u = R ? f + 1 : 0;
888
+ const a = R && (getOverlayLayer(e, "length") ?? O).length;
889
+ const l = R && u > a ? u : undefined;
890
+ if (E === c && l === undefined) return true;
814
891
  armOptimisticStoreWrite(e, n);
815
- if (f !== undefined && f === o && a === undefined) {
892
+ if (c !== undefined && c === o && l === undefined) {
816
893
  delete e[i]?.[t];
817
894
  if (i === STORE_OPTIMISTIC_OVERRIDE) delete e[STORE_OPTIMISTIC_OWNERS]?.[t];
818
895
  } else {
819
896
  const r = e[i] || (e[i] = Object.create(null));
820
- r[t] = f;
897
+ r[t] = c;
821
898
  stampOptimisticOwner(e, i, t);
822
- if (a !== undefined) {
823
- r.length = a;
899
+ if (l !== undefined) {
900
+ r.length = l;
824
901
  stampOptimisticOwner(e, i, "length");
825
902
  }
826
903
  }
827
- notifyStoreProperty(e, t, "set", f, E, S);
904
+ notifyStoreProperty(e, t, "set", c, E, S);
828
905
  // Shrinking an array's length must remove the truncated indices, otherwise
829
906
  // they leak through `has`, `ownKeys`, and (tracked) index reads from the
830
907
  // underlying value. Mark each as deleted and notify so reactive reads update. #2768
831
- if (Array.isArray(O) && t === "length" && typeof f === "number" && typeof E === "number" && f < E) {
908
+ if (Array.isArray(O) && t === "length" && typeof c === "number" && typeof E === "number" && c < E) {
832
909
  const t = e[i] || (e[i] = Object.create(null));
833
- for (let r = f; r < E; r++) {
910
+ for (let r = c; r < E; r++) {
834
911
  if (t[r] === $DELETED) continue;
835
912
  const n = r in t ? t[r] : O[r];
836
913
  if (!(r in t) && !(r in O)) continue;
@@ -840,13 +917,13 @@ const storeTraps = {
840
917
  }
841
918
  }
842
919
  // notify length change
843
- if (Array.isArray(O) && t !== "length" && a !== undefined) {
920
+ if (Array.isArray(O) && t !== "length" && l !== undefined) {
844
921
  const t = getNodes(e, STORE_NODE);
845
922
  if (t.length) {
846
- setSignal(t.length, a);
923
+ setSignal(t.length, l);
847
924
  } else if (!projectionWriteActive && !e[STORE_OPTIMISTIC]) {
848
- const r = upsertStoreNode(e, t, "length", u, e[STORE_SNAPSHOT_PROPS]);
849
- setSignal(r, a);
925
+ const r = upsertStoreNode(e, t, "length", a, e[STORE_SNAPSHOT_PROPS]);
926
+ setSignal(r, l);
850
927
  }
851
928
  }
852
929
  if (false) ;
@@ -907,7 +984,7 @@ const storeTraps = {
907
984
  // path is exempt (like the get/has traps' writeOnly early returns):
908
985
  // the first landing's reconcile enumerates the store while
909
986
  // STATUS_UNINITIALIZED is still set — it IS the initialization.
910
- if (!getObserver() && !writeOnly(e[$PROXY])) throwIfUninitialized(e);
987
+ if (!getObserver() && !writeOnly(e[$PROXY])) throwIfUnreadable(e);
911
988
  }
912
989
  // Merge optimistic override with regular override for key enumeration
913
990
  let t = getKeys(e[STORE_VALUE], e[STORE_OVERRIDE], false);
@@ -3,6 +3,8 @@ import type { Computed, Link } from "./types.js";
3
3
  export declare function addPendingSource(el: Computed<any>, source: Computed<any>): boolean;
4
4
  export declare function setPendingError(el: Computed<any>, source?: Computed<any>, error?: any): void;
5
5
  export declare function forEachDependent(el: Computed<any>, fn: (node: Computed<any>, link: Link) => void): void;
6
+ export declare function releaseSettledDependents(el: Computed<any>): void;
7
+ export declare function settleErroredDependents(el: Computed<any>, error: any): void;
6
8
  export declare function settlePendingSource(el: Computed<any>): void;
7
9
  export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>;
8
10
  export declare function handleAsync<T>(el: Computed<T>, result: T | PromiseLike<T> | AsyncIterable<T>, setter?: (value: T) => void): T;
@@ -64,6 +64,7 @@ export declare class Queue implements IQueue {
64
64
  _parent: IQueue | null;
65
65
  _queues: [QueueCallback[], QueueCallback[]];
66
66
  _children: IQueue[];
67
+ _ranAt: number;
67
68
  created: number;
68
69
  addChild(child: IQueue): void;
69
70
  removeChild(child: IQueue): void;
@@ -14,8 +14,8 @@ import { type NoFn, type ProjectionOptions, type Store, type StoreSetter } from
14
14
  * optimistic overlay reverts after each transition.
15
15
  *
16
16
  * `options.key` defaults to `"id"`; specify it only when your data uses a
17
- * different identity field (e.g. `{ key: "uuid" }` or `{ key: t => t.slug }`).
18
- * Restating the default just adds noise.
17
+ * different identity field (e.g. `{ key: "uuid" }` or `{ key: t => t.slug }`),
18
+ * or `null` to merge positionally. Restating the default just adds noise.
19
19
  *
20
20
  * @example
21
21
  * ```ts
@@ -12,6 +12,10 @@ export declare function createProjectionInternal<T extends object = {}>(fn: (dra
12
12
  * items keep their proxy identity — only added/removed items are
13
13
  * created/disposed.
14
14
  *
15
+ * If the derive returns a different entity than the one currently held (the
16
+ * `/users/1` → `/users/2` shape), the store swaps to it rather than merging,
17
+ * and nothing below it is treated as surviving.
18
+ *
15
19
  * Returns the projected store directly (no setter — reads only).
16
20
  *
17
21
  * Use this when you want the structural-sharing / per-property tracking
@@ -24,7 +28,8 @@ export declare function createProjectionInternal<T extends object = {}>(fn: (dra
24
28
  * @param seed the backing store value to wrap and reconcile into
25
29
  * @param options `ProjectionOptions` — `name`, `key`. `key` defaults to
26
30
  * `"id"`; specify it only when your data uses a different identity field
27
- * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`).
31
+ * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`), or `null` to merge
32
+ * positionally with no keyed pass.
28
33
  *
29
34
  * @example
30
35
  * ```ts
@@ -61,5 +66,5 @@ export declare function createProjection<T extends object = {}>(fn: (draft: T) =
61
66
  * instead of `STORE_OPTIMISTIC_OVERRIDE` even when an async yield fires outside
62
67
  * the outer `setProjectionWriteActive` scope.
63
68
  */
64
- export declare function runProjectionComputed<T extends object>(wrappedStore: Store<T>, fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, key: string | ((item: NonNullable<any>) => any), wrapCommit?: (write: () => void) => void, onDraftWrite?: () => void): Computed<void | T>;
69
+ export declare function runProjectionComputed<T extends object>(wrappedStore: Store<T>, fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, key: string | ((item: NonNullable<any>) => any) | null, wrapCommit?: (write: () => void) => void, onDraftWrite?: () => void): Computed<void | T>;
65
70
  export declare function createWriteTraps(isActive?: () => boolean, onDraftWrite?: () => void): ProxyHandler<any>;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Shared body of `reconcile()` and the projection commit. `replace` is the
3
+ * only difference: a projection commit is a value swap, not a merge — its root
4
+ * proxy is a cell handed out by `createProjection` that can never change
5
+ * reference, so a derive returning a different entity is not the slot mistake
6
+ * `reconcile()` throws on. Nothing below the root survives that swap, which is
7
+ * the rule the keyed diff already applies at a nested slot on a key mismatch.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare function reconcileState(value: any, state: any, key: any, replace: boolean): void;
1
12
  /**
2
13
  * Returns a draft-mutating function that smart-merges `value` into a store,
3
14
  * preserving fine-grained reactivity: only changed leaves trigger updates.
@@ -12,6 +23,9 @@
12
23
  * the classic pattern for fixed-shape data that churns in place (dashboards,
13
24
  * monitors), where no keyed diff pass is needed or wanted.
14
25
  *
26
+ * Merging into a slot that holds a *different* entity throws — the caller
27
+ * picked the slot, so a key mismatch there is a bug.
28
+ *
15
29
  * @param value the next state to merge in
16
30
  * @param key property name (string) or extractor function for stable
17
31
  * identity (default `"id"`); pass `null` for positional merging
@@ -29,8 +29,8 @@ export interface StoreOptions {
29
29
  }
30
30
  /** Options for derived/projected stores created with `createStore(fn)`, `createProjection`, or `createOptimisticStore(fn)`. */
31
31
  export interface ProjectionOptions extends StoreOptions {
32
- /** Key property name or function for reconciliation identity */
33
- key?: string | ((item: NonNullable<any>) => any);
32
+ /** Key property name or function for reconciliation identity; `null` merges positionally */
33
+ key?: string | ((item: NonNullable<any>) => any) | null;
34
34
  /** Single-layer store: root keys reactive, values raw records replaced by reference */
35
35
  shallow?: boolean;
36
36
  }
@@ -3,6 +3,8 @@ import type { Computed, Link } from "./types.cjs";
3
3
  export declare function addPendingSource(el: Computed<any>, source: Computed<any>): boolean;
4
4
  export declare function setPendingError(el: Computed<any>, source?: Computed<any>, error?: any): void;
5
5
  export declare function forEachDependent(el: Computed<any>, fn: (node: Computed<any>, link: Link) => void): void;
6
+ export declare function releaseSettledDependents(el: Computed<any>): void;
7
+ export declare function settleErroredDependents(el: Computed<any>, error: any): void;
6
8
  export declare function settlePendingSource(el: Computed<any>): void;
7
9
  export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>;
8
10
  export declare function handleAsync<T>(el: Computed<T>, result: T | PromiseLike<T> | AsyncIterable<T>, setter?: (value: T) => void): T;
@@ -64,6 +64,7 @@ export declare class Queue implements IQueue {
64
64
  _parent: IQueue | null;
65
65
  _queues: [QueueCallback[], QueueCallback[]];
66
66
  _children: IQueue[];
67
+ _ranAt: number;
67
68
  created: number;
68
69
  addChild(child: IQueue): void;
69
70
  removeChild(child: IQueue): void;
@@ -14,8 +14,8 @@ import { type NoFn, type ProjectionOptions, type Store, type StoreSetter } from
14
14
  * optimistic overlay reverts after each transition.
15
15
  *
16
16
  * `options.key` defaults to `"id"`; specify it only when your data uses a
17
- * different identity field (e.g. `{ key: "uuid" }` or `{ key: t => t.slug }`).
18
- * Restating the default just adds noise.
17
+ * different identity field (e.g. `{ key: "uuid" }` or `{ key: t => t.slug }`),
18
+ * or `null` to merge positionally. Restating the default just adds noise.
19
19
  *
20
20
  * @example
21
21
  * ```ts
@@ -12,6 +12,10 @@ export declare function createProjectionInternal<T extends object = {}>(fn: (dra
12
12
  * items keep their proxy identity — only added/removed items are
13
13
  * created/disposed.
14
14
  *
15
+ * If the derive returns a different entity than the one currently held (the
16
+ * `/users/1` → `/users/2` shape), the store swaps to it rather than merging,
17
+ * and nothing below it is treated as surviving.
18
+ *
15
19
  * Returns the projected store directly (no setter — reads only).
16
20
  *
17
21
  * Use this when you want the structural-sharing / per-property tracking
@@ -24,7 +28,8 @@ export declare function createProjectionInternal<T extends object = {}>(fn: (dra
24
28
  * @param seed the backing store value to wrap and reconcile into
25
29
  * @param options `ProjectionOptions` — `name`, `key`. `key` defaults to
26
30
  * `"id"`; specify it only when your data uses a different identity field
27
- * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`).
31
+ * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`), or `null` to merge
32
+ * positionally with no keyed pass.
28
33
  *
29
34
  * @example
30
35
  * ```ts
@@ -61,5 +66,5 @@ export declare function createProjection<T extends object = {}>(fn: (draft: T) =
61
66
  * instead of `STORE_OPTIMISTIC_OVERRIDE` even when an async yield fires outside
62
67
  * the outer `setProjectionWriteActive` scope.
63
68
  */
64
- export declare function runProjectionComputed<T extends object>(wrappedStore: Store<T>, fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, key: string | ((item: NonNullable<any>) => any), wrapCommit?: (write: () => void) => void, onDraftWrite?: () => void): Computed<void | T>;
69
+ export declare function runProjectionComputed<T extends object>(wrappedStore: Store<T>, fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, key: string | ((item: NonNullable<any>) => any) | null, wrapCommit?: (write: () => void) => void, onDraftWrite?: () => void): Computed<void | T>;
65
70
  export declare function createWriteTraps(isActive?: () => boolean, onDraftWrite?: () => void): ProxyHandler<any>;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Shared body of `reconcile()` and the projection commit. `replace` is the
3
+ * only difference: a projection commit is a value swap, not a merge — its root
4
+ * proxy is a cell handed out by `createProjection` that can never change
5
+ * reference, so a derive returning a different entity is not the slot mistake
6
+ * `reconcile()` throws on. Nothing below the root survives that swap, which is
7
+ * the rule the keyed diff already applies at a nested slot on a key mismatch.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare function reconcileState(value: any, state: any, key: any, replace: boolean): void;
1
12
  /**
2
13
  * Returns a draft-mutating function that smart-merges `value` into a store,
3
14
  * preserving fine-grained reactivity: only changed leaves trigger updates.
@@ -12,6 +23,9 @@
12
23
  * the classic pattern for fixed-shape data that churns in place (dashboards,
13
24
  * monitors), where no keyed diff pass is needed or wanted.
14
25
  *
26
+ * Merging into a slot that holds a *different* entity throws — the caller
27
+ * picked the slot, so a key mismatch there is a bug.
28
+ *
15
29
  * @param value the next state to merge in
16
30
  * @param key property name (string) or extractor function for stable
17
31
  * identity (default `"id"`); pass `null` for positional merging
@@ -29,8 +29,8 @@ export interface StoreOptions {
29
29
  }
30
30
  /** Options for derived/projected stores created with `createStore(fn)`, `createProjection`, or `createOptimisticStore(fn)`. */
31
31
  export interface ProjectionOptions extends StoreOptions {
32
- /** Key property name or function for reconciliation identity */
33
- key?: string | ((item: NonNullable<any>) => any);
32
+ /** Key property name or function for reconciliation identity; `null` merges positionally */
33
+ key?: string | ((item: NonNullable<any>) => any) | null;
34
34
  /** Single-layer store: root keys reactive, values raw records replaced by reference */
35
35
  shallow?: boolean;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/signals",
3
- "version": "2.0.0-beta.25",
3
+ "version": "2.0.0-beta.27",
4
4
  "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",