@solidjs/signals 2.0.0-beta.26 → 2.0.0-beta.28

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
 
@@ -181,11 +181,39 @@ function wrapShallow(e) {
181
181
  return r;
182
182
  }
183
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
+
184
191
  function isWrappable(e) {
185
192
  if (e == null || typeof e !== "object" || Object.isFrozen(e)) return false;
186
- // Dynamic Node check (kept dynamic so test/SSR overrides of `globalThis.Node`
187
- // are observed at call time).
188
- 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;
189
217
  }
190
218
 
191
219
  let writeOverride = false;
@@ -263,7 +291,7 @@ function ownEnumerableKeysPlain(e) {
263
291
  * The value a store leaf's backing signal currently shows to readers: active
264
292
  * override, else held pending value, else committed value.
265
293
  */ function visibleNodeValue(e) {
266
- return e.Ae !== undefined && e.Ae !== NOT_PENDING ? unwrapOverride(e.Ae) : e._e !== NOT_PENDING ? e._e : e.Ue;
294
+ return e.Ae !== undefined && e.Ae !== NOT_PENDING ? unwrapOverride(e.Ae) : e.De !== NOT_PENDING ? e.De : e.Ue;
267
295
  }
268
296
 
269
297
  function hasOwnStoreProperty(e, t) {
@@ -436,11 +464,11 @@ o) {
436
464
  // Callers guard on `pendingCheckActive`, which only flips inside
437
465
  // isPending() — the verdict layer is loaded and its hook installed.
438
466
  const r = e[STORE_NODE]?.[$AFFECTS];
439
- if (r?.t) GlobalQueue.Dt(r);
467
+ if (r?.t) GlobalQueue.Lt(r);
440
468
  if (affectsScopes.size) {
441
469
  const n = e[STORE_VALUE];
442
470
  for (const [e, o] of affectsScopes) {
443
- if (e !== r && e.t && o.scope.has(n) && (o.key === undefined || o.key === t)) GlobalQueue.Dt(e);
471
+ if (e !== r && e.t && o.scope.has(n) && (o.key === undefined || o.key === t)) GlobalQueue.Lt(e);
444
472
  }
445
473
  }
446
474
  }
@@ -626,8 +654,11 @@ function prepareStoreWrite(e, t, r) {
626
654
  * concurrent actions writing disjoint keys must revert independently, exactly
627
655
  * like optimistic signal nodes do via the transition's _optimisticNodes.
628
656
  * `activeTransition` is the write's transaction (action() opens it before the
629
- * body runs); null marks an ambient write that clears at plain flush end.
630
- * 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.
631
662
  */ function stampOptimisticOwner(e, t, r) {
632
663
  if (t === STORE_OPTIMISTIC_OVERRIDE) (e[STORE_OPTIMISTIC_OWNERS] ??= Object.create(null))[r] = activeTransition;
633
664
  }
@@ -680,16 +711,36 @@ function notifyStoreProperty(e, t, r, n, o, i) {
680
711
  let Writing = null;
681
712
 
682
713
  /**
683
- * A derived store's seed is a draft for the derive function, never an
684
- * observable value (#2897): until the firewall first resolves there is
685
- * nothing to read, so every consumer path throws NotReady tracked reads
686
- * through their node (core read()), and the untracked fall-throughs in the
687
- * traps through this guard. Returning the seed leaked it; returning
688
- * `undefined` would break non-nullable types. Callers exempt the firewall
689
- * itself (the derive function works its own draft while uninitialized).
690
- */ 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) {
691
740
  const t = e[STORE_FIREWALL];
692
- 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);
693
744
  }
694
745
 
695
746
  const storeTraps = {
@@ -739,20 +790,20 @@ const storeTraps = {
739
790
  const s = getOverlayLayer(e, t);
740
791
  const E = !!s;
741
792
  const S = !!e[STORE_VALUE][$TARGET];
742
- const f = s ?? e[STORE_VALUE];
793
+ const T = s ?? e[STORE_VALUE];
743
794
  if (!i) {
744
- const n = Object.getOwnPropertyDescriptor(f, t);
795
+ const n = Object.getOwnPropertyDescriptor(T, t);
745
796
  if (n && n.get) return n.get.call(r);
746
797
  if (!n && !E && e[STORE_CUSTOM_PROTO]) {
747
- const e = unwrapStoreValue(f);
798
+ const e = unwrapStoreValue(T);
748
799
  if (hasInheritedAccessor(e, t)) {
749
- return Reflect.get(f, t, r);
800
+ return Reflect.get(T, t, r);
750
801
  }
751
802
  }
752
803
  }
753
804
  if (writeOnly(r)) {
754
805
  if (isPrototypePollutionKey(t) && !hasOwnStoreProperty(e, t)) return undefined;
755
- let r = i && (E || !S) ? visibleNodeValue(i) : f[t];
806
+ let r = i && (E || !S) ? visibleNodeValue(i) : T[t];
756
807
  r === $DELETED && (r = undefined);
757
808
  if (!isWrappable(r)) return r;
758
809
  // Shallow boundary: records are replaced, never edited in place. Reads
@@ -764,14 +815,14 @@ const storeTraps = {
764
815
  Writing?.add(n);
765
816
  return n;
766
817
  }
767
- let R = i ? E || !S ? read(o[t]) : (read(o[t]), f[t]) : f[t];
768
- R === $DELETED && (R = undefined);
818
+ let c = i ? E || !S ? read(o[t]) : (read(o[t]), T[t]) : T[t];
819
+ c === $DELETED && (c = undefined);
769
820
  if (!i) {
770
- if (!E && typeof R === "function" && !Object.prototype.hasOwnProperty.call(f, t)) {
821
+ if (!E && typeof c === "function" && !Object.prototype.hasOwnProperty.call(T, t)) {
771
822
  let t;
772
- return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? R.bind(f) : R;
823
+ return !Array.isArray(e[STORE_VALUE]) && (t = Object.getPrototypeOf(e[STORE_VALUE])) && t !== Object.prototype ? c.bind(T) : c;
773
824
  } else if (getObserver() && !n) {
774
- return read(getNode(e, o, t, isWrappable(R) ? wrap(R, e) : R, isEqual, e[STORE_SNAPSHOT_PROPS]));
825
+ return read(getNode(e, o, t, isWrappable(c) ? wrap(c, e) : c, isEqual, e[STORE_SNAPSHOT_PROPS]));
775
826
  }
776
827
  }
777
828
  // Untracked fall-through (tracked reads already threw via their node in
@@ -783,8 +834,8 @@ const storeTraps = {
783
834
  // threw a fresh NotReadyError for an already-settled source, which no
784
835
  // sweep would ever release (#2938: projection over an async store wedged
785
836
  // its Loading boundary on `undefined`).
786
- if (!n && !getObserver()) throwIfUninitialized(e);
787
- return isWrappable(R) ? wrap(R, e) : R;
837
+ if (!n && !getObserver()) throwIfUnreadable(e);
838
+ return isWrappable(c) ? wrap(c, e) : c;
788
839
  },
789
840
  has(e, t) {
790
841
  if (t === $PROXY || t === $TRACK || t === "__proto__") return true;
@@ -803,7 +854,7 @@ const storeTraps = {
803
854
  if (getObserver()) {
804
855
  return read(getNode(e, o, t, n));
805
856
  }
806
- throwIfUninitialized(e);
857
+ throwIfUnreadable(e);
807
858
  return n;
808
859
  },
809
860
  set(e, t, r) {
@@ -818,45 +869,45 @@ const storeTraps = {
818
869
  // Shallow slots hold store proxies verbatim (pass-through reference,
819
870
  // never raw-marked — see markRawOne/#2932); everything else unwraps
820
871
  // and marks as usual.
821
- const f = !!e[STORE_SHALLOW] && r?.[$TARGET] !== undefined;
822
- const R = f ? r : unwrapStoreValue(r);
823
- if (e[STORE_SHALLOW] && !f && isWrappable(R)) {
872
+ const T = !!e[STORE_SHALLOW] && r?.[$TARGET] !== undefined;
873
+ const c = T ? r : unwrapStoreValue(r);
874
+ if (e[STORE_SHALLOW] && !T && isWrappable(c)) {
824
875
  // Flip the live gate too: a bare add was inert unless something else
825
876
  // had already marked a raw somewhere (wrap() checks rawValuesUsed
826
877
  // first), so the documented set-trap ingest mark silently no-oped in
827
878
  // apps whose only shallow data arrived through writes.
828
879
  rawValuesUsed = true;
829
- rawValues.add(R);
880
+ rawValues.add(c);
830
881
  }
831
882
  // Symbol-keyed writes on arrays are metadata, not index writes — never run
832
883
  // them through the numeric index/length machinery (`parseInt` on a symbol
833
884
  // throws). #2769
834
- const c = typeof t === "string" ? Number(t) : -1;
835
- const T = Array.isArray(O) && Number.isInteger(c) && c >= 0 && c < 4294967295 && String(c) === t;
836
- const u = T ? c + 1 : 0;
837
- const a = T && (getOverlayLayer(e, "length") ?? O).length;
838
- const l = T && u > a ? u : undefined;
839
- if (E === R && l === 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;
840
891
  armOptimisticStoreWrite(e, n);
841
- if (R !== undefined && R === o && l === undefined) {
892
+ if (c !== undefined && c === o && l === undefined) {
842
893
  delete e[i]?.[t];
843
894
  if (i === STORE_OPTIMISTIC_OVERRIDE) delete e[STORE_OPTIMISTIC_OWNERS]?.[t];
844
895
  } else {
845
896
  const r = e[i] || (e[i] = Object.create(null));
846
- r[t] = R;
897
+ r[t] = c;
847
898
  stampOptimisticOwner(e, i, t);
848
899
  if (l !== undefined) {
849
900
  r.length = l;
850
901
  stampOptimisticOwner(e, i, "length");
851
902
  }
852
903
  }
853
- notifyStoreProperty(e, t, "set", R, E, S);
904
+ notifyStoreProperty(e, t, "set", c, E, S);
854
905
  // Shrinking an array's length must remove the truncated indices, otherwise
855
906
  // they leak through `has`, `ownKeys`, and (tracked) index reads from the
856
907
  // underlying value. Mark each as deleted and notify so reactive reads update. #2768
857
- if (Array.isArray(O) && t === "length" && typeof R === "number" && typeof E === "number" && R < E) {
908
+ if (Array.isArray(O) && t === "length" && typeof c === "number" && typeof E === "number" && c < E) {
858
909
  const t = e[i] || (e[i] = Object.create(null));
859
- for (let r = R; r < E; r++) {
910
+ for (let r = c; r < E; r++) {
860
911
  if (t[r] === $DELETED) continue;
861
912
  const n = r in t ? t[r] : O[r];
862
913
  if (!(r in t) && !(r in O)) continue;
@@ -933,7 +984,7 @@ const storeTraps = {
933
984
  // path is exempt (like the get/has traps' writeOnly early returns):
934
985
  // the first landing's reconcile enumerates the store while
935
986
  // STATUS_UNINITIALIZED is still set — it IS the initialization.
936
- if (!getObserver() && !writeOnly(e[$PROXY])) throwIfUninitialized(e);
987
+ if (!getObserver() && !writeOnly(e[$PROXY])) throwIfUnreadable(e);
937
988
  }
938
989
  // Merge optimistic override with regular override for key enumeration
939
990
  let t = getKeys(e[STORE_VALUE], e[STORE_OVERRIDE], false);
@@ -4,6 +4,7 @@ export declare function addPendingSource(el: Computed<any>, source: Computed<any
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
6
  export declare function releaseSettledDependents(el: Computed<any>): void;
7
+ export declare function settleErroredDependents(el: Computed<any>, error: any): void;
7
8
  export declare function settlePendingSource(el: Computed<any>): void;
8
9
  export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>;
9
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
  }
@@ -4,6 +4,7 @@ export declare function addPendingSource(el: Computed<any>, source: Computed<any
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
6
  export declare function releaseSettledDependents(el: Computed<any>): void;
7
+ export declare function settleErroredDependents(el: Computed<any>, error: any): void;
7
8
  export declare function settlePendingSource(el: Computed<any>): void;
8
9
  export declare function isThenable<T>(value: T | PromiseLike<T>): value is PromiseLike<T>;
9
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.26",
3
+ "version": "2.0.0-beta.28",
4
4
  "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",