Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/store-owner-stamp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

Store-owned backings now carry their owning target under an internal enumerable symbol stamp instead of registering in two weak collections (ownership set + raw→target map) on every draft — the identity-hash/ephemeron cost of those registrations was the remaining floor of a one-key store write (#3360, part two). Steady-state single-key writes drop from ~340 ns to ~180 ns; reconcile and projection benches improve 10–80%. The stamp is invisible through the proxy (`ownKeys`, `in`, descriptors, spreads), never appears in `snapshot()` output, never acquires a node, and is skipped by every key walk (membership/deep-witness diffs, reconcile, optimistic staging, affects scopes).
17 changes: 9 additions & 8 deletions packages/signals/src/store/next/optimistic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import {
// Cycle with reconcile.js is benign: the binding resolves at call time (the
// optimistic write), long after both modules initialize.
import { sameKey } from "./reconcile.js";
import { setOptHooks, storeNextLookup } from "./target.js";
import { $OWNER, lookupTarget, setOptHooks } from "./target.js";
type KeyFn = (item: any) => any;
import { isRawValue, isWrappable, rawValuesUsed, setNextOptimisticViewResolver } from "../store.js";
import type { StoreNextFamily, StoreNextTarget } from "./target.js";
Expand Down Expand Up @@ -501,7 +501,7 @@ function stagedApply(cur: any, incoming: any, keyFn: KeyFn | null): void {
// Object merge; also the degenerate root-kind-change shape (arrays accept
// keyed writes/deletes, so a wholesale restatement still lands staged).
for (const k of Reflect.ownKeys(incoming)) {
if (curArr && k === "length") continue;
if ((curArr && k === "length") || k === $OWNER) continue;
const nv = (incoming as any)[k];
const pv = unwrapValue(cur[k]);
if (pv === nv) continue;
Expand All @@ -522,7 +522,7 @@ function stagedApply(cur: any, incoming: any, keyFn: KeyFn | null): void {
}
}
for (const k of Reflect.ownKeys(cur)) {
if ((curArr && k === "length") || k in incoming) continue;
if ((curArr && k === "length") || k === $OWNER || k in incoming) continue;
delete cur[k];
}
}
Expand Down Expand Up @@ -571,7 +571,7 @@ export function notifyOptimisticWrites(t: StoreNextTarget, pb: Record<PropertyKe
let structural = false;
const isArr = Array.isArray(pb);
for (const key of Reflect.ownKeys(pb)) {
if (isArr && key === "length") continue;
if ((isArr && key === "length") || key === $OWNER) continue;
const nv = unwrapValue(pb[key as any]);
if (!visiblePresent(key)) {
// Optimistic add: value node + presence node + membership bump.
Expand All @@ -587,7 +587,7 @@ export function notifyOptimisticWrites(t: StoreNextTarget, pb: Record<PropertyKe
}
}
for (const key of Reflect.ownKeys(old)) {
if (isArr && key === "length") continue;
if ((isArr && key === "length") || key === $OWNER) continue;
if (key in pb || !visiblePresent(key)) continue;
// Optimistic delete: node reads undefined, presence flips, membership bumps.
setSignal(getNode(t, key, old[key as any]), () => undefined);
Expand Down Expand Up @@ -654,15 +654,15 @@ export function optimisticView(
function applyTentative(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null): void {
const base = t.pb ?? t.v;
const view = optimisticView(t, base);
const map = t.fam!.map;
const fam = t.fam!;
const isArr = Array.isArray(incoming);
if (Array.isArray(view) !== isArr) return; // kind change at root: flat overrides below
const pairs: Array<[StoreNextTarget, any]> = [];
let pbLike: any;
if (isArr) pbLike = [...(incoming as any[])];
else {
pbLike = {};
for (const k of Reflect.ownKeys(incoming)) pbLike[k] = (incoming as any)[k];
for (const k of Reflect.ownKeys(incoming)) if (k !== $OWNER) pbLike[k] = (incoming as any)[k];
}
const match = (pv: any, nv: any): StoreNextTarget | null => {
if (!isWrappable(pv) || !isWrappable(nv)) return null;
Expand All @@ -675,7 +675,7 @@ function applyTentative(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null):
// channel — NaN keys are self-equal.
if (pk !== undefined && nk !== undefined && !sameKey(pk, nk)) return null;
}
return map.get(unwrapValue(pv)) ?? null;
return lookupTarget(unwrapValue(pv), fam) ?? null;
};
if (isArr) {
const viewRows = view as any[];
Expand Down Expand Up @@ -724,6 +724,7 @@ function applyTentative(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null):
}
} else {
for (const k of Reflect.ownKeys(incoming)) {
if (k === $OWNER) continue;
const pv = unwrapValue((view as any)[k]);
const nv = (incoming as any)[k];
const ct = match(pv, nv);
Expand Down
18 changes: 12 additions & 6 deletions packages/signals/src/store/next/reconcile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import {
unwrapValue
} from "./store.js";
import {
ownedRaw,
$OWNER,
isOwned,
lookupTarget,
storeNextLookup,
type StoreNextFamily,
type StoreNextTarget,
Expand Down Expand Up @@ -96,7 +98,10 @@ export function reconcileNextState(
// across an entity change even when their own keys align (proj R7).
// Displaced-raw unregistration (proj R10): the outgoing raw stops
// resolving to this proxy; re-handed later it wraps fresh.
(t.fam?.map ?? storeNextLookup).delete(t.pb ?? t.v);
const out = t.pb ?? t.v;
if (isOwned(out))
delete (out as any)[$OWNER]; // disown: wraps fresh if re-handed
else (t.fam?.map ?? storeNextLookup).delete(out);
adoptPB(t, incoming);
return;
}
Expand All @@ -116,7 +121,7 @@ export function reconcileNextState(
function applyAdopt(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null, proj = false): void {
const prev = t.pb ?? t.v;
// The sound identity skip (O7): same reference AND we never diverged it.
if (incoming === prev && !ownedRaw.has(prev)) return;
if (incoming === prev && !isOwned(prev)) return;
const fam = t.fam;
// §6b (R28): the diff's previous-arrangement baseline is the LANE VIEW —
// optimistic rows must be visible to key matching so a landing carrying the
Expand Down Expand Up @@ -181,7 +186,7 @@ function applyAdopt(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null, proj
break; // misaligned: fall to the keyed remainder below
// Identity skip inline (FINDING-1 guard), then descend the pair.
if (
(pvRaw !== nv || (nv !== null && typeof nv === "object" && ownedRaw.has(nv))) &&
(pvRaw !== nv || (nv !== null && typeof nv === "object" && isOwned(nv))) &&
nv !== null &&
typeof nv === "object"
)
Expand Down Expand Up @@ -319,7 +324,7 @@ function applyAdopt(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null, proj
const isObj = nv !== null && typeof nv === "object";
if (
ov === nv &&
(!isObj || !ownedRaw.has(nv)) &&
(!isObj || !isOwned(nv)) &&
(nodes === null || nodes[k] === undefined || !hasAccessorFlag(nodes[k]))
) {
if (nodes !== null && nodes[k] !== undefined) nodesHit++;
Expand All @@ -345,6 +350,7 @@ function applyAdopt(t: StoreNextTarget, incoming: any, keyFn: KeyFn | null, proj
const syms = Object.getOwnPropertySymbols(incoming);
for (let i = 0; i < syms.length; i++) {
const k = syms[i];
if (k === $OWNER) continue;
const nv = (incoming as any)[k];
if (!shallow && nv !== null && typeof nv === "object")
descend(unwrapValue((prevView as any)[k]), nv, keyFn, fam, proj);
Expand Down Expand Up @@ -396,7 +402,7 @@ function descend(
// wrappables acquire targets; rawValues never wrap) — one WeakMap get
// replaces isWrappable(pv) + isRawValue(pv), and a miss prunes untracked
// subtrees before any further checks.
const ct = (fam?.map ?? storeNextLookup).get(pv);
const ct = lookupTarget(pv, fam);
if (ct === undefined) return; // nothing proxied below this pair
// The NEW side still validates fully: a frozen/platform/markRaw'd incoming
// value is a leaf for reconcile — replaced by reference, never recursed
Expand Down
Loading
Loading