@solidjs/signals 2.0.0-rc.2 → 2.0.0-rc.3

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 { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, CONFIG_HAS_LANE, OVERRIDE_UNDEFINED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js";
1
+ import { NOT_PENDING, unwrapOverride, STATUS_UNINITIALIZED, REACTIVE_DIRTY, REACTIVE_CHECK, CONFIG_HAS_LANE, OVERRIDE_UNDEFINED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, REACTIVE_OPTIMISTIC_DIRTY, EFFECT_RENDER, EFFECT_USER } from "./constants.js";
2
2
 
3
3
  import { ext, latestReadActive, currentOptimisticLane, stale } from "./core.js";
4
4
 
@@ -27,7 +27,14 @@ import { GlobalQueue, activeTransition, globalQueue, clock, insertSubs, schedule
27
27
  const t = e.o?.De !== NOT_PENDING;
28
28
  const i = t ? unwrapOverride(e.o?.De) : e.be;
29
29
  if (typeof n === "function") n = n(i);
30
- const u = !!(e.S & STATUS_UNINITIALIZED) || !e.Ue || !e.Ue(i, n);
30
+ const u = !!(e.S & STATUS_UNINITIALIZED) ||
31
+ // A dirty node's _value is stale (its queued recompute hasn't run — e.g.
32
+ // a latest() shadow marked by the previous landing's companion snap), so
33
+ // equality against it must not swallow the write. Without this, a sync
34
+ // push returning the shadow to that stale value was dropped, the snap
35
+ // recompute then committed the parent's old value, and the banner showed
36
+ // the previous transition's target (#3041 follow-up).
37
+ !!((e.ie ?? 0) & (REACTIVE_DIRTY | REACTIVE_CHECK)) || !e.Ue || !e.Ue(i, n);
31
38
  if (!u) {
32
39
  // Same-value write with an active override still entangles the current
33
40
  // action's transition — the hold must outlast all overlapping actions.
@@ -166,16 +173,31 @@ function cleanupCompletedLanes(e) {
166
173
  * read()'s value selection under a lane: return the committed `_value` for
167
174
  * optimistic/lane-assigned signals, stale-mode reads, and pending owners.
168
175
  */ function laneReadsCommitted(e, n, t) {
169
- if (e.o?.De !== undefined || !!e.o?.Be || !!(n.S & STATUS_PENDING)) return true;
176
+ if (e.o?.De !== undefined || !!e.o?.Be || !!(n.S & STATUS_PENDING)) {
177
+ // The committed view hides a staged in-flight value that will promote
178
+ // silently (commitPendingNode never re-notifies). gatedRead records plain
179
+ // signals for replay at commit; async memos are excluded from it by the
180
+ // `_fn` check and reach here instead — a lane-assigned source whose async
181
+ // already settled (laneAsyncSettled keeps _optimisticLane) served its
182
+ // committed value to a reader that never re-ran after the landing, so a
183
+ // pending-gated branch stayed one value behind permanently (#3041
184
+ // follow-up). Record the reader under the same replay contract.
185
+ if (e.Pe !== NOT_PENDING) (activeTransition ?? globalQueue.k).ln.add(t);
186
+ return true;
187
+ }
170
188
  if (n === e && stale && t.o?.It !== e) {
171
- // The committed view can hide a same-tick ambient write (a lane member —
172
- // even just an isPending companion flip — puts the reader "under a lane"
173
- // against unrelated plain writes). With no transaction the write commits
174
- // at THIS flush's end with no re-delivery, so record the reader for
175
- // replay at commit the same contract gatedRead provides when a
176
- // transaction is active (#2963). If a transaction forms mid-flush,
177
- // initTransition carries the recording over to it.
178
- if (e.Pe !== NOT_PENDING && activeTransition === null) globalQueue.k.ln.add(t);
189
+ // The committed view can hide a staged write (a lane member — even just
190
+ // an isPending companion flip — puts the reader "under a lane"). The
191
+ // staged value commits with no re-delivery (commitPendingNode never
192
+ // re-notifies), so record the reader for replay at commit — the same
193
+ // contract gatedRead provides (#2963). gatedRead itself only covers
194
+ // signal reads where the reading computed differs from the source; the
195
+ // owner === el memo/self read lands here instead. With a transaction
196
+ // active the staged value promotes silently at ITS landing, so record
197
+ // into the transaction (#3041 follow-up: a pending-gated branch that
198
+ // first read its async source during the landing flush stayed one value
199
+ // behind permanently); with none, into the ambient batch.
200
+ if (e.Pe !== NOT_PENDING) (activeTransition ?? globalQueue.k).ln.add(t);
179
201
  return true;
180
202
  }
181
203
  return false;
@@ -250,16 +272,16 @@ function trackOptimisticStore(e) {
250
272
  if (GlobalQueue.vt !== null) return;
251
273
  GlobalQueue.vt = optimisticWrite;
252
274
  GlobalQueue.fn = resolveOptimisticNodes;
253
- GlobalQueue.dn = transitionBlocked;
254
- GlobalQueue.En = cleanupCompletedLanes;
255
- GlobalQueue.Tn = runLaneEffects;
275
+ GlobalQueue.En = transitionBlocked;
276
+ GlobalQueue.Tn = cleanupCompletedLanes;
277
+ GlobalQueue.dn = runLaneEffects;
256
278
  GlobalQueue.Ft = gatedRead;
257
279
  GlobalQueue.ht = laneSuspends;
258
280
  GlobalQueue.gt = laneReadsCommitted;
259
281
  GlobalQueue.je = recomputeLane;
260
282
  GlobalQueue.$e = laneAsyncPending;
261
283
  GlobalQueue.ze = laneAsyncSettled;
262
- GlobalQueue.Nn = trackOptimisticStore;
284
+ GlobalQueue.In = trackOptimisticStore;
263
285
  }
264
286
 
265
287
  export { installOptimisticEngine };
@@ -329,16 +329,16 @@ class GlobalQueue extends Queue {
329
329
  // once the gate holds.
330
330
  static vt=null;
331
331
  static fn=null;
332
- static dn=null;
333
332
  static En=null;
334
333
  static Tn=null;
334
+ static dn=null;
335
335
  static Ft=null;
336
336
  static ht=null;
337
337
  static gt=null;
338
338
  static je=null;
339
339
  static $e=null;
340
340
  static ze=null;
341
- static Nn=null;
341
+ static In=null;
342
342
  flush() {
343
343
  if (this.sn) return;
344
344
  // Fast drain: nothing in flight but plain pending commits — no dirty
@@ -383,8 +383,8 @@ class GlobalQueue extends Queue {
383
383
  if (this.k === e) currentBatch = this.k = createBatch();
384
384
  // Run lane effects immediately (before stashing) - lanes with no pending async
385
385
  if (activeLanes.size) {
386
- GlobalQueue.Tn(EFFECT_RENDER);
387
- GlobalQueue.Tn(EFFECT_USER);
386
+ GlobalQueue.dn(EFFECT_RENDER);
387
+ GlobalQueue.dn(EFFECT_USER);
388
388
  }
389
389
  this.stashQueues(e.bt);
390
390
  clock++;
@@ -432,9 +432,9 @@ class GlobalQueue extends Queue {
432
432
  // Check if finalization added items to the heap (from optimistic reversion)
433
433
  scheduled = dirtyQueue.EE >= dirtyQueue.xe;
434
434
  // Run lane effects first (for ready lanes), then regular effects
435
- activeLanes.size && GlobalQueue.Tn(EFFECT_RENDER);
435
+ activeLanes.size && GlobalQueue.dn(EFFECT_RENDER);
436
436
  this.run(EFFECT_RENDER);
437
- activeLanes.size && GlobalQueue.Tn(EFFECT_USER);
437
+ activeLanes.size && GlobalQueue.dn(EFFECT_USER);
438
438
  this.run(EFFECT_USER);
439
439
  if (false) ;
440
440
  if (false && !scheduled && !activeTransition && transitions.size === 0 && activeLanes.size === 0) ;
@@ -677,7 +677,7 @@ function finalizePureQueue(e = null, t = false) {
677
677
  if (t.cn.size) GlobalQueue.Vt(t.cn, e);
678
678
  sweepTransientStoreNodes();
679
679
  // Lanes only enter activeLanes through the engine's getOrCreateLane.
680
- if (activeLanes.size) GlobalQueue.En(e);
680
+ if (activeLanes.size) GlobalQueue.Tn(e);
681
681
  }
682
682
  }
683
683
 
@@ -783,7 +783,7 @@ function transitionComplete(e) {
783
783
  // Override blockage lives with the engine (absent hook = "no optimistic
784
784
  // blockage"); the hook's loops over _optimisticNodes/_optimisticStores are
785
785
  // no-ops when the transition holds neither, so no pre-check is needed.
786
- if (t && GlobalQueue.dn?.(e)) t = false;
786
+ if (t && GlobalQueue.En?.(e)) t = false;
787
787
  t && (e.an = true);
788
788
  return t;
789
789
  }
@@ -273,6 +273,13 @@ function snapCompanionsToState(e) {
273
273
 
274
274
  function getLatestValueComputed(e) {
275
275
  let t = e.o?.ge;
276
+ // A shadow disposed while unobserved (its gated reader unmounted at a
277
+ // landing) is a corpse: sync writes into it equality-swallow against its
278
+ // frozen _value, and a later read revives it via recompute — clearing
279
+ // DISPOSED and re-deriving from the committed view, so the banner showed
280
+ // the previous transition's target (#3041 follow-up). Treat it as absent;
281
+ // recreation backfills from the in-flight write below.
282
+ if (t && t.ie & REACTIVE_DISPOSED) t = undefined;
276
283
  if (!t) {
277
284
  const n = latestReadActive;
278
285
  setLatestReadActive(false);
@@ -56,8 +56,8 @@ function installNextBlockedHalf() {
56
56
  e.clear();
57
57
  };
58
58
  }
59
- const e = GlobalQueue.dn;
60
- GlobalQueue.dn = t => {
59
+ const e = GlobalQueue.En;
60
+ GlobalQueue.En = t => {
61
61
  for (const e of t.cn) {
62
62
  const t = e?.[$TARGET];
63
63
  const n = t?.fam?.node;
@@ -198,7 +198,7 @@ function createOptimisticStoreNext(e, t, n) {
198
198
  // and the target for landing consumption (RUL-2).
199
199
  e.pb = null;
200
200
  (e.fam.overlaid ??= new Set).add(e);
201
- GlobalQueue.Nn?.(e.fam.px ?? e.px);
201
+ GlobalQueue.In?.(e.fam.px ?? e.px);
202
202
  }
203
203
 
204
204
  /**
@@ -33,81 +33,143 @@ import { wrapNext, storeSetterNext } from "./store.js";
33
33
  * loading windows, handleAsync landings, commit-through-setter) on next
34
34
  * primitives; the generic draft write-traps are reused from the legacy
35
35
  * module unchanged.
36
- */ function createWriteTraps(e, t) {
37
- // Save/restore, never hard-reset: the draft can be driven from inside an
38
- // enclosing authoritative-write scope (next-store optimistic derives), and
39
- // a hard `false` would clobber it mid-derive.
40
- const r = {
41
- get(e, t) {
42
- let o;
43
- const i = projectionWriteActive;
36
+ */
37
+ /**
38
+ * Wrap a store proxy as a projection DRAFT: every operation carries the write
39
+ * override (the derive is the author — its ops must not hit the §6c firewall
40
+ * gate, even in a continuation after an `await`/`yield` where the sync write
41
+ * scope has closed).
42
+ *
43
+ * FAKE TARGET, not the store proxy itself (#3060): after a proxy trap
44
+ * returns, the engine runs spec invariant validation against the proxy's
45
+ * TARGET — [[OwnPropertyKeys]] after ownKeys, [[GetOwnProperty]] after
46
+ * set/getOwnPropertyDescriptor/defineProperty. With the store proxy as
47
+ * target those checks re-enter the store's traps OUTSIDE the override
48
+ * bracket (the trap's finally has already run), so `Object.keys(state)` in
49
+ * a derive continuation fired the firewall gate and re-threw the
50
+ * projection's own pending NotReadyError into the derive. A dummy of
51
+ * matching kind (array/object, same trick as the store's own TargetShape)
52
+ * keeps invariant validation away from the store entirely; the traps
53
+ * forward to the closed-over inner proxy inside the bracket.
54
+ *
55
+ * Save/restore projectionWriteActive, never hard-reset: the draft can be
56
+ * driven from inside an enclosing authoritative-write scope (next-store
57
+ * optimistic derives), and a hard `false` would clobber it mid-derive.
58
+ */ function wrapDraft(e, t, r) {
59
+ const i = {
60
+ get(i, o) {
61
+ let n;
62
+ const c = projectionWriteActive;
44
63
  setWriteOverride(true);
45
64
  setProjectionWriteActive(true);
46
65
  try {
47
- o = e[t];
66
+ n = e[o];
48
67
  } finally {
49
68
  setWriteOverride(false);
50
- setProjectionWriteActive(i);
69
+ setProjectionWriteActive(c);
51
70
  }
52
- if (t === $TARGET) return o;
53
- return typeof o === "object" && o !== null ? new Proxy(o, r) : o;
71
+ if (o === $TARGET) return n;
72
+ return typeof n === "object" && n !== null ? wrapDraft(n, t, r) : n;
54
73
  },
55
- has(e, t) {
56
- let r;
74
+ has(t, r) {
75
+ let i;
57
76
  const o = projectionWriteActive;
58
77
  setWriteOverride(true);
59
78
  setProjectionWriteActive(true);
60
79
  try {
61
- r = t in e;
80
+ i = r in e;
62
81
  } finally {
63
82
  setWriteOverride(false);
64
83
  setProjectionWriteActive(o);
65
84
  }
66
- return r;
85
+ return i;
86
+ },
87
+ set(i, o, n) {
88
+ if (t && !t()) return true;
89
+ const c = projectionWriteActive;
90
+ setWriteOverride(true);
91
+ setProjectionWriteActive(true);
92
+ try {
93
+ e[o] = n;
94
+ r?.();
95
+ } finally {
96
+ setWriteOverride(false);
97
+ setProjectionWriteActive(c);
98
+ }
99
+ return true;
67
100
  },
68
- set(r, o, i) {
69
- if (e && !e()) return true;
101
+ deleteProperty(i, o) {
102
+ if (t && !t()) return true;
70
103
  const n = projectionWriteActive;
71
104
  setWriteOverride(true);
72
105
  setProjectionWriteActive(true);
73
106
  try {
74
- r[o] = i;
75
- t?.();
107
+ delete e[o];
108
+ r?.();
76
109
  } finally {
77
110
  setWriteOverride(false);
78
111
  setProjectionWriteActive(n);
79
112
  }
80
113
  return true;
81
114
  },
82
- deleteProperty(r, o) {
83
- if (e && !e()) return true;
84
- const i = projectionWriteActive;
115
+ ownKeys() {
116
+ const t = projectionWriteActive;
117
+ setWriteOverride(true);
118
+ setProjectionWriteActive(true);
119
+ try {
120
+ return Reflect.ownKeys(e);
121
+ } finally {
122
+ setWriteOverride(false);
123
+ setProjectionWriteActive(t);
124
+ }
125
+ },
126
+ getOwnPropertyDescriptor(t, r) {
127
+ let i;
128
+ const o = projectionWriteActive;
129
+ setWriteOverride(true);
130
+ setProjectionWriteActive(true);
131
+ try {
132
+ i = Reflect.getOwnPropertyDescriptor(e, r);
133
+ } finally {
134
+ setWriteOverride(false);
135
+ setProjectionWriteActive(o);
136
+ }
137
+ // The dummy target doesn't hold the key, so a non-configurable report
138
+ // would violate the proxy invariant. Store descriptors are already
139
+ // normalized configurable; enforce it for raw leaves too.
140
+ if (i) i.configurable = true;
141
+ return i;
142
+ },
143
+ defineProperty(i, o, n) {
144
+ if (t && !t()) return true;
145
+ const c = projectionWriteActive;
85
146
  setWriteOverride(true);
86
147
  setProjectionWriteActive(true);
87
148
  try {
88
- delete r[o];
89
- t?.();
149
+ Reflect.defineProperty(e, o, n);
150
+ r?.();
90
151
  } finally {
91
152
  setWriteOverride(false);
92
- setProjectionWriteActive(i);
153
+ setProjectionWriteActive(c);
93
154
  }
94
155
  return true;
95
156
  }
96
157
  };
97
- return r;
158
+ // Matching-kind dummy so Array.isArray(draft) answers like the store.
159
+ return new Proxy(Array.isArray(e) ? [] : {}, i);
98
160
  }
99
161
 
100
162
  function createProjectionNextInternal(e, t, r) {
101
- const o = {
163
+ const i = {
102
164
  map: new WeakMap,
103
165
  node: null,
104
166
  shallow: !!r?.shallow
105
167
  };
106
- const i = wrapNext(t, null, null, o);
107
- if (o.shallow) {
168
+ const o = wrapNext(t, null, null, i);
169
+ if (i.shallow) {
108
170
  // Shallow projection: the root is the only wrapped level — slot values
109
171
  // serve raw, ingests sticky raw-mark (same t.s machinery as plain).
110
- i[$TARGET].s = true;
172
+ o[$TARGET].s = true;
111
173
  markRawIngest(t);
112
174
  }
113
175
  let n;
@@ -115,13 +177,13 @@ function createProjectionNextInternal(e, t, r) {
115
177
  loadingValue: undefined
116
178
  };
117
179
  const c = computed(() => {
118
- if (!o.node) o.node = getOwner();
119
- runProjectionComputedNext(i, e, r?.key === undefined ? "id" : r.key);
180
+ if (!i.node) i.node = getOwner();
181
+ runProjectionComputedNext(o, e, r?.key === undefined ? "id" : r.key);
120
182
  }, n);
121
183
  c.T &= ~CONFIG_AUTO_DISPOSE;
122
- o.node = c;
184
+ i.node = c;
123
185
  return {
124
- store: i,
186
+ store: o,
125
187
  node: c
126
188
  };
127
189
  }
@@ -133,15 +195,15 @@ function createProjectionNext(e, t, r) {
133
195
  /** Derived writable store (legacy parity): a projection whose public setter
134
196
  * masks the recompute for the tick (core R31 — the manual write wins over a
135
197
  * same-flush dependency change). */ function createStoreDerivedNext(e, t, r) {
136
- const {store: o, node: i} = createProjectionNextInternal(e, t, r);
137
- return [ o, e => {
198
+ const {store: i, node: o} = createProjectionNextInternal(e, t, r);
199
+ return [ i, e => {
138
200
  // Mark the projection as manually written before notifying nodes.
139
- suppressComputedRecompute(i);
140
- storeSetterNext(o, e);
201
+ suppressComputedRecompute(o);
202
+ storeSetterNext(i, e);
141
203
  } ];
142
204
  }
143
205
 
144
- function runProjectionComputedNext(e, t, r, o, i) {
206
+ function runProjectionComputedNext(e, t, r, i, o) {
145
207
  const n = getOwner();
146
208
  let c = false;
147
209
  let s;
@@ -150,18 +212,18 @@ function runProjectionComputedNext(e, t, r, o, i) {
150
212
  // seed so draft writes cannot tear through to readers (#2988). Every commit
151
213
  // point reconciles the shadow through the normal commit path.
152
214
  const u = n.Ie ? JSON.parse(JSON.stringify(e[$TARGET][STORE_VALUE])) : null;
153
- const l = new Proxy(e, createWriteTraps(() => !c || n.o?.Ee === s, i));
154
- storeSetterNext(l, i => {
155
- s = t(u ?? i);
215
+ const l = wrapDraft(e, () => !c || n.o?.Ee === s, o);
216
+ storeSetterNext(l, o => {
217
+ s = t(u ?? o);
156
218
  c = true;
157
219
  const commit = t => {
158
220
  // Shadow run: commit a detached snapshot, never the shadow itself
159
221
  // (adoption takes the value by identity — handing it the live shadow
160
222
  // would fuse the draft to the observable store).
161
223
  if (u && (t === undefined || t === u)) t = JSON.parse(JSON.stringify(u));
162
- if (t === i || t === undefined) return;
224
+ if (t === o || t === undefined) return;
163
225
  const write = () => storeSetterNext(e, e => reconcileNextState(t, e, r, true), false);
164
- o ? o(write) : write();
226
+ i ? i(write) : write();
165
227
  };
166
228
  const l = handleAsync(n, s, commit);
167
229
  if (!n.Ie) commit(l);
@@ -2,7 +2,7 @@ import type { OptimisticLane } from "./lanes.js";
2
2
  import type { Computed, Signal } from "./types.js";
3
3
  /**
4
4
  * Test-mode invariant checks for the async/transition/lane machinery.
5
- * Catalog and rationale: packages/solid-signals/INTERNALS-ASYNC-STATE.md.
5
+ * Catalog and rationale: packages/signals/INTERNALS-ASYNC-STATE.md.
6
6
  *
7
7
  * These are implementation self-consistency checks, not semantic rules: a
8
8
  * violation means the reactive system contradicted itself.
@@ -2,7 +2,7 @@ import type { OptimisticLane } from "./lanes.cjs";
2
2
  import type { Computed, Signal } from "./types.cjs";
3
3
  /**
4
4
  * Test-mode invariant checks for the async/transition/lane machinery.
5
- * Catalog and rationale: packages/solid-signals/INTERNALS-ASYNC-STATE.md.
5
+ * Catalog and rationale: packages/signals/INTERNALS-ASYNC-STATE.md.
6
6
  *
7
7
  * These are implementation self-consistency checks, not semantic rules: a
8
8
  * violation means the reactive system contradicted itself.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/signals",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.3",
4
4
  "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.",
5
5
  "author": "Ryan Carniato",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/solidjs/solid.git",
11
- "directory": "packages/solid-signals"
11
+ "directory": "packages/signals"
12
12
  },
13
13
  "publishConfig": {
14
14
  "access": "public"
@@ -39,6 +39,18 @@
39
39
  },
40
40
  "./package.json": "./package.json"
41
41
  },
42
+ "scripts": {
43
+ "build": "npm-run-all -nl build:* && pnpm types",
44
+ "build:clean": "rimraf dist/dev dist/prod dist/node dist/dev.js dist/prod.js dist/node.cjs",
45
+ "build:js": "rollup -c && node ./scripts/mangle-props.mjs dist/prod dist/node.cjs && node ./scripts/check-pure.mjs dist/prod",
46
+ "types": "tsc -p tsconfig.build.json && node ../../scripts/sync-dual-types.mjs ./dist/types ./dist/types-cjs",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest watch tests",
49
+ "test:gc": "node --expose-gc ./vitest.js",
50
+ "test:gc:watch": "node --expose-gc ./vitest.js --watch",
51
+ "coverage": "vitest run --coverage",
52
+ "bench": "vitest bench --run"
53
+ },
42
54
  "devDependencies": {
43
55
  "@codspeed/vitest-plugin": "^5.4.0",
44
56
  "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
@@ -53,17 +65,5 @@
53
65
  "typescript": "^6.0.3",
54
66
  "vite": "^7.0.0",
55
67
  "vitest": "^4.1.6"
56
- },
57
- "scripts": {
58
- "build": "npm-run-all -nl build:* && pnpm types",
59
- "build:clean": "rimraf dist/dev dist/prod dist/node dist/dev.js dist/prod.js dist/node.cjs",
60
- "build:js": "rollup -c && node ./scripts/mangle-props.mjs dist/prod dist/node.cjs && node ./scripts/check-pure.mjs dist/prod",
61
- "types": "tsc -p tsconfig.build.json && node ../../scripts/sync-dual-types.mjs ./dist/types ./dist/types-cjs",
62
- "test": "vitest run",
63
- "test:watch": "vitest watch tests",
64
- "test:gc": "node --expose-gc ./vitest.js",
65
- "test:gc:watch": "node --expose-gc ./vitest.js --watch",
66
- "coverage": "vitest run --coverage",
67
- "bench": "vitest bench --run"
68
68
  }
69
- }
69
+ }
@@ -1,45 +0,0 @@
1
- import { type Refreshable } from "../core/index.js";
2
- import { type NoFn, type ProjectionOptions, type Store, type StoreSetter } from "./store.js";
3
- /**
4
- * The store equivalent of `createOptimistic`. Writes inside an `action`
5
- * transition are tentative — they show up immediately but auto-revert (or
6
- * reconcile to the action's resolved value) once the transition finishes.
7
- *
8
- * Use this for optimistic UI on collection-shaped data. For single-value
9
- * optimistic state, prefer `createOptimistic`.
10
- *
11
- * - Plain form: `createOptimisticStore(initialValue)`.
12
- * - Derived form: `createOptimisticStore(fn, seed, options?)` — a projection
13
- * store whose authoritative value is recomputed by `fn` and whose
14
- * optimistic overlay reverts after each transition.
15
- *
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
- * or `null` to merge positionally. Restating the default just adds noise.
19
- *
20
- * @example
21
- * ```ts
22
- * const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
23
- *
24
- * // Mutation: optimistic add, then in-place reconcile to the saved row.
25
- * const addTodo = action(function* (text: string) {
26
- * const tempId = crypto.randomUUID();
27
- * setTodos(t => { t.push({ id: tempId, text, pending: true }); });
28
- * const saved = yield api.createTodo(text);
29
- * setTodos(t => {
30
- * const i = t.findIndex(x => x.id === tempId);
31
- * if (i >= 0) t[i] = saved;
32
- * });
33
- * });
34
- *
35
- * // Return form: filter is the natural shape for removal.
36
- * const removeTodo = action(function* (id: string) {
37
- * setTodos(t => t.filter(x => x.id !== id));
38
- * yield api.removeTodo(id);
39
- * });
40
- * ```
41
- *
42
- * @returns `[store: Store<T>, setStore: StoreSetter<T>]`
43
- */
44
- export declare function createOptimisticStore<T extends object = {}>(store: NoFn<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Store<T>, set: StoreSetter<T>];
45
- export declare function createOptimisticStore<T extends object = {}>(fn: (store: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, store: Partial<T> | Store<NoFn<T>>, options?: ProjectionOptions): [get: Refreshable<Store<T>>, set: StoreSetter<T>];
@@ -1,70 +0,0 @@
1
- import { type Computed, type Refreshable } from "../core/index.js";
2
- import { type NoFn, type ProjectionOptions, type Store } from "./store.js";
3
- export declare function createProjectionInternal<T extends object = {}>(fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, seed: Partial<T>, options?: ProjectionOptions): {
4
- store: Refreshable<Store<T>>;
5
- node: Computed<void | T>;
6
- };
7
- /**
8
- * Creates a derived (projected) store. Like `createMemo` but for stores: the
9
- * derive function receives a mutable draft and either mutates it in place
10
- * (canonical) or returns a new value. Either way the result is reconciled
11
- * against the previous draft by `options.key` (default `"id"`), so surviving
12
- * items keep their proxy identity — only added/removed items are
13
- * created/disposed.
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
- *
19
- * Returns the projected store directly (no setter — reads only).
20
- *
21
- * Use this when you want the structural-sharing / per-property tracking
22
- * behaviour of a store on top of a derived computation. For simple read-only
23
- * derivations, `createMemo` is lighter.
24
- *
25
- * @param fn receives the current draft; mutate it in place or return new
26
- * data. Return is convenient for filter/derive shapes where mutation is
27
- * awkward.
28
- * @param seed the backing store value to wrap and reconcile into
29
- * @param options `ProjectionOptions` — `name`, `key`. `key` defaults to
30
- * `"id"`; specify it only when your data uses a different identity field
31
- * (e.g. `{ key: "uuid" }` or `{ key: u => u.slug }`), or `null` to merge
32
- * positionally with no keyed pass.
33
- *
34
- * @example
35
- * ```ts
36
- * // Mutation form — update individual fields on the draft.
37
- * const summary = createProjection<{ total: number; active: number }>(
38
- * draft => {
39
- * draft.total = users().length;
40
- * draft.active = users().filter(u => u.active).length;
41
- * },
42
- * { total: 0, active: 0 }
43
- * );
44
- *
45
- * // Return form — produce a derived collection. Reconciled by `id` so each
46
- * // surviving user keeps the same store identity across recomputes.
47
- * const activeUsers = createProjection<User[]>(
48
- * () => allUsers().filter(u => u.active),
49
- * []
50
- * );
51
- * ```
52
- *
53
- * @see {@link https://github.com/solidjs/x-reactivity#createprojection}
54
- */
55
- export declare function createProjection<T extends object = {}>(fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, seed: Partial<T> | Store<NoFn<T>>, options?: ProjectionOptions): Refreshable<Store<T>>;
56
- /**
57
- * Shared projection computed body used by both `createProjection` and the derived
58
- * form of `createOptimisticStore`. Encapsulates the write-trap draft, `storeSetter`
59
- * wrapping, the `handleAsync` subscription with a setter callback, and the commit
60
- * path (which must always go through `storeSetter` so the `writeOnly` guard is
61
- * engaged during `reconcile`'s property reads).
62
- *
63
- * `wrapCommit` is invoked for every commit (sync return and each async yield) and
64
- * lets callers layer extra context around the write — e.g. the optimistic store
65
- * re-enters `setProjectionWriteActive` so reconciles target `STORE_OVERRIDE`
66
- * instead of `STORE_OPTIMISTIC_OVERRIDE` even when an async yield fires outside
67
- * the outer `setProjectionWriteActive` scope.
68
- */
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>;
70
- export declare function createWriteTraps(isActive?: () => boolean, onDraftWrite?: () => void): ProxyHandler<any>;
@@ -1,46 +0,0 @@
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;
12
- /**
13
- * Returns a draft-mutating function that smart-merges `value` into a store,
14
- * preserving fine-grained reactivity: only changed leaves trigger updates.
15
- *
16
- * With a `key` (default `"id"`), array items whose key matches between old
17
- * and new states keep their identity (updated in place, moves and removals
18
- * update the corresponding signals) — the shape for keyed server payloads.
19
- * Items without the key field fall back to positional matching.
20
- *
21
- * With `key: null`, matching is purely positional: index N of the new array
22
- * merges into index N of the old, and object properties merge recursively —
23
- * the classic pattern for fixed-shape data that churns in place (dashboards,
24
- * monitors), where no keyed diff pass is needed or wanted.
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
- *
29
- * @param value the next state to merge in
30
- * @param key property name (string) or extractor function for stable
31
- * identity (default `"id"`); pass `null` for positional merging
32
- *
33
- * @example
34
- * ```ts
35
- * const [todos, setTodos] = createStore<Todo[]>([]);
36
- *
37
- * async function refresh() {
38
- * const fresh = await api.getTodos();
39
- * setTodos(reconcile(fresh)); // diff-merge by `id`
40
- * }
41
- *
42
- * // fixed-shape polling data — positional merge
43
- * setStats(reconcile(nextStats, null));
44
- * ```
45
- */
46
- export declare function reconcile<T extends U, U>(value: T, key?: string | ((item: NonNullable<any>) => any) | null): (state: U) => void;