@warp-ts/reactivity 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,714 @@
1
+ // @warp-ts/reactivity · REQ-LFE-001-R · Signal / Computed / Effect / batch / owner(设计 01-R §2 ~ §5、§7)
2
+ import { CLEAN, CHECK, DIRTY, isComputation } from './types';
3
+ import { err, warn } from './error';
4
+ // ---- 全局上下文(模块级,单实例)----
5
+ let Listener = null;
6
+ let Owner_ = null;
7
+ let Updates = null;
8
+ let BatchDepth = 0;
9
+ let Pending = false;
10
+ let Flushing = false;
11
+ let Clock = 0;
12
+ let NextId = 0;
13
+ let Seq = 0;
14
+ /** FD-43:失去最后一个订阅者的 computed,flush 尾部退订上游(对齐 Vue 3.5:computed 不归 owner)*/
15
+ let Orphans = null;
16
+ /** FD-48:flush 中首个未处理错误,整轮跑完后再抛 */
17
+ let Failed = false, FailErr;
18
+ /** dev:顶层 root 登记,供依赖图快照 */
19
+ const allRoots = __DEV__ ? new Set() : null;
20
+ /** dev:flush 尾部回调(DevTools 探针的唯一插桩点,REQ-LFE-003 RB-301) */
21
+ const flushCbs = __DEV__ ? new Set() : null;
22
+ /**
23
+ * REQ-LFE-002 ADR-205 / RB-202 · 服务端模式:无 DOM 则无需订阅,也不能让订阅跨请求存活。
24
+ * 开启后 `effect / renderEffect` **同步求值一次即返回、不建订阅**;
25
+ * `signal / computed / owner` 语义不变(computed 仍惰性可缓存)。由 `@warp-ts/server` 在进程启动时开启。
26
+ *
27
+ * `batch / flushSync` **不加分支**(03 §1 要求「空转」,但那是结果不是手段):serverMode 下
28
+ * 没有任何非纯 computation 会进 `Updates`,两者本来就什么都不做,多写两个 `if` 只是白给
29
+ * 客户端产物字节(NFR-002)。
30
+ */
31
+ let ServerMode = false;
32
+ const NOOP = () => { };
33
+ function eqOf(o) {
34
+ if (!o || o.equals === undefined)
35
+ return Object.is;
36
+ if (o.equals === false)
37
+ return () => false;
38
+ return o.equals;
39
+ }
40
+ // ---- R-01 signal ----
41
+ export function signal(v, opts) {
42
+ const n = { _v: v, _eq: eqOf(opts), _subs: null, _subSlots: null };
43
+ if (__DEV__) {
44
+ n._name = opts?.name;
45
+ n._id = ++NextId;
46
+ }
47
+ const read = (() => readSignal(n));
48
+ read.set = x => writeSignal(n, typeof x === 'function' ? x(n._v) : x);
49
+ read.peek = () => n._v;
50
+ return read;
51
+ }
52
+ function readSignal(n) {
53
+ const l = Listener;
54
+ if (l) {
55
+ // R-M5:单源 / 单订阅者不建数组;第二个出现时升级为数组
56
+ let lSlot;
57
+ const ls = l._sources;
58
+ if (ls === null) {
59
+ l._sources = n;
60
+ lSlot = 0;
61
+ }
62
+ else if (!Array.isArray(ls)) {
63
+ l._sources = [ls, n];
64
+ l._sourceSlots = [l._sourceSlots, 0];
65
+ lSlot = 1;
66
+ }
67
+ else {
68
+ ls.push(n);
69
+ l._sourceSlots.push(0);
70
+ lSlot = ls.length - 1;
71
+ }
72
+ let sSlot;
73
+ const ns = n._subs;
74
+ if (ns === null || ns === undefined) {
75
+ n._subs = l;
76
+ n._subSlots = lSlot;
77
+ sSlot = 0;
78
+ }
79
+ else if (!Array.isArray(ns)) {
80
+ n._subs = [ns, l];
81
+ n._subSlots = [n._subSlots, lSlot];
82
+ sSlot = 1;
83
+ }
84
+ else {
85
+ ns.push(l);
86
+ n._subSlots.push(lSlot);
87
+ sSlot = ns.length - 1;
88
+ }
89
+ if (Array.isArray(l._sourceSlots))
90
+ l._sourceSlots[lSlot] = sSlot;
91
+ else
92
+ l._sourceSlots = sSlot;
93
+ }
94
+ return n._v;
95
+ }
96
+ function writeSignal(n, v) {
97
+ // E-2(REQ-LFE-008 M-00 / 设计 006-00 §2.5):R103「写 computed」整体收进 `__DEV__` —— prod 下
98
+ // 该分支与 `isComputation` 判定一起被 DCE(−28 B),写 computed 静默无效(设计 01-R:仅 dev 抛错)。
99
+ if (__DEV__ && isComputation(n))
100
+ throw err('WARP-R103', n._name);
101
+ if (__DEV__ && Listener && Listener._pure && Listener._running)
102
+ warn('WARP-R104', Listener._name);
103
+ if (n._eq(n._v, v))
104
+ return;
105
+ n._v = v;
106
+ ++Clock;
107
+ if (n._subs)
108
+ runUpdates(() => eachSub(n._subs, c => markDirty(c, DIRTY)));
109
+ }
110
+ /** 遍历订阅者(单值或数组);遍历期间不修改集合 */
111
+ function eachSub(subs, fn) {
112
+ if (!subs)
113
+ return;
114
+ if (Array.isArray(subs)) {
115
+ for (let i = 0; i < subs.length; i++)
116
+ fn(subs[i]);
117
+ }
118
+ else
119
+ fn(subs);
120
+ }
121
+ /** 非 computed 的 signal 节点被外部(reactive)触发 */
122
+ export function _notify(n) {
123
+ ++Clock;
124
+ if (n._subs)
125
+ runUpdates(() => eachSub(n._subs, c => markDirty(c, DIRTY)));
126
+ }
127
+ export const _track = readSignal;
128
+ export function _mkSignalNode() { return { _v: undefined, _eq: () => false, _subs: null, _subSlots: null }; }
129
+ export function _hasListener() { return Listener !== null; }
130
+ /** 内部:轻量信号(runtime-dom 行 item / index 用):只有节点 + 一个读闭包,写入走 _write,省去 set / peek 闭包(内存专项 M-1) */
131
+ export function _lightSignal(v) {
132
+ const node = { _v: v, _eq: Object.is, _subs: null, _subSlots: null };
133
+ if (__DEV__)
134
+ node._id = ++NextId;
135
+ return { read: () => readSignal(node), node };
136
+ }
137
+ export const _write = writeSignal;
138
+ // ---- R-02 / R-03 调度(push-pull)----
139
+ function markDirty(c, state) {
140
+ if (c._state >= state)
141
+ return;
142
+ const was = c._state;
143
+ c._state = state;
144
+ if (was === CLEAN && !c._pure)
145
+ Updates.push(c);
146
+ eachSub(c._subs, s => markDirty(s, CHECK));
147
+ }
148
+ function readComputed(c) {
149
+ if (c._running)
150
+ throw err('WARP-R102', __DEV__ ? `computed "${c._name || '(anonymous)'}" 在自身求值期间被读取` : '');
151
+ if (c._state !== CLEAN && !(c._seen === Clock && !Listener))
152
+ updateIfNecessary(c);
153
+ if (!Listener && !c._subs && c._sources)
154
+ orphan(c); // FD-43:非追踪读取且无订阅者 → flush 尾部退订上游
155
+ return readSignal(c);
156
+ }
157
+ function updateIfNecessary(c) {
158
+ if (c._disposed)
159
+ return;
160
+ if (c._state === CHECK) {
161
+ const src = c._sources;
162
+ const check = (s) => { if (isComputation(s)) {
163
+ updateIfNecessary(s);
164
+ if (s._updatedAt > c._updatedAt) {
165
+ c._state = DIRTY;
166
+ return true;
167
+ }
168
+ } return false; };
169
+ if (src) {
170
+ if (Array.isArray(src)) {
171
+ for (let i = 0; i < src.length; i++)
172
+ if (check(src[i]))
173
+ break;
174
+ }
175
+ else
176
+ check(src);
177
+ }
178
+ if (c._state === CHECK)
179
+ c._state = CLEAN;
180
+ }
181
+ if (c._state === DIRTY)
182
+ runComputation(c);
183
+ }
184
+ function runComputation(c) {
185
+ cleanNode(c);
186
+ c._state = CLEAN; // 执行前置 CLEAN:运行期间写入自身依赖 → 重新标脏并入队(Solid 语义,DC-23)
187
+ const pL = Listener, pO = Owner_, pR = RunningC;
188
+ Listener = c;
189
+ Owner_ = c._pure ? c._parent : c;
190
+ RunningC = c;
191
+ c._running = true;
192
+ try {
193
+ const nv = c._fn(ON_CLEANUP); // R-M6:共享注册函数,不再每次运行分配闭包
194
+ if (c._pure && (!c._updatedAt || !c._eq(c._v, nv))) {
195
+ c._v = nv;
196
+ c._updatedAt = ++Clock;
197
+ eachSub(c._subs, s => markDirty(s, DIRTY));
198
+ }
199
+ else if (!c._pure)
200
+ c._updatedAt = Clock;
201
+ }
202
+ catch (e) {
203
+ handleError(c, e);
204
+ }
205
+ finally {
206
+ c._running = false;
207
+ Listener = pL;
208
+ Owner_ = pO;
209
+ RunningC = pR;
210
+ }
211
+ }
212
+ let RunningC = null;
213
+ /**
214
+ * dev 诊断(prod 整体 DCE):`WARP-R101` 无 owner;
215
+ * `WARP-R105`(RDC-33 / 006 L-23)computed 内创建 effect / createRoot —— computed 必须是纯函数,
216
+ * 重算不会释放它们(行为不改,只出告警;设计 01-R §7 已登记为设计约束)
217
+ */
218
+ function devPure() {
219
+ if (RunningC && RunningC._pure && RunningC._running)
220
+ warn('WARP-R105', RunningC._name);
221
+ }
222
+ function devNewOwner(n) {
223
+ if (!Owner_)
224
+ warn('WARP-R101', n);
225
+ devPure();
226
+ }
227
+ const ON_CLEANUP = (f) => { const c = RunningC; if (c)
228
+ (c._cleanups || (c._cleanups = [])).push(f);
229
+ else if (__DEV__)
230
+ warn('WARP-R101', 'onCleanup(fn 参数) 仅在计算同步运行期内有效(MEM-02)'); };
231
+ function handleError(from, e) {
232
+ let o = from;
233
+ while (o) {
234
+ if (o._err && o._err(e) !== false)
235
+ return;
236
+ o = o._parent;
237
+ }
238
+ throw e;
239
+ }
240
+ function runUpdates(fn) {
241
+ if (Updates)
242
+ return fn();
243
+ Updates = [];
244
+ try {
245
+ return fn();
246
+ }
247
+ finally {
248
+ if (BatchDepth === 0)
249
+ scheduleFlush();
250
+ }
251
+ }
252
+ function scheduleFlush() {
253
+ if (Pending)
254
+ return;
255
+ Pending = true;
256
+ queueMicrotask(flush);
257
+ }
258
+ const bySeq = (a, b) => a._seq - b._seq;
259
+ /** 跑完当前队列(含本轮新入队):FD-38 每轮按创建序执行(守卫先于其内容);FD-48 单项抛错不中断 */
260
+ function drain() {
261
+ let q = Updates;
262
+ Updates = [];
263
+ let round = 0;
264
+ while (q && q.length) {
265
+ if (__DEV__ && ++round > 100)
266
+ throw err('WARP-R102', 'flush 超过 100 轮,疑似 effect 互写');
267
+ q.sort(bySeq);
268
+ for (let i = 0; i < q.length; i++) {
269
+ const c = q[i];
270
+ if (c._state !== CLEAN)
271
+ try {
272
+ updateIfNecessary(c);
273
+ }
274
+ catch (e) {
275
+ if (!Failed) {
276
+ Failed = true;
277
+ FailErr = e;
278
+ }
279
+ }
280
+ }
281
+ q = Updates;
282
+ Updates = [];
283
+ }
284
+ }
285
+ /** 仍无订阅者的孤儿 computed:退订全部上游(可被 GC),保留缓存值;_seen 记下当时时钟,期间无写入则读缓存 */
286
+ function orphan(c) {
287
+ (Orphans || (Orphans = [])).push(c);
288
+ if (!Flushing)
289
+ scheduleFlush();
290
+ }
291
+ function detachOrphans() {
292
+ while (Orphans) {
293
+ const list = Orphans;
294
+ Orphans = null;
295
+ for (let i = 0; i < list.length; i++) {
296
+ const c = list[i];
297
+ if (c._subs || c._running || !c._sources)
298
+ continue;
299
+ c._seen = c._state === CLEAN ? Clock : -1;
300
+ cleanNode(c);
301
+ c._state = DIRTY;
302
+ }
303
+ }
304
+ }
305
+ function flush() {
306
+ if (Flushing) {
307
+ drain();
308
+ return;
309
+ } // FD-36:flush 中再 flushSync 并入当前轮,不复位外层状态
310
+ Pending = false;
311
+ Flushing = true;
312
+ try {
313
+ drain();
314
+ }
315
+ finally {
316
+ Updates = null;
317
+ Flushing = false;
318
+ ++Clock;
319
+ detachOrphans();
320
+ if (__DEV__ && flushCbs.size)
321
+ runFlushCbs();
322
+ }
323
+ if (Failed) {
324
+ const e = FailErr;
325
+ Failed = false;
326
+ FailErr = undefined;
327
+ throw e;
328
+ }
329
+ }
330
+ /** dev:逐个调用 flush 回调;探针抛错不得影响应用(拷贝一份,允许回调内注销自己) */
331
+ function runFlushCbs() {
332
+ const list = Array.from(flushCbs);
333
+ for (let i = 0; i < list.length; i++) {
334
+ try {
335
+ list[i]();
336
+ }
337
+ catch (e) {
338
+ console.warn('[warp] __DEV_onFlush 回调抛错(已忽略):' + String(e));
339
+ }
340
+ }
341
+ }
342
+ // ---- R-04 batch ----
343
+ export function batch(fn) {
344
+ BatchDepth++;
345
+ try {
346
+ return runUpdates(fn);
347
+ }
348
+ finally {
349
+ if (--BatchDepth === 0 && Updates)
350
+ scheduleFlush();
351
+ }
352
+ }
353
+ /** 同步刷新待处理 effect(测试与 HMR 用) */
354
+ export function flushSync() { if (Updates || Pending)
355
+ flush(); }
356
+ export function untrack(fn) {
357
+ const pL = Listener;
358
+ Listener = null;
359
+ try {
360
+ return fn();
361
+ }
362
+ finally {
363
+ Listener = pL;
364
+ }
365
+ }
366
+ // ---- R-03 依赖清理 ----
367
+ function cleanNode(c) {
368
+ const src = c._sources;
369
+ if (src) {
370
+ if (Array.isArray(src)) {
371
+ const ss = c._sourceSlots;
372
+ for (let i = src.length - 1; i >= 0; i--)
373
+ unsubscribe(src[i], ss[i]);
374
+ }
375
+ else
376
+ unsubscribe(src, c._sourceSlots);
377
+ c._sources = null;
378
+ c._sourceSlots = null;
379
+ }
380
+ disposeChildren(c);
381
+ const cl = c._cleanups;
382
+ if (cl) {
383
+ c._cleanups = null;
384
+ for (let i = cl.length - 1; i >= 0; i--)
385
+ cl[i]();
386
+ }
387
+ }
388
+ /** owner 子节点:单值直接存,第二个起升级为数组(R-M9) */
389
+ function addChild(o, child) {
390
+ const ch = o._children;
391
+ if (!ch)
392
+ o._children = child;
393
+ else if (Array.isArray(ch))
394
+ ch.push(child);
395
+ else
396
+ o._children = [ch, child];
397
+ }
398
+ function disposeChildren(o) {
399
+ const ch = o._children;
400
+ if (!ch)
401
+ return;
402
+ o._children = null;
403
+ o._dead = 0; // MEM-11:整批释放后压缩计数归零
404
+ if (Array.isArray(ch)) {
405
+ for (let i = ch.length - 1; i >= 0; i--)
406
+ disposeNode(ch[i]);
407
+ }
408
+ else
409
+ disposeNode(ch);
410
+ }
411
+ /** 把 s._subs 中位于 slot 的订阅者移除(swap-remove),并维护被搬动者的反向索引;长度回到 1 时塌回单值 */
412
+ function unsubscribe(s, slot) {
413
+ const subs = s._subs;
414
+ if (!subs)
415
+ return;
416
+ if (!Array.isArray(subs)) {
417
+ s._subs = null;
418
+ s._subSlots = null;
419
+ if (s._pure)
420
+ orphan(s);
421
+ return;
422
+ }
423
+ const slots = s._subSlots;
424
+ const last = subs.pop(), lastSlot = slots.pop();
425
+ if (slot < subs.length) {
426
+ subs[slot] = last;
427
+ slots[slot] = lastSlot;
428
+ setSourceSlot(last, lastSlot, slot);
429
+ }
430
+ if (subs.length === 1) {
431
+ const only = subs[0], onlySlot = slots[0];
432
+ s._subs = only;
433
+ s._subSlots = onlySlot;
434
+ setSourceSlot(only, onlySlot, 0);
435
+ }
436
+ }
437
+ function setSourceSlot(c, idxInSources, value) {
438
+ if (Array.isArray(c._sourceSlots))
439
+ c._sourceSlots[idxInSources] = value;
440
+ else
441
+ c._sourceSlots = value;
442
+ }
443
+ // ---- R-06 owner 树 ----
444
+ function mkComputation(fn, pure, opts) {
445
+ // R-M4:effect 永远不被订阅,不分配 _v / _eq / _subs / _subSlots(对象少 4 槽)
446
+ const c = pure ? {
447
+ _v: undefined, _eq: eqOf(opts), _subs: null, _subSlots: null,
448
+ _fn: fn, _sources: null, _sourceSlots: null, _state: DIRTY, _pure: true, _running: false, _updatedAt: 0, _seq: ++Seq, _seen: -1,
449
+ _parent: Owner_, _children: null, _cleanups: null, _context: null, _disposed: false,
450
+ } : {
451
+ _fn: fn, _sources: null, _sourceSlots: null, _state: DIRTY, _pure: false, _running: false, _updatedAt: 0, _seq: ++Seq,
452
+ _parent: Owner_, _children: null, _cleanups: null, _context: null, _disposed: false,
453
+ };
454
+ if (__DEV__) {
455
+ c._name = opts?.name;
456
+ c._id = ++NextId;
457
+ }
458
+ if (Owner_ && !pure)
459
+ addChild(Owner_, c);
460
+ if (__DEV__ && pure && Owner_)
461
+ (Owner_._devC || (Owner_._devC = [])).push(c); // FD-43:computed 不入 owner 树,dev 依赖图仍按创建处枚举
462
+ return c;
463
+ }
464
+ export function effect(fn, opts) {
465
+ if (ServerMode) {
466
+ fn(onCleanup);
467
+ return NOOP;
468
+ } // 同步求值一次、不建订阅(清理仍归当前 owner)
469
+ const c = mkComputation(fn, false, opts);
470
+ if (__DEV__)
471
+ devNewOwner(opts?.name);
472
+ // 创建即跑;仅在 flush / batch 进行中排队以保持顺序(队列仅「待 flush」时仍立即执行)
473
+ if (Flushing || BatchDepth > 0)
474
+ Updates.push(c);
475
+ else
476
+ runComputation(c);
477
+ return () => disposeNode(c);
478
+ }
479
+ /** 渲染 effect:创建即同步执行,即使处于 flush / batch 中(runtime-dom `bind` 用;Solid createRenderEffect 语义,DC-23) */
480
+ export function renderEffect(fn, opts) {
481
+ if (ServerMode) {
482
+ fn(onCleanup);
483
+ return NOOP;
484
+ }
485
+ const c = mkComputation(fn, false, opts);
486
+ if (__DEV__)
487
+ devNewOwner(opts?.name);
488
+ runComputation(c);
489
+ return () => disposeNode(c);
490
+ }
491
+ export function computed(fn, opts) {
492
+ const c = mkComputation(fn, true, opts);
493
+ return () => readComputed(c);
494
+ }
495
+ export function createRoot(fn, owner = Owner_) {
496
+ if (__DEV__)
497
+ devPure();
498
+ const out = { root: null };
499
+ return _rootRun(() => fn(() => disposeNode(out.root)), owner, out);
500
+ }
501
+ /** 内部:在新 root 下运行 fn,返回 root 对象本身(不分配 dispose 闭包;runtime-dom 行用,R-M8) */
502
+ export function _rootRun(fn, owner, out) {
503
+ const parent = owner ?? null;
504
+ const root = { _parent: parent, _children: null, _cleanups: null, _context: null, _disposed: false };
505
+ if (__DEV__) {
506
+ root._id = ++NextId;
507
+ if (!parent)
508
+ allRoots.add(root);
509
+ }
510
+ if (parent)
511
+ addChild(parent, root);
512
+ out.root = root;
513
+ const pL = Listener, pO = Owner_;
514
+ Listener = null;
515
+ Owner_ = root;
516
+ try {
517
+ return fn();
518
+ }
519
+ finally {
520
+ Listener = pL;
521
+ Owner_ = pO;
522
+ }
523
+ }
524
+ export function _disposeOwner(o) { disposeNode(o); }
525
+ /** 内部(devtools / 测试):owner 子节点数组视图(单值形态时分配临时数组) */
526
+ export const _childrenOf = __DEV__ ? (o) => { const ch = o._children; return (!ch ? [] : Array.isArray(ch) ? ch : [ch]); } : undefined;
527
+ function disposeNode(n) {
528
+ if (n._disposed)
529
+ return;
530
+ n._disposed = true;
531
+ // 从父 _children 惰性摘除:计数超过半数时一次压缩(均摊 O(1),避免长寿命 owner 累积已释放子节点)
532
+ const p = n._parent;
533
+ if (p && !p._disposed && p._children) {
534
+ if (!Array.isArray(p._children)) {
535
+ if (p._children === n)
536
+ p._children = null;
537
+ }
538
+ else {
539
+ const dead = (p._dead = (p._dead || 0) + 1);
540
+ if (dead > 8 && dead * 2 > p._children.length) {
541
+ p._children = p._children.filter(c => !c._disposed);
542
+ p._dead = 0;
543
+ }
544
+ }
545
+ }
546
+ n._context = null; // RDC-26(006 L-35):两分支共用 —— effect 上 provide 过的 context 也要断开,否则整棵 map 随 owner 泄漏
547
+ if (isComputation(n)) {
548
+ cleanNode(n);
549
+ n._fn = null;
550
+ if (n._pure)
551
+ n._v = undefined;
552
+ n._state = CLEAN;
553
+ } // MEM-12:effect 无 _v 槽(R-M4),不追加属性触发隐藏类跃迁
554
+ else {
555
+ disposeChildren(n);
556
+ const cl = n._cleanups;
557
+ if (cl) {
558
+ n._cleanups = null;
559
+ for (let i = cl.length - 1; i >= 0; i--)
560
+ cl[i]();
561
+ }
562
+ if (__DEV__ && !n._parent)
563
+ allRoots.delete(n);
564
+ }
565
+ }
566
+ /**
567
+ * @internal 服务端模式开关(ADR-205)。`@warp-ts/server` 进程启动时 `enableServerMode(true)`;
568
+ * 测试用 `enableServerMode(false)` 还原。
569
+ */
570
+ export function enableServerMode(on) { ServerMode = on; }
571
+ export function onCleanup(f) {
572
+ if (Owner_)
573
+ (Owner_._cleanups || (Owner_._cleanups = [])).push(f);
574
+ else if (__DEV__)
575
+ warn('WARP-R101', 'onCleanup');
576
+ }
577
+ export function getOwner() { return Owner_; }
578
+ export function runWithOwner(owner, fn) {
579
+ const pL = Listener, pO = Owner_;
580
+ Listener = null;
581
+ Owner_ = owner;
582
+ try {
583
+ return fn();
584
+ }
585
+ finally {
586
+ Listener = pL;
587
+ Owner_ = pO;
588
+ }
589
+ }
590
+ /** 内部:owner 上挂错误处理器(core 用) */
591
+ export function _setOwnerError(owner, h) { owner._err = h; }
592
+ /**
593
+ * 内部:dev 下把组件元信息写到 owner(比照 `_setOwnerError`,REQ-LFE-003 RB-302)。
594
+ * 与 `__DEV_graph` 同形(prod 为 `undefined`)而不是空函数:prod 里连函数壳都不留(NFR-002 逐字节)。
595
+ */
596
+ export const _setOwnerDev = __DEV__ ? (owner, info) => {
597
+ const o = owner;
598
+ if (info.name !== undefined)
599
+ o._name = info.name;
600
+ if (info.hmrId !== undefined)
601
+ o._hmrId = info.hmrId;
602
+ if (info.node !== undefined)
603
+ o._node = info.node;
604
+ } : undefined;
605
+ /** dev:flush 尾部回调登记,返回注销函数;prod 为 `undefined`(与 `__DEV_graph` 同口径) */
606
+ export const __DEV_onFlush = __DEV__ ? (cb) => {
607
+ flushCbs.add(cb);
608
+ return () => { flushCbs.delete(cb); };
609
+ } : undefined;
610
+ export function _ownerContext(owner, create) {
611
+ const o = owner;
612
+ if (!o._context && create)
613
+ o._context = new Map();
614
+ return o._context;
615
+ }
616
+ // Owner 公开只读视图:直接在节点原型上暴露 getter 成本高;这里用 defineProperty 一次性给出的访问函数
617
+ export function ownerView(o) {
618
+ if (!o)
619
+ return null;
620
+ const n = o;
621
+ return { parent: n._parent, context: n._context, disposed: n._disposed };
622
+ }
623
+ export const __DEV_graph = __DEV__ ? () => {
624
+ const nodes = [], edges = [], seen = new Set();
625
+ const visitSig = (s) => {
626
+ if (s._id === undefined || seen.has(s._id))
627
+ return;
628
+ seen.add(s._id);
629
+ if (isComputation(s))
630
+ nodes.push({ id: s._id, kind: s._pure ? 'computed' : 'effect', name: s._name, state: s._state, value: s._v });
631
+ else
632
+ nodes.push({ id: s._id, kind: 'signal', name: s._name, value: s._v });
633
+ eachSub(s._subs, c => { visitSig(c); edges.push({ from: s._id, to: c._id, kind: 'dep' }); });
634
+ };
635
+ const visitOwner = (o) => {
636
+ if (o._disposed)
637
+ return;
638
+ if (isComputation(o)) {
639
+ visitSig(o);
640
+ const srcs = o._sources;
641
+ if (srcs)
642
+ for (const s of Array.isArray(srcs) ? srcs : [srcs]) {
643
+ if (s._id === undefined)
644
+ continue;
645
+ visitSig(s);
646
+ edges.push({ from: s._id, to: o._id, kind: 'dep' });
647
+ }
648
+ } // 匿名节点(selector per-key)不产脏边(B1-06)
649
+ else if (o._id !== undefined && !seen.has(o._id)) {
650
+ seen.add(o._id);
651
+ nodes.push({ id: o._id, kind: 'owner', name: o._name, hmrId: o._hmrId });
652
+ }
653
+ const kids = o._children;
654
+ if (kids)
655
+ for (const c of Array.isArray(kids) ? kids : [kids]) {
656
+ if (c._disposed)
657
+ continue;
658
+ visitOwner(c);
659
+ edges.push({ from: o._id, to: c._id, kind: 'own' });
660
+ } // 惰性摘除下已释放子节点不产悬空边(MEM-08 / VD-28)
661
+ const dc = o._devC;
662
+ if (dc)
663
+ for (const c of dc) {
664
+ if (c._disposed)
665
+ continue;
666
+ visitOwner(c);
667
+ edges.push({ from: o._id, to: c._id, kind: 'own' });
668
+ }
669
+ };
670
+ for (const r of allRoots)
671
+ visitOwner(r);
672
+ return { nodes, edges };
673
+ } : undefined;
674
+ // ---- createSelector(NFR-003 select row 优化)----
675
+ /** 返回 `(key) => boolean`:source 变化时只通知旧 key 与新 key 的订阅者,而非全部行(Solid 同名 API) */
676
+ export function createSelector(source, equals = ((a, b) => a === b)) {
677
+ const subs = new Map();
678
+ let pruneAt = 64;
679
+ let value = untrack(source); // RDC-21(006 L-35):首次求值不得把 source 记进**外层**监听者
680
+ effect(() => {
681
+ const v = source();
682
+ if (Object.is(v, value))
683
+ return;
684
+ const prev = value;
685
+ value = v;
686
+ let n;
687
+ subs.forEach((node, key) => { if (!node._subs) {
688
+ subs.delete(key);
689
+ return;
690
+ } if (equals(key, prev) || equals(key, v)) {
691
+ n = node;
692
+ if (n)
693
+ _notify(n);
694
+ } });
695
+ });
696
+ return (key) => {
697
+ if (Listener) {
698
+ let n = subs.get(key);
699
+ if (!n) {
700
+ // R-M7:不为每个 key 注册清理闭包;map 超过上次剪枝后两倍时扫一遍删除无订阅者的 key(均摊 O(1))
701
+ // 上界(MEM-07 接受):源不变且无新 key 时死条目最多保留至 max(64, 2 × 峰值),每条 ≈ 40 B;key 请用原始值(对象 key 会连带持有对象图,见 guide 02)
702
+ if (subs.size >= pruneAt) {
703
+ subs.forEach((node, k) => { if (!node._subs)
704
+ subs.delete(k); });
705
+ pruneAt = Math.max(64, subs.size * 2);
706
+ }
707
+ n = _mkSignalNode();
708
+ subs.set(key, n);
709
+ }
710
+ readSignal(n);
711
+ }
712
+ return equals(key, value);
713
+ };
714
+ }