react-fine-grained-signals 0.1.1 → 0.2.0
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.
- package/LICENSE-ALIEN-SIGNALS.txt +21 -0
- package/README.ja.md +17 -10
- package/README.md +18 -10
- package/dist/base-WD_74mfC.js +55 -0
- package/dist/base-WD_74mfC.js.map +1 -0
- package/dist/core-runtime-2jSE9Tz5.js +1354 -0
- package/dist/core-runtime-2jSE9Tz5.js.map +1 -0
- package/dist/index.d.ts +20 -25
- package/dist/index.js +4 -4
- package/dist/{jsx-Xp5q2y97.d.ts → jsx-BQGrTir3.d.ts} +40 -5
- package/dist/jsx-P6volZdr.js +1833 -0
- package/dist/jsx-P6volZdr.js.map +1 -0
- package/dist/jsx-dev-runtime.d.ts +3 -3
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +4 -4
- package/dist/jsx-runtime.js +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +2 -2
- package/dist/use-signals-C1K8-vJX.js +169 -0
- package/dist/use-signals-C1K8-vJX.js.map +1 -0
- package/dist/use-signals-CJYcdFdL.d.ts +11 -0
- package/dist/utils.d.ts +14 -15
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +22 -18
- package/dist/base-BZTnpU8U.js +0 -286
- package/dist/base-BZTnpU8U.js.map +0 -1
- package/dist/hooks-BFIWh7AD.js +0 -914
- package/dist/hooks-BFIWh7AD.js.map +0 -1
- package/dist/jsx-D9U6dSwL.js +0 -730
- package/dist/jsx-D9U6dSwL.js.map +0 -1
- package/dist/render-tracking-DCzw57BW.js +0 -96
- package/dist/render-tracking-DCzw57BW.js.map +0 -1
- package/dist/use-signals-8i2B-94o.js +0 -173
- package/dist/use-signals-8i2B-94o.js.map +0 -1
- package/dist/use-signals-CBDbbAbV.d.ts +0 -24
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
import { a as untrackedRender, c as getSharedInteropContext, d as UNTRACKED_OWNER, f as executionContext, i as notifyListener, l as isInteropSpeculative, n as SIGNAL_BRAND, o as attachReadableInterop, r as hasActiveRenderCollector, s as getReadableInterop, t as coreRuntime, u as markInteropSpeculativeDeepRead } from "./core-runtime-2jSE9Tz5.js";
|
|
2
|
+
import { a as isSignal, c as untracked, n as computed, o as registerSignal, r as detachedEffect, s as signal, t as batch } from "./base-WD_74mfC.js";
|
|
3
|
+
import { Fragment, createElement, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
//#region src/core/deep-signal-engine.ts
|
|
5
|
+
function isObjectLike(value) {
|
|
6
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
7
|
+
}
|
|
8
|
+
function assertExtensible(value) {
|
|
9
|
+
if (!Object.isExtensible(value)) throw new TypeError("deepSignal() cannot proxy a non-extensible object or array");
|
|
10
|
+
}
|
|
11
|
+
function assertDataProperties(value) {
|
|
12
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
13
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
14
|
+
if (descriptor !== void 0 && !("value" in descriptor)) throw new TypeError("deepSignal() does not support accessor properties");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function emptyArrayOfLength(length) {
|
|
18
|
+
const result = [];
|
|
19
|
+
result.length = length;
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
function isArrayIndex(key) {
|
|
23
|
+
if (typeof key !== "string" || key === "") return false;
|
|
24
|
+
const index = Number(key);
|
|
25
|
+
return Number.isInteger(index) && index >= 0 && index < 4294967295 && String(index) === key;
|
|
26
|
+
}
|
|
27
|
+
const notify = (versions, key) => {
|
|
28
|
+
const version = versions.get(key);
|
|
29
|
+
if (version !== void 0) version.value += 1;
|
|
30
|
+
};
|
|
31
|
+
const markPrunable = (metadata, key) => {
|
|
32
|
+
if (!metadata.properties.has(key) && !metadata.existence.has(key)) return;
|
|
33
|
+
(metadata.prunable ??= /* @__PURE__ */ new Set()).add(key);
|
|
34
|
+
};
|
|
35
|
+
const notifyIteration = (metadata) => {
|
|
36
|
+
if (metadata.iteration !== void 0) metadata.iteration.value += 1;
|
|
37
|
+
};
|
|
38
|
+
function createDeepSignalFactory(adapter) {
|
|
39
|
+
const ARRAY_MUTATORS = /* @__PURE__ */ new Set([
|
|
40
|
+
"copyWithin",
|
|
41
|
+
"fill",
|
|
42
|
+
"pop",
|
|
43
|
+
"push",
|
|
44
|
+
"reverse",
|
|
45
|
+
"shift",
|
|
46
|
+
"sort",
|
|
47
|
+
"splice",
|
|
48
|
+
"unshift"
|
|
49
|
+
]);
|
|
50
|
+
const ARRAY_SEARCHES = /* @__PURE__ */ new Set([
|
|
51
|
+
"includes",
|
|
52
|
+
"indexOf",
|
|
53
|
+
"lastIndexOf"
|
|
54
|
+
]);
|
|
55
|
+
const proxyToRaw = /* @__PURE__ */ new WeakMap();
|
|
56
|
+
const rawToMetadata = /* @__PURE__ */ new WeakMap();
|
|
57
|
+
const readonlyMapViews = /* @__PURE__ */ new WeakMap();
|
|
58
|
+
const readonlySetViews = /* @__PURE__ */ new WeakMap();
|
|
59
|
+
const readonlyCollectionViewToRaw = /* @__PURE__ */ new WeakMap();
|
|
60
|
+
const normalizedProxyCarriers = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
const isSpeculative = adapter.isSpeculative ?? isInteropSpeculative;
|
|
62
|
+
const markSpeculativeDeepRead = adapter.markSpeculativeDeepRead ?? markInteropSpeculativeDeepRead;
|
|
63
|
+
function shouldTrackDeepRead() {
|
|
64
|
+
if (isSpeculative()) {
|
|
65
|
+
markSpeculativeDeepRead();
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const shared = getSharedInteropContext();
|
|
69
|
+
return adapter.hasActiveSubscriber() || hasActiveRenderCollector() || shared.graphCollector !== void 0 || shared.renderCollector !== void 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The realm's built-in prototype objects, rejected deliberately rather than by
|
|
73
|
+
* accident. Several of them look exactly like plain data to the rest of this
|
|
74
|
+
* module: `Array.prototype` is array-exotic (so `Array.isArray` short-circuits
|
|
75
|
+
* the prototype check below), while `Error.prototype`, `Date.prototype`,
|
|
76
|
+
* `Promise.prototype`, `String.prototype`, ... all inherit straight from
|
|
77
|
+
* `Object.prototype` and expose only extensible, writable data properties, so
|
|
78
|
+
* neither `assertExtensible` nor `assertDataProperties` stops them. Adopting
|
|
79
|
+
* one as deep state would let the `set` trap `Reflect.set` onto the real,
|
|
80
|
+
* globally shared intrinsic. Some of them (`Object.prototype.__proto__`,
|
|
81
|
+
* `Map.prototype.size`, ...) happen to be caught today by the accessor check,
|
|
82
|
+
* but that protection is incidental and asymmetric; this set makes it uniform.
|
|
83
|
+
*
|
|
84
|
+
* Built from `globalThis` so an engine missing a builtin simply contributes
|
|
85
|
+
* nothing. Only this realm's intrinsics are listed: an object from another
|
|
86
|
+
* realm already fails the `Object.prototype` identity check below (the one
|
|
87
|
+
* exception being a cross-realm array, which stays accepted as it is today).
|
|
88
|
+
*/
|
|
89
|
+
const INTRINSIC_PROTOTYPES = (() => {
|
|
90
|
+
const prototypes = /* @__PURE__ */ new Set();
|
|
91
|
+
for (const name of [
|
|
92
|
+
"Object",
|
|
93
|
+
"Array",
|
|
94
|
+
"Function",
|
|
95
|
+
"Boolean",
|
|
96
|
+
"Number",
|
|
97
|
+
"String",
|
|
98
|
+
"Symbol",
|
|
99
|
+
"BigInt",
|
|
100
|
+
"Date",
|
|
101
|
+
"RegExp",
|
|
102
|
+
"Error",
|
|
103
|
+
"AggregateError",
|
|
104
|
+
"EvalError",
|
|
105
|
+
"RangeError",
|
|
106
|
+
"ReferenceError",
|
|
107
|
+
"SyntaxError",
|
|
108
|
+
"TypeError",
|
|
109
|
+
"URIError",
|
|
110
|
+
"Map",
|
|
111
|
+
"Set",
|
|
112
|
+
"WeakMap",
|
|
113
|
+
"WeakSet",
|
|
114
|
+
"WeakRef",
|
|
115
|
+
"FinalizationRegistry",
|
|
116
|
+
"Promise",
|
|
117
|
+
"ArrayBuffer",
|
|
118
|
+
"SharedArrayBuffer",
|
|
119
|
+
"DataView"
|
|
120
|
+
]) {
|
|
121
|
+
const intrinsic = globalThis[name];
|
|
122
|
+
if (typeof intrinsic !== "function") continue;
|
|
123
|
+
const prototype = intrinsic.prototype;
|
|
124
|
+
if (typeof prototype === "object" && prototype !== null || typeof prototype === "function") prototypes.add(prototype);
|
|
125
|
+
}
|
|
126
|
+
return prototypes;
|
|
127
|
+
})();
|
|
128
|
+
const isIntrinsicPrototype = (value) => isObjectLike(value) && INTRINSIC_PROTOTYPES.has(value);
|
|
129
|
+
function isPlainObjectOrArray(value) {
|
|
130
|
+
if (typeof value !== "object" || value === null || adapter.isSignal(value)) return false;
|
|
131
|
+
if (INTRINSIC_PROTOTYPES.has(value)) return false;
|
|
132
|
+
if (Array.isArray(value)) return true;
|
|
133
|
+
const prototype = Object.getPrototypeOf(value);
|
|
134
|
+
return prototype === Object.prototype || prototype === null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolves a value that may be one of our proxies or one of our readonly
|
|
138
|
+
* collection views back to the raw object it wraps. Returns `undefined` when
|
|
139
|
+
* `value` is not object-like or is not a value this module has wrapped, so
|
|
140
|
+
* callers can fall back to using it as-is.
|
|
141
|
+
*/
|
|
142
|
+
function toRaw(value) {
|
|
143
|
+
if (!isObjectLike(value)) return void 0;
|
|
144
|
+
const directRaw = proxyToRaw.get(value);
|
|
145
|
+
if (directRaw !== void 0) return directRaw;
|
|
146
|
+
return readonlyCollectionViewToRaw.get(value);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Shared scaffolding for `readonlyMapView`/`readonlySetView`: cache lookup,
|
|
150
|
+
* the "reject this mutation" factory, defining the descriptors, the
|
|
151
|
+
* feature-detected extras loop, and the dual-WeakMap registration. Only the
|
|
152
|
+
* per-type descriptor bodies (which truly differ between Map and Set) and
|
|
153
|
+
* the extras list are supplied by the caller.
|
|
154
|
+
*/
|
|
155
|
+
function createReadonlyCollectionView(raw, cache, prototype, kind, buildCore, buildExtras) {
|
|
156
|
+
const cached = cache.get(raw);
|
|
157
|
+
if (cached !== void 0) return cached;
|
|
158
|
+
const view = Object.create(prototype);
|
|
159
|
+
const rejectMutation = (operation) => () => {
|
|
160
|
+
throw new TypeError(`deepSignal() ${kind}#${operation}() is not allowed through .value; replace the ${kind} immutably`);
|
|
161
|
+
};
|
|
162
|
+
Object.defineProperties(view, buildCore(raw, view, rejectMutation));
|
|
163
|
+
for (const [name, impl] of buildExtras(raw, rejectMutation)) Object.defineProperty(view, name, {
|
|
164
|
+
enumerable: false,
|
|
165
|
+
configurable: false,
|
|
166
|
+
value: impl
|
|
167
|
+
});
|
|
168
|
+
cache.set(raw, view);
|
|
169
|
+
readonlyCollectionViewToRaw.set(view, raw);
|
|
170
|
+
return view;
|
|
171
|
+
}
|
|
172
|
+
function readonlyMapView(raw) {
|
|
173
|
+
return createReadonlyCollectionView(raw, readonlyMapViews, Map.prototype, "Map", (target, view, rejectMutation) => ({
|
|
174
|
+
size: {
|
|
175
|
+
enumerable: false,
|
|
176
|
+
configurable: false,
|
|
177
|
+
get: () => target.size
|
|
178
|
+
},
|
|
179
|
+
get: {
|
|
180
|
+
enumerable: false,
|
|
181
|
+
configurable: false,
|
|
182
|
+
value: (key) => target.get(key)
|
|
183
|
+
},
|
|
184
|
+
has: {
|
|
185
|
+
enumerable: false,
|
|
186
|
+
configurable: false,
|
|
187
|
+
value: (key) => target.has(key)
|
|
188
|
+
},
|
|
189
|
+
entries: {
|
|
190
|
+
enumerable: false,
|
|
191
|
+
configurable: false,
|
|
192
|
+
value: () => Map.prototype.entries.call(target)
|
|
193
|
+
},
|
|
194
|
+
keys: {
|
|
195
|
+
enumerable: false,
|
|
196
|
+
configurable: false,
|
|
197
|
+
value: () => Map.prototype.keys.call(target)
|
|
198
|
+
},
|
|
199
|
+
values: {
|
|
200
|
+
enumerable: false,
|
|
201
|
+
configurable: false,
|
|
202
|
+
value: () => Map.prototype.values.call(target)
|
|
203
|
+
},
|
|
204
|
+
forEach: {
|
|
205
|
+
enumerable: false,
|
|
206
|
+
configurable: false,
|
|
207
|
+
value: (callback, thisArg) => {
|
|
208
|
+
Map.prototype.forEach.call(target, (value, key) => callback.call(thisArg, value, key, view));
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
[Symbol.iterator]: {
|
|
212
|
+
enumerable: false,
|
|
213
|
+
configurable: false,
|
|
214
|
+
value: () => Map.prototype.entries.call(target)
|
|
215
|
+
},
|
|
216
|
+
set: {
|
|
217
|
+
enumerable: false,
|
|
218
|
+
configurable: false,
|
|
219
|
+
value: rejectMutation("set")
|
|
220
|
+
},
|
|
221
|
+
delete: {
|
|
222
|
+
enumerable: false,
|
|
223
|
+
configurable: false,
|
|
224
|
+
value: rejectMutation("delete")
|
|
225
|
+
},
|
|
226
|
+
clear: {
|
|
227
|
+
enumerable: false,
|
|
228
|
+
configurable: false,
|
|
229
|
+
value: rejectMutation("clear")
|
|
230
|
+
}
|
|
231
|
+
}), (_target, rejectMutation) => {
|
|
232
|
+
const extras = [];
|
|
233
|
+
for (const operation of [
|
|
234
|
+
"getOrInsert",
|
|
235
|
+
"getOrInsertComputed",
|
|
236
|
+
"emplace"
|
|
237
|
+
]) if (typeof Map.prototype[operation] === "function") extras.push([operation, rejectMutation(operation)]);
|
|
238
|
+
return extras;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function readonlySetView(raw) {
|
|
242
|
+
return createReadonlyCollectionView(raw, readonlySetViews, Set.prototype, "Set", (target, view, rejectMutation) => ({
|
|
243
|
+
size: {
|
|
244
|
+
enumerable: false,
|
|
245
|
+
configurable: false,
|
|
246
|
+
get: () => target.size
|
|
247
|
+
},
|
|
248
|
+
has: {
|
|
249
|
+
enumerable: false,
|
|
250
|
+
configurable: false,
|
|
251
|
+
value: (value) => target.has(value)
|
|
252
|
+
},
|
|
253
|
+
entries: {
|
|
254
|
+
enumerable: false,
|
|
255
|
+
configurable: false,
|
|
256
|
+
value: () => Set.prototype.entries.call(target)
|
|
257
|
+
},
|
|
258
|
+
keys: {
|
|
259
|
+
enumerable: false,
|
|
260
|
+
configurable: false,
|
|
261
|
+
value: () => Set.prototype.keys.call(target)
|
|
262
|
+
},
|
|
263
|
+
values: {
|
|
264
|
+
enumerable: false,
|
|
265
|
+
configurable: false,
|
|
266
|
+
value: () => Set.prototype.values.call(target)
|
|
267
|
+
},
|
|
268
|
+
forEach: {
|
|
269
|
+
enumerable: false,
|
|
270
|
+
configurable: false,
|
|
271
|
+
value: (callback, thisArg) => {
|
|
272
|
+
Set.prototype.forEach.call(target, (value) => callback.call(thisArg, value, value, view));
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[Symbol.iterator]: {
|
|
276
|
+
enumerable: false,
|
|
277
|
+
configurable: false,
|
|
278
|
+
value: () => Set.prototype.values.call(target)
|
|
279
|
+
},
|
|
280
|
+
add: {
|
|
281
|
+
enumerable: false,
|
|
282
|
+
configurable: false,
|
|
283
|
+
value: rejectMutation("add")
|
|
284
|
+
},
|
|
285
|
+
delete: {
|
|
286
|
+
enumerable: false,
|
|
287
|
+
configurable: false,
|
|
288
|
+
value: rejectMutation("delete")
|
|
289
|
+
},
|
|
290
|
+
clear: {
|
|
291
|
+
enumerable: false,
|
|
292
|
+
configurable: false,
|
|
293
|
+
value: rejectMutation("clear")
|
|
294
|
+
}
|
|
295
|
+
}), (target) => {
|
|
296
|
+
const extras = [];
|
|
297
|
+
const forwardSetOperation = (operation) => (other) => {
|
|
298
|
+
const method = Set.prototype[operation];
|
|
299
|
+
if (typeof method !== "function") throw new TypeError(`Set#${operation}() is unavailable in this JavaScript engine`);
|
|
300
|
+
const collection = isObjectLike(other) ? readonlyCollectionViewToRaw.get(other) ?? other : other;
|
|
301
|
+
return Reflect.apply(method, target, [collection]);
|
|
302
|
+
};
|
|
303
|
+
for (const operation of [
|
|
304
|
+
"union",
|
|
305
|
+
"intersection",
|
|
306
|
+
"difference",
|
|
307
|
+
"symmetricDifference",
|
|
308
|
+
"isSubsetOf",
|
|
309
|
+
"isSupersetOf",
|
|
310
|
+
"isDisjointFrom"
|
|
311
|
+
]) if (typeof Set.prototype[operation] === "function") extras.push([operation, forwardSetOperation(operation)]);
|
|
312
|
+
return extras;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function assertRootValue(value) {
|
|
316
|
+
if (!isPlainObjectOrArray(value)) throw new TypeError("deepSignal() only accepts a plain object or array root");
|
|
317
|
+
}
|
|
318
|
+
function assertDeepDataGraph(value) {
|
|
319
|
+
if (!isObjectLike(value)) return false;
|
|
320
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
321
|
+
const pending = [{
|
|
322
|
+
value,
|
|
323
|
+
insideOpaque: false
|
|
324
|
+
}];
|
|
325
|
+
let needsNormalization = false;
|
|
326
|
+
while (pending.length > 0) {
|
|
327
|
+
const { value: current, insideOpaque } = pending.pop();
|
|
328
|
+
if (toRaw(current) !== void 0) {
|
|
329
|
+
if (insideOpaque) throw new TypeError(proxyToRaw.has(current) ? "deepSignal() cannot store a deep proxy inside an opaque value" : "deepSignal() cannot store a deep collection view inside an opaque value");
|
|
330
|
+
needsNormalization = true;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const raw = current;
|
|
334
|
+
const visitedAs = seen.get(raw) ?? 0;
|
|
335
|
+
const visitFlag = insideOpaque ? 2 : 1;
|
|
336
|
+
if ((visitedAs & visitFlag) !== 0) continue;
|
|
337
|
+
seen.set(raw, visitedAs | visitFlag);
|
|
338
|
+
if (!insideOpaque && isIntrinsicPrototype(raw)) throw new TypeError("deepSignal() cannot store a built-in prototype object in deep state");
|
|
339
|
+
if (raw instanceof Map) {
|
|
340
|
+
for (const [key, entry] of raw) {
|
|
341
|
+
if (isObjectLike(key)) pending.push({
|
|
342
|
+
value: key,
|
|
343
|
+
insideOpaque: true
|
|
344
|
+
});
|
|
345
|
+
if (isObjectLike(entry)) pending.push({
|
|
346
|
+
value: entry,
|
|
347
|
+
insideOpaque: true
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (raw instanceof Set) {
|
|
353
|
+
for (const entry of raw) if (isObjectLike(entry)) pending.push({
|
|
354
|
+
value: entry,
|
|
355
|
+
insideOpaque: true
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!isPlainObjectOrArray(raw)) {
|
|
360
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
361
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(raw, key);
|
|
362
|
+
if (descriptor !== void 0 && "value" in descriptor && isObjectLike(descriptor.value)) pending.push({
|
|
363
|
+
value: descriptor.value,
|
|
364
|
+
insideOpaque: true
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!insideOpaque) {
|
|
370
|
+
assertExtensible(raw);
|
|
371
|
+
assertDataProperties(raw);
|
|
372
|
+
}
|
|
373
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
374
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(raw, key);
|
|
375
|
+
if (descriptor !== void 0 && "value" in descriptor) {
|
|
376
|
+
const child = descriptor.value;
|
|
377
|
+
if (isObjectLike(child)) pending.push({
|
|
378
|
+
value: child,
|
|
379
|
+
insideOpaque
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return needsNormalization;
|
|
385
|
+
}
|
|
386
|
+
function matchesNormalizedGraph(source, normalized) {
|
|
387
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
388
|
+
const targets = /* @__PURE__ */ new WeakMap();
|
|
389
|
+
const pending = [[source, normalized]];
|
|
390
|
+
while (pending.length > 0) {
|
|
391
|
+
const [currentSource, currentTarget] = pending.pop();
|
|
392
|
+
if (typeof currentSource !== "object" || currentSource === null) {
|
|
393
|
+
if (!Object.is(currentSource, currentTarget)) return false;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const wrapperRaw = toRaw(currentSource);
|
|
397
|
+
if (wrapperRaw !== void 0) {
|
|
398
|
+
if (wrapperRaw !== currentTarget) return false;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!isPlainObjectOrArray(currentSource)) {
|
|
402
|
+
if (currentSource !== currentTarget) return false;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (typeof currentTarget !== "object" || currentTarget === null || !isPlainObjectOrArray(currentTarget)) return false;
|
|
406
|
+
const knownTarget = sources.get(currentSource);
|
|
407
|
+
if (knownTarget !== void 0) {
|
|
408
|
+
if (knownTarget !== currentTarget) return false;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const knownSource = targets.get(currentTarget);
|
|
412
|
+
if (knownSource !== void 0 && knownSource !== currentSource) return false;
|
|
413
|
+
sources.set(currentSource, currentTarget);
|
|
414
|
+
targets.set(currentTarget, currentSource);
|
|
415
|
+
if (Array.isArray(currentSource) !== Array.isArray(currentTarget) || Object.getPrototypeOf(currentSource) !== Object.getPrototypeOf(currentTarget)) return false;
|
|
416
|
+
const sourceKeys = Reflect.ownKeys(currentSource);
|
|
417
|
+
const targetKeys = Reflect.ownKeys(currentTarget);
|
|
418
|
+
if (sourceKeys.length !== targetKeys.length) return false;
|
|
419
|
+
for (const [index, key] of sourceKeys.entries()) {
|
|
420
|
+
if (key !== targetKeys[index]) return false;
|
|
421
|
+
const sourceDescriptor = Reflect.getOwnPropertyDescriptor(currentSource, key);
|
|
422
|
+
const targetDescriptor = Reflect.getOwnPropertyDescriptor(currentTarget, key);
|
|
423
|
+
if (sourceDescriptor === void 0 || targetDescriptor === void 0 || !("value" in sourceDescriptor) || !("value" in targetDescriptor) || sourceDescriptor.configurable !== targetDescriptor.configurable || sourceDescriptor.enumerable !== targetDescriptor.enumerable || sourceDescriptor.writable !== targetDescriptor.writable) return false;
|
|
424
|
+
pending.push([sourceDescriptor.value, targetDescriptor.value]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* An array of `length` with no own index properties — holes, not `undefined`s.
|
|
431
|
+
* `Array.from({ length })` would materialize every index as an own property,
|
|
432
|
+
* so cloning a sparse carrier produced an array with strictly more own keys
|
|
433
|
+
* than its source. `matchesNormalizedGraph` compares own keys one-for-one, so
|
|
434
|
+
* that clone could never match its source again: the carrier cache missed on
|
|
435
|
+
* every re-assignment, breaking the documented guarantee that assigning the
|
|
436
|
+
* same carrier twice preserves identity and aliasing.
|
|
437
|
+
*/
|
|
438
|
+
function cloneWithoutProxies(value) {
|
|
439
|
+
const wrapperRaw = toRaw(value);
|
|
440
|
+
if (wrapperRaw !== void 0) return wrapperRaw;
|
|
441
|
+
if (!isObjectLike(value)) return value;
|
|
442
|
+
const cachedRoot = normalizedProxyCarriers.get(value);
|
|
443
|
+
if (cachedRoot !== void 0 && matchesNormalizedGraph(value, cachedRoot)) return cachedRoot;
|
|
444
|
+
const clones = /* @__PURE__ */ new WeakMap();
|
|
445
|
+
const created = [];
|
|
446
|
+
const pending = [];
|
|
447
|
+
const resolve = (current) => {
|
|
448
|
+
const resolvedRaw = toRaw(current);
|
|
449
|
+
if (resolvedRaw !== void 0) return resolvedRaw;
|
|
450
|
+
if (!isPlainObjectOrArray(current)) return current;
|
|
451
|
+
const local = clones.get(current);
|
|
452
|
+
if (local !== void 0) return local;
|
|
453
|
+
const result = Array.isArray(current) ? emptyArrayOfLength(current.length) : Object.create(Object.getPrototypeOf(current));
|
|
454
|
+
clones.set(current, result);
|
|
455
|
+
created.push([current, result]);
|
|
456
|
+
pending.push(current);
|
|
457
|
+
return result;
|
|
458
|
+
};
|
|
459
|
+
const root = resolve(value);
|
|
460
|
+
while (pending.length > 0) {
|
|
461
|
+
const source = pending.pop();
|
|
462
|
+
const target = clones.get(source);
|
|
463
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
464
|
+
if (Array.isArray(source) && key === "length") continue;
|
|
465
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
|
|
466
|
+
if (descriptor === void 0) continue;
|
|
467
|
+
if (!("value" in descriptor)) throw new TypeError("deepSignal() does not support accessor properties");
|
|
468
|
+
descriptor.value = resolve(descriptor.value);
|
|
469
|
+
Reflect.defineProperty(target, key, descriptor);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const [source, target] of created) normalizedProxyCarriers.set(source, target);
|
|
473
|
+
return root;
|
|
474
|
+
}
|
|
475
|
+
function prepareDeepValue(value) {
|
|
476
|
+
const wrapperRaw = toRaw(value);
|
|
477
|
+
if (wrapperRaw !== void 0) return wrapperRaw;
|
|
478
|
+
return assertDeepDataGraph(value) ? cloneWithoutProxies(value) : value;
|
|
479
|
+
}
|
|
480
|
+
const unwrap = (value) => {
|
|
481
|
+
return prepareDeepValue(value);
|
|
482
|
+
};
|
|
483
|
+
const OBJECT_PROTOTYPE_KEYS = new Set(Reflect.ownKeys(Object.prototype));
|
|
484
|
+
const ARRAY_PROTOTYPE_KEYS = /* @__PURE__ */ new Set([...Reflect.ownKeys(Object.prototype), ...Reflect.ownKeys(Array.prototype)]);
|
|
485
|
+
const isInheritedPrototypeMember = (target, key) => {
|
|
486
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return false;
|
|
487
|
+
const prototype = Object.getPrototypeOf(target);
|
|
488
|
+
if (prototype === Object.prototype) return OBJECT_PROTOTYPE_KEYS.has(key);
|
|
489
|
+
if (prototype === Array.prototype) return ARRAY_PROTOTYPE_KEYS.has(key);
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
492
|
+
const getVersion = (versions, key) => {
|
|
493
|
+
let version = versions.get(key);
|
|
494
|
+
if (version === void 0) {
|
|
495
|
+
version = adapter.createVersionSignal(0);
|
|
496
|
+
versions.set(key, version);
|
|
497
|
+
}
|
|
498
|
+
return version;
|
|
499
|
+
};
|
|
500
|
+
const track = (metadata, target, versions, indices, key) => {
|
|
501
|
+
if (!shouldTrackDeepRead()) return;
|
|
502
|
+
if (isInheritedPrototypeMember(target, key)) {
|
|
503
|
+
if (!Array.isArray(target)) trackIterationOf(metadata);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (isArrayIndex(key)) indices.add(Number(key));
|
|
507
|
+
const version = getVersion(versions, key);
|
|
508
|
+
adapter.markWatched(version);
|
|
509
|
+
version.value;
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Queues `key`'s version signals for removal once nothing depends on them.
|
|
513
|
+
* Only called for a key that has just been removed *and* notified, which is
|
|
514
|
+
* what makes the eventual removal safe: the notification forces every
|
|
515
|
+
* subscriber to re-run, so one that still cares re-reads the key (re-arming
|
|
516
|
+
* `markWatched`) and one that does not has already been unlinked.
|
|
517
|
+
*/
|
|
518
|
+
/**
|
|
519
|
+
* Drops the version signals of removed keys once they have no subscribers
|
|
520
|
+
* left, so a virtualized or repeatedly-spliced list stops accumulating one
|
|
521
|
+
* entry per key that ever existed.
|
|
522
|
+
*
|
|
523
|
+
* Runs only at batch depth 0, where the write that queued these keys has
|
|
524
|
+
* already flushed its effects; before that, `hasSubscribers()` would still
|
|
525
|
+
* report the pre-flush picture. A key that is still subscribed (typically a
|
|
526
|
+
* React store whose re-render has not committed yet) stays queued and is
|
|
527
|
+
* reconsidered on the next sweep rather than being dropped early.
|
|
528
|
+
*/
|
|
529
|
+
const sweepPrunedKeys = (metadata, target) => {
|
|
530
|
+
const prunable = metadata.prunable;
|
|
531
|
+
if (prunable === void 0 || prunable.size === 0) return;
|
|
532
|
+
if (adapter.getBatchDepth() !== 0) return;
|
|
533
|
+
for (const key of prunable) {
|
|
534
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
535
|
+
prunable.delete(key);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const property = metadata.properties.get(key);
|
|
539
|
+
const existence = metadata.existence.get(key);
|
|
540
|
+
if (property !== void 0 && adapter.hasSubscribers(property) || existence !== void 0 && adapter.hasSubscribers(existence)) continue;
|
|
541
|
+
metadata.properties.delete(key);
|
|
542
|
+
metadata.existence.delete(key);
|
|
543
|
+
if (isArrayIndex(key)) {
|
|
544
|
+
const index = Number(key);
|
|
545
|
+
metadata.propertyIndices.delete(index);
|
|
546
|
+
metadata.existenceIndices.delete(index);
|
|
547
|
+
}
|
|
548
|
+
prunable.delete(key);
|
|
549
|
+
}
|
|
550
|
+
if (prunable.size === 0) metadata.prunable = void 0;
|
|
551
|
+
};
|
|
552
|
+
const trackIterationOf = (metadata) => {
|
|
553
|
+
metadata.iteration ??= adapter.createVersionSignal(0);
|
|
554
|
+
metadata.iteration.value;
|
|
555
|
+
};
|
|
556
|
+
const trackIteration = (metadata) => {
|
|
557
|
+
if (!shouldTrackDeepRead()) return;
|
|
558
|
+
trackIterationOf(metadata);
|
|
559
|
+
};
|
|
560
|
+
const notifyTruncatedIndices = (metadata, currentLength, oldLength) => {
|
|
561
|
+
const truncatedCount = oldLength - currentLength;
|
|
562
|
+
const trackedCount = metadata.propertyIndices.size + metadata.existenceIndices.size;
|
|
563
|
+
if (trackedCount === 0) return;
|
|
564
|
+
if (truncatedCount <= trackedCount) {
|
|
565
|
+
for (let index = currentLength; index < oldLength; index++) {
|
|
566
|
+
const key = String(index);
|
|
567
|
+
notify(metadata.properties, key);
|
|
568
|
+
notify(metadata.existence, key);
|
|
569
|
+
markPrunable(metadata, key);
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
for (const index of metadata.propertyIndices) if (index >= currentLength && index < oldLength) {
|
|
574
|
+
const key = String(index);
|
|
575
|
+
notify(metadata.properties, key);
|
|
576
|
+
markPrunable(metadata, key);
|
|
577
|
+
}
|
|
578
|
+
for (const index of metadata.existenceIndices) if (index >= currentLength && index < oldLength) {
|
|
579
|
+
const key = String(index);
|
|
580
|
+
notify(metadata.existence, key);
|
|
581
|
+
markPrunable(metadata, key);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* Whether `wrap()` would hand back something other than `value` itself — a
|
|
586
|
+
* deep proxy for a plain object/array, or a readonly view for a Map/Set. The
|
|
587
|
+
* `get` trap uses this to reject a non-configurable, non-writable property
|
|
588
|
+
* whose value cannot be substituted, before `wrap()` performs the swap.
|
|
589
|
+
*/
|
|
590
|
+
const isWrappedByValue = (value) => isPlainObjectOrArray(value) || value instanceof Map || value instanceof Set;
|
|
591
|
+
const wrap = (value) => {
|
|
592
|
+
const rawValue = toRaw(value) ?? value;
|
|
593
|
+
if (rawValue instanceof Map) return readonlyMapView(rawValue);
|
|
594
|
+
if (rawValue instanceof Set) return readonlySetView(rawValue);
|
|
595
|
+
if (!isPlainObjectOrArray(rawValue)) return rawValue;
|
|
596
|
+
assertExtensible(rawValue);
|
|
597
|
+
const cached = rawToMetadata.get(rawValue);
|
|
598
|
+
if (cached !== void 0) return cached.proxy;
|
|
599
|
+
assertDataProperties(rawValue);
|
|
600
|
+
const metadata = {
|
|
601
|
+
properties: /* @__PURE__ */ new Map(),
|
|
602
|
+
existence: /* @__PURE__ */ new Map(),
|
|
603
|
+
propertyIndices: /* @__PURE__ */ new Set(),
|
|
604
|
+
existenceIndices: /* @__PURE__ */ new Set(),
|
|
605
|
+
arrayMethods: /* @__PURE__ */ new Map(),
|
|
606
|
+
proxy: void 0
|
|
607
|
+
};
|
|
608
|
+
const proxy = new Proxy(rawValue, {
|
|
609
|
+
get(target, key, receiver) {
|
|
610
|
+
if (key === SIGNAL_BRAND && !Object.prototype.hasOwnProperty.call(target, key)) return;
|
|
611
|
+
track(metadata, target, metadata.properties, metadata.propertyIndices, key);
|
|
612
|
+
const result = Reflect.get(target, key, receiver);
|
|
613
|
+
if (Array.isArray(target) && ARRAY_MUTATORS.has(key) && typeof result === "function") {
|
|
614
|
+
const cachedMethod = metadata.arrayMethods.get(key);
|
|
615
|
+
if (cachedMethod?.method === result) return cachedMethod.wrapper;
|
|
616
|
+
const method = result;
|
|
617
|
+
const wrapper = function(...args) {
|
|
618
|
+
try {
|
|
619
|
+
return adapter.batch(() => adapter.untracked(() => Reflect.apply(method, this, args)));
|
|
620
|
+
} finally {
|
|
621
|
+
sweepPrunedKeys(metadata, target);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
metadata.arrayMethods.set(key, {
|
|
625
|
+
method,
|
|
626
|
+
wrapper
|
|
627
|
+
});
|
|
628
|
+
return wrapper;
|
|
629
|
+
}
|
|
630
|
+
if (Array.isArray(target) && ARRAY_SEARCHES.has(key) && typeof result === "function") {
|
|
631
|
+
const cachedMethod = metadata.arrayMethods.get(key);
|
|
632
|
+
if (cachedMethod?.method === result) return cachedMethod.wrapper;
|
|
633
|
+
const method = result;
|
|
634
|
+
const wrapper = function(...args) {
|
|
635
|
+
const found = Reflect.apply(method, this, args);
|
|
636
|
+
if (found !== -1 && found !== false || !isObjectLike(args[0])) return found;
|
|
637
|
+
args[0] = toRaw(args[0]) ?? args[0];
|
|
638
|
+
return Reflect.apply(method, toRaw(this) ?? this, args);
|
|
639
|
+
};
|
|
640
|
+
metadata.arrayMethods.set(key, {
|
|
641
|
+
method,
|
|
642
|
+
wrapper
|
|
643
|
+
});
|
|
644
|
+
return wrapper;
|
|
645
|
+
}
|
|
646
|
+
if (isWrappedByValue(result)) {
|
|
647
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
|
|
648
|
+
if (descriptor !== void 0 && "value" in descriptor && descriptor.configurable === false && descriptor.writable === false) throw new TypeError("deepSignal() cannot wrap a non-configurable, non-writable object property");
|
|
649
|
+
}
|
|
650
|
+
return wrap(result);
|
|
651
|
+
},
|
|
652
|
+
set(target, key, nextValue, receiver) {
|
|
653
|
+
if (receiver !== metadata.proxy) return Reflect.set(target, key, nextValue, receiver);
|
|
654
|
+
if (key === "__proto__") throw new TypeError("deepSignal() does not support prototype mutation");
|
|
655
|
+
if (key === SIGNAL_BRAND) throw new TypeError("deepSignal() does not support branding state as a signal");
|
|
656
|
+
const oldValue = Reflect.get(target, key, target);
|
|
657
|
+
const existed = Reflect.has(target, key);
|
|
658
|
+
const owned = Object.prototype.hasOwnProperty.call(target, key);
|
|
659
|
+
const oldLength = Array.isArray(target) ? target.length : void 0;
|
|
660
|
+
const rawNextValue = unwrap(nextValue);
|
|
661
|
+
if (!Reflect.set(target, key, rawNextValue, target)) return false;
|
|
662
|
+
const currentValue = Reflect.get(target, key, target);
|
|
663
|
+
const existsNow = Reflect.has(target, key);
|
|
664
|
+
const ownedNow = Object.prototype.hasOwnProperty.call(target, key);
|
|
665
|
+
adapter.batch(() => {
|
|
666
|
+
if (!Object.is(oldValue, currentValue) || owned !== ownedNow) notify(metadata.properties, key);
|
|
667
|
+
if (existed !== existsNow || owned !== ownedNow) notify(metadata.existence, key);
|
|
668
|
+
if (owned !== ownedNow) notifyIteration(metadata);
|
|
669
|
+
if (Array.isArray(target) && oldLength !== void 0) {
|
|
670
|
+
const currentLength = target.length;
|
|
671
|
+
if (key !== "length" && oldLength !== currentLength) notify(metadata.properties, "length");
|
|
672
|
+
if (key === "length" && currentLength < oldLength) {
|
|
673
|
+
notifyTruncatedIndices(metadata, currentLength, oldLength);
|
|
674
|
+
notifyIteration(metadata);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
sweepPrunedKeys(metadata, target);
|
|
679
|
+
return true;
|
|
680
|
+
},
|
|
681
|
+
deleteProperty(target, key) {
|
|
682
|
+
const owned = Object.prototype.hasOwnProperty.call(target, key);
|
|
683
|
+
const succeeded = Reflect.deleteProperty(target, key);
|
|
684
|
+
if (!succeeded || !owned) return succeeded;
|
|
685
|
+
adapter.batch(() => {
|
|
686
|
+
notify(metadata.properties, key);
|
|
687
|
+
notify(metadata.existence, key);
|
|
688
|
+
notifyIteration(metadata);
|
|
689
|
+
markPrunable(metadata, key);
|
|
690
|
+
});
|
|
691
|
+
sweepPrunedKeys(metadata, target);
|
|
692
|
+
return true;
|
|
693
|
+
},
|
|
694
|
+
getOwnPropertyDescriptor(target, key) {
|
|
695
|
+
track(metadata, target, metadata.existence, metadata.existenceIndices, key);
|
|
696
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
|
|
697
|
+
if (descriptor === void 0 || !("value" in descriptor)) return descriptor;
|
|
698
|
+
if (descriptor.configurable === false && descriptor.writable === false) return descriptor;
|
|
699
|
+
descriptor.value = wrap(descriptor.value);
|
|
700
|
+
return descriptor;
|
|
701
|
+
},
|
|
702
|
+
has(target, key) {
|
|
703
|
+
track(metadata, target, metadata.existence, metadata.existenceIndices, key);
|
|
704
|
+
return Reflect.has(target, key);
|
|
705
|
+
},
|
|
706
|
+
ownKeys(target) {
|
|
707
|
+
trackIteration(metadata);
|
|
708
|
+
return Reflect.ownKeys(target);
|
|
709
|
+
},
|
|
710
|
+
defineProperty() {
|
|
711
|
+
throw new TypeError("deepSignal() does not support property descriptors");
|
|
712
|
+
},
|
|
713
|
+
preventExtensions() {
|
|
714
|
+
throw new TypeError("deepSignal() state must remain extensible");
|
|
715
|
+
},
|
|
716
|
+
setPrototypeOf() {
|
|
717
|
+
throw new TypeError("deepSignal() does not support prototype mutation");
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
metadata.proxy = proxy;
|
|
721
|
+
rawToMetadata.set(rawValue, metadata);
|
|
722
|
+
proxyToRaw.set(proxy, rawValue);
|
|
723
|
+
return proxy;
|
|
724
|
+
};
|
|
725
|
+
class DeepSignalImpl {
|
|
726
|
+
#source;
|
|
727
|
+
constructor(source) {
|
|
728
|
+
this.#source = source;
|
|
729
|
+
const protocol = getReadableInterop(this.#source);
|
|
730
|
+
if (protocol !== void 0) attachReadableInterop(this, protocol);
|
|
731
|
+
}
|
|
732
|
+
get value() {
|
|
733
|
+
return wrap(this.#source.value);
|
|
734
|
+
}
|
|
735
|
+
set value(nextValue) {
|
|
736
|
+
const rawValue = unwrap(nextValue);
|
|
737
|
+
assertRootValue(rawValue);
|
|
738
|
+
this.#source.value = rawValue;
|
|
739
|
+
}
|
|
740
|
+
peek() {
|
|
741
|
+
return this.#source.peek();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Reports the per-key reactive metadata currently retained for a deep proxy
|
|
746
|
+
* (or for the raw object behind it). Exposed for this package's own tests
|
|
747
|
+
* only — it is deliberately absent from every published entry point, and the
|
|
748
|
+
* shape it returns is internal detail, not API.
|
|
749
|
+
*/
|
|
750
|
+
function inspectDeepSignalMetadata(value) {
|
|
751
|
+
const metadata = rawToMetadata.get(toRaw(value) ?? value);
|
|
752
|
+
if (metadata === void 0) return void 0;
|
|
753
|
+
return {
|
|
754
|
+
properties: [...metadata.properties.keys()],
|
|
755
|
+
existence: [...metadata.existence.keys()],
|
|
756
|
+
propertyIndices: [...metadata.propertyIndices],
|
|
757
|
+
existenceIndices: [...metadata.existenceIndices]
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Creates a signal that lazily tracks nested plain-object and array properties.
|
|
762
|
+
* Mutations must go through `.value`; changes made through the original raw
|
|
763
|
+
* object are intentionally not observable.
|
|
764
|
+
*/
|
|
765
|
+
function deepSignal(initialValue) {
|
|
766
|
+
const rawInitialValue = prepareDeepValue(initialValue);
|
|
767
|
+
assertRootValue(rawInitialValue);
|
|
768
|
+
wrap(rawInitialValue);
|
|
769
|
+
const root = adapter.createSignal(rawInitialValue);
|
|
770
|
+
const result = new DeepSignalImpl(root);
|
|
771
|
+
adapter.registerDeepSignal?.(result);
|
|
772
|
+
return result;
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
deepSignal,
|
|
776
|
+
inspectDeepSignalMetadata
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/core/deep-signal.ts
|
|
781
|
+
let productionDeepSignals;
|
|
782
|
+
function getProductionDeepSignals() {
|
|
783
|
+
return productionDeepSignals ??= createDeepSignalFactory({
|
|
784
|
+
createSignal(initial) {
|
|
785
|
+
return coreRuntime.createDeepSignal(initial);
|
|
786
|
+
},
|
|
787
|
+
createVersionSignal(initial) {
|
|
788
|
+
return coreRuntime.createDeepSignalVersion(initial);
|
|
789
|
+
},
|
|
790
|
+
markWatched(source) {
|
|
791
|
+
coreRuntime.markDeepSignalWatched(source);
|
|
792
|
+
},
|
|
793
|
+
hasSubscribers(source) {
|
|
794
|
+
return coreRuntime.hasDeepSignalSubscribers(source);
|
|
795
|
+
},
|
|
796
|
+
batch,
|
|
797
|
+
untracked,
|
|
798
|
+
isSignal,
|
|
799
|
+
hasActiveSubscriber: () => {
|
|
800
|
+
const owner = executionContext.owner;
|
|
801
|
+
if (owner === UNTRACKED_OWNER) return false;
|
|
802
|
+
return coreRuntime.hasActiveSubscriber() || owner !== void 0 && typeof owner !== "symbol" && (owner.kind === "graph" || owner.kind === "render");
|
|
803
|
+
},
|
|
804
|
+
getBatchDepth: () => coreRuntime.getBatchDepth(),
|
|
805
|
+
isSpeculative: () => coreRuntime.isSpeculative(),
|
|
806
|
+
markSpeculativeDeepRead: () => coreRuntime.markSpeculativeDeepRead(),
|
|
807
|
+
registerDeepSignal(value) {
|
|
808
|
+
registerSignal(value);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Creates a signal that lazily tracks nested plain-object and array properties.
|
|
814
|
+
* Mutations must go through `.value`; changes made through the original raw
|
|
815
|
+
* object are intentionally not observable.
|
|
816
|
+
*/
|
|
817
|
+
function deepSignal(initialValue) {
|
|
818
|
+
return getProductionDeepSignals().deepSignal(initialValue);
|
|
819
|
+
}
|
|
820
|
+
//#endregion
|
|
821
|
+
//#region src/core/readable-subscription.ts
|
|
822
|
+
/** Subscribe through the private V1 boundary without creating an EffectNode. */
|
|
823
|
+
function subscribeReadableV1(readable, listener) {
|
|
824
|
+
const protocol = getReadableInterop(readable);
|
|
825
|
+
if (protocol === void 0) return void 0;
|
|
826
|
+
const subscription = protocol.subscribe(() => notifyListener(listener));
|
|
827
|
+
return () => subscription.unsubscribe();
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/react/hooks.ts
|
|
831
|
+
const EMPTY_DEPENDENCIES = [];
|
|
832
|
+
function assertSignalSnapshot(value) {
|
|
833
|
+
if (typeof value === "object" && value !== null || typeof value === "function") throw new TypeError("useDeepSignalValue selector must return a primitive snapshot; objects, Proxies, and functions are not supported");
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Shared `useSyncExternalStore` wiring for effect-backed subscriptions only:
|
|
837
|
+
* the dynamic deep-selector store and structural-readable compatibility
|
|
838
|
+
* fallback. Package readables with ReadableInterop V1 use a direct watcher.
|
|
839
|
+
* On `subscribe`, starts an `effect()` that reruns `onEvaluate` on relevant
|
|
840
|
+
* writes and notifies React when it reports a change.
|
|
841
|
+
*
|
|
842
|
+
* `getSnapshot` is intentionally not this helper's concern — the caller
|
|
843
|
+
* wires its own, whether that means recomputing fresh each call
|
|
844
|
+
* (`useSignalValue`) or returning a cached result (the deep-selector store).
|
|
845
|
+
*
|
|
846
|
+
* An exception thrown by `onEvaluate` is swallowed here and treated as a
|
|
847
|
+
* change worth notifying about. That leaves the next render-time snapshot
|
|
848
|
+
* read to surface the error to an Error Boundary rather than letting the
|
|
849
|
+
* background effect's exception escape through the signal writer.
|
|
850
|
+
*/
|
|
851
|
+
function createEffectBackedStoreSubscription(onEvaluate) {
|
|
852
|
+
return (notify) => {
|
|
853
|
+
let isInitialRun = true;
|
|
854
|
+
return detachedEffect(() => {
|
|
855
|
+
let changed;
|
|
856
|
+
try {
|
|
857
|
+
changed = onEvaluate();
|
|
858
|
+
} catch {
|
|
859
|
+
changed = true;
|
|
860
|
+
}
|
|
861
|
+
if (isInitialRun) {
|
|
862
|
+
isInitialRun = false;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (changed) notify();
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Holds a selector result outside the reactive graph. A selector error must
|
|
871
|
+
* not escape from the signal write that caused a reactive re-evaluation:
|
|
872
|
+
* React needs to observe it during its next render so an Error Boundary can
|
|
873
|
+
* handle it. The cached result also avoids rerunning the selector on every
|
|
874
|
+
* render.
|
|
875
|
+
*/
|
|
876
|
+
function createDeepSelectorStore(source, selector) {
|
|
877
|
+
const evaluate = () => {
|
|
878
|
+
try {
|
|
879
|
+
const value = untrackedRender(() => selector(source.value));
|
|
880
|
+
assertSignalSnapshot(value);
|
|
881
|
+
return {
|
|
882
|
+
kind: "value",
|
|
883
|
+
value
|
|
884
|
+
};
|
|
885
|
+
} catch (error) {
|
|
886
|
+
return {
|
|
887
|
+
kind: "error",
|
|
888
|
+
error
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
let result = evaluate();
|
|
893
|
+
const hasChanged = (next) => {
|
|
894
|
+
if (result.kind !== next.kind) return true;
|
|
895
|
+
return result.kind === "value" && next.kind === "value" ? !Object.is(result.value, next.value) : result.kind === "error" && next.kind === "error" ? !Object.is(result.error, next.error) : false;
|
|
896
|
+
};
|
|
897
|
+
return {
|
|
898
|
+
subscribe: createEffectBackedStoreSubscription(() => {
|
|
899
|
+
const next = evaluate();
|
|
900
|
+
if (!hasChanged(next)) return false;
|
|
901
|
+
result = next;
|
|
902
|
+
return true;
|
|
903
|
+
}),
|
|
904
|
+
getSnapshot() {
|
|
905
|
+
if (result.kind === "error") throw result.error;
|
|
906
|
+
return result.value;
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/** Creates a signal whose identity is stable for the lifetime of this component. */
|
|
911
|
+
function useSignal(initialValue) {
|
|
912
|
+
const signalRef = useRef(void 0);
|
|
913
|
+
if (signalRef.current === void 0) signalRef.current = signal(initialValue);
|
|
914
|
+
return signalRef.current;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Creates a deep signal whose identity is stable for the component lifetime.
|
|
918
|
+
* A factory is evaluated only while initializing a mounted component instance;
|
|
919
|
+
* it must remain pure because React Strict Mode may replay initial rendering.
|
|
920
|
+
*/
|
|
921
|
+
function useDeepSignal(initialValue) {
|
|
922
|
+
const signalRef = useRef(void 0);
|
|
923
|
+
if (signalRef.current === void 0) signalRef.current = deepSignal(typeof initialValue === "function" ? initialValue() : initialValue);
|
|
924
|
+
return signalRef.current;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Selects a property-level primitive snapshot from a deep signal.
|
|
928
|
+
*
|
|
929
|
+
* Every non-signal value captured by `selector` must be listed in
|
|
930
|
+
* `dependencies`. Object and proxy results are intentionally rejected because
|
|
931
|
+
* mutable snapshots cannot satisfy `useSyncExternalStore` identity semantics.
|
|
932
|
+
*
|
|
933
|
+
* `dependencies` must keep a fixed length across this component's lifetime,
|
|
934
|
+
* matching `useMemo`'s own rule (its length feeds a `useMemo` deps array
|
|
935
|
+
* below via `[source, ...dependencies]`). A length change is caught and
|
|
936
|
+
* thrown as a clear error rather than left to degrade into React's silent
|
|
937
|
+
* "changed size between renders" dev warning, mirroring `useComputed`'s
|
|
938
|
+
* dependency-mode-switch guard.
|
|
939
|
+
*/
|
|
940
|
+
function useDeepSignalValue(source, selector, dependencies) {
|
|
941
|
+
const initialDependencyLengthRef = useRef(void 0);
|
|
942
|
+
initialDependencyLengthRef.current ??= dependencies.length;
|
|
943
|
+
if (dependencies.length !== initialDependencyLengthRef.current) {
|
|
944
|
+
const error = /* @__PURE__ */ new Error(`useDeepSignalValue: the \`dependencies\` array length changed between renders (from ${initialDependencyLengthRef.current} to ${dependencies.length}) for this call site. Keep \`dependencies\` a fixed length across the component's lifetime, matching useMemo's rules.`);
|
|
945
|
+
error.name = "UseDeepSignalValueDependencyLengthChangeError";
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
const store = useMemo(() => createDeepSelectorStore(source, selector), [source, ...dependencies]);
|
|
949
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Creates a computed signal with a stable identity.
|
|
953
|
+
*
|
|
954
|
+
* When `dependencies` is omitted, the getter must only read signals. Its initial
|
|
955
|
+
* closure is retained for the component lifetime, so props, state, and other
|
|
956
|
+
* non-signal values must not be captured in that mode.
|
|
957
|
+
*
|
|
958
|
+
* When the getter captures props, state, or any other non-signal value, list all
|
|
959
|
+
* of those values in `dependencies`. React memoization then creates a separate
|
|
960
|
+
* computed when they change, rather than replacing the getter of an existing
|
|
961
|
+
* computed during render. An abandoned render therefore cannot change the
|
|
962
|
+
* closure used by the previously committed computed. Choose one mode for a
|
|
963
|
+
* component's lifetime.
|
|
964
|
+
*/
|
|
965
|
+
function useComputed(getValue, dependencies) {
|
|
966
|
+
const dependencyComputed = useMemo(() => dependencies === void 0 ? void 0 : computed(getValue), dependencies ?? EMPTY_DEPENDENCIES);
|
|
967
|
+
const initialModeRef = useRef(void 0);
|
|
968
|
+
const signalOnlyComputedRef = useRef(void 0);
|
|
969
|
+
const mode = dependencies === void 0 ? "without a dependency array" : "with a dependency array";
|
|
970
|
+
initialModeRef.current ??= mode;
|
|
971
|
+
if (initialModeRef.current !== mode) {
|
|
972
|
+
const error = /* @__PURE__ */ new Error(`useComputed: the dependency-array mode changed between renders (from ${initialModeRef.current} to ${mode}) for this call site. Keep passing deps consistently, matching useMemo's rules.`);
|
|
973
|
+
error.name = "UseComputedModeChangeError";
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
if (dependencyComputed !== void 0) return dependencyComputed;
|
|
977
|
+
if (signalOnlyComputedRef.current === void 0) signalOnlyComputedRef.current = computed(getValue);
|
|
978
|
+
return signalOnlyComputedRef.current;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Runs a reactive effect after this component has committed, disposing it when
|
|
982
|
+
* the component unmounts (and during React Strict Mode's development replay).
|
|
983
|
+
*
|
|
984
|
+
* When `dependencies` is omitted, the callback must only capture signals. Its
|
|
985
|
+
* initial closure is retained for the component lifetime, so unrelated React
|
|
986
|
+
* renders do not restart the effect.
|
|
987
|
+
*
|
|
988
|
+
* When the callback captures props, state, or any other non-signal value, list
|
|
989
|
+
* all of those values in `dependencies`. The effect is then reconnected after
|
|
990
|
+
* those dependencies change. Choose one mode for a component's lifetime.
|
|
991
|
+
*/
|
|
992
|
+
function useSignalEffect(callback, dependencies) {
|
|
993
|
+
useEffect(() => detachedEffect(callback), dependencies ?? EMPTY_DEPENDENCIES);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Reads a signal and subscribes the component to subsequent changes through
|
|
997
|
+
* ReadableInterop V1 when available.
|
|
998
|
+
*
|
|
999
|
+
* `useSyncExternalStore` owns the initial consistency check after subscribing.
|
|
1000
|
+
* For V1 readables, its watcher tracks revisions without an EffectNode. A
|
|
1001
|
+
* read that throws (a computed whose cached error `.value` rethrows) remains
|
|
1002
|
+
* for the render-time `getSnapshot` to surface to an Error Boundary; only the
|
|
1003
|
+
* structural-readable fallback needs an effect to contain background errors.
|
|
1004
|
+
*
|
|
1005
|
+
* A `deepSignal` is rejected at compile time: this subscribes to root
|
|
1006
|
+
* replacement only, so nested mutations would never re-render. Read deep state
|
|
1007
|
+
* with `useDeepSignalValue` or inside a tracked component instead.
|
|
1008
|
+
*/
|
|
1009
|
+
function useSignalValue(source) {
|
|
1010
|
+
const subscribe = useCallback((notify) => {
|
|
1011
|
+
const unsubscribe = subscribeReadableV1(source, notify);
|
|
1012
|
+
if (unsubscribe !== void 0) return unsubscribe;
|
|
1013
|
+
return createEffectBackedStoreSubscription(() => {
|
|
1014
|
+
source.value;
|
|
1015
|
+
return true;
|
|
1016
|
+
})(notify);
|
|
1017
|
+
}, [source]);
|
|
1018
|
+
const getSnapshot = useCallback(() => untracked(() => source.value), [source]);
|
|
1019
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
1020
|
+
}
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/runtime/jsx.ts
|
|
1023
|
+
/** The small set of DOM properties that support direct signal bindings. */
|
|
1024
|
+
const REACTIVE_PROP_NAMES = /* @__PURE__ */ new Set([
|
|
1025
|
+
"title",
|
|
1026
|
+
"id",
|
|
1027
|
+
"className",
|
|
1028
|
+
"hidden",
|
|
1029
|
+
"disabled",
|
|
1030
|
+
"style",
|
|
1031
|
+
"value",
|
|
1032
|
+
"checked"
|
|
1033
|
+
]);
|
|
1034
|
+
function isControlledTwoWayProp(tagName, name) {
|
|
1035
|
+
if (name === "value") return tagName === "input" || tagName === "textarea" || tagName === "select";
|
|
1036
|
+
if (name === "checked") return tagName === "input";
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
function resolveBindingKind(tagName, name) {
|
|
1040
|
+
if (name === "style") return "style";
|
|
1041
|
+
if (isControlledTwoWayProp(tagName, name)) {
|
|
1042
|
+
if (name === "value") return tagName === "select" ? "select-value" : "text-value";
|
|
1043
|
+
return "checked";
|
|
1044
|
+
}
|
|
1045
|
+
return "prop";
|
|
1046
|
+
}
|
|
1047
|
+
function isTwoWayBindingKind(kind) {
|
|
1048
|
+
return kind === "select-value" || kind === "text-value" || kind === "checked";
|
|
1049
|
+
}
|
|
1050
|
+
const UNCONTROLLED_PROP_NAMES = {
|
|
1051
|
+
value: "defaultValue",
|
|
1052
|
+
checked: "defaultChecked"
|
|
1053
|
+
};
|
|
1054
|
+
const UNITLESS_CSS_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1055
|
+
"animationIterationCount",
|
|
1056
|
+
"aspectRatio",
|
|
1057
|
+
"borderImageOutset",
|
|
1058
|
+
"borderImageSlice",
|
|
1059
|
+
"borderImageWidth",
|
|
1060
|
+
"boxFlex",
|
|
1061
|
+
"boxFlexGroup",
|
|
1062
|
+
"boxOrdinalGroup",
|
|
1063
|
+
"columnCount",
|
|
1064
|
+
"columns",
|
|
1065
|
+
"flex",
|
|
1066
|
+
"flexGrow",
|
|
1067
|
+
"flexPositive",
|
|
1068
|
+
"flexShrink",
|
|
1069
|
+
"flexNegative",
|
|
1070
|
+
"flexOrder",
|
|
1071
|
+
"gridArea",
|
|
1072
|
+
"gridColumn",
|
|
1073
|
+
"gridColumnEnd",
|
|
1074
|
+
"gridColumnSpan",
|
|
1075
|
+
"gridColumnStart",
|
|
1076
|
+
"gridRow",
|
|
1077
|
+
"gridRowEnd",
|
|
1078
|
+
"gridRowSpan",
|
|
1079
|
+
"gridRowStart",
|
|
1080
|
+
"fontWeight",
|
|
1081
|
+
"lineClamp",
|
|
1082
|
+
"lineHeight",
|
|
1083
|
+
"opacity",
|
|
1084
|
+
"order",
|
|
1085
|
+
"orphans",
|
|
1086
|
+
"scale",
|
|
1087
|
+
"tabSize",
|
|
1088
|
+
"WebkitLineClamp",
|
|
1089
|
+
"widows",
|
|
1090
|
+
"zIndex",
|
|
1091
|
+
"zoom"
|
|
1092
|
+
]);
|
|
1093
|
+
const SVG_ELEMENTS = [
|
|
1094
|
+
"svg",
|
|
1095
|
+
"animate",
|
|
1096
|
+
"animateMotion",
|
|
1097
|
+
"animateTransform",
|
|
1098
|
+
"circle",
|
|
1099
|
+
"clipPath",
|
|
1100
|
+
"defs",
|
|
1101
|
+
"desc",
|
|
1102
|
+
"ellipse",
|
|
1103
|
+
"feBlend",
|
|
1104
|
+
"feColorMatrix",
|
|
1105
|
+
"feComponentTransfer",
|
|
1106
|
+
"feComposite",
|
|
1107
|
+
"feConvolveMatrix",
|
|
1108
|
+
"feDiffuseLighting",
|
|
1109
|
+
"feDisplacementMap",
|
|
1110
|
+
"feDistantLight",
|
|
1111
|
+
"feDropShadow",
|
|
1112
|
+
"feFlood",
|
|
1113
|
+
"feFuncA",
|
|
1114
|
+
"feFuncB",
|
|
1115
|
+
"feFuncG",
|
|
1116
|
+
"feFuncR",
|
|
1117
|
+
"feGaussianBlur",
|
|
1118
|
+
"feImage",
|
|
1119
|
+
"feMerge",
|
|
1120
|
+
"feMergeNode",
|
|
1121
|
+
"feMorphology",
|
|
1122
|
+
"feOffset",
|
|
1123
|
+
"fePointLight",
|
|
1124
|
+
"feSpecularLighting",
|
|
1125
|
+
"feSpotLight",
|
|
1126
|
+
"feTile",
|
|
1127
|
+
"feTurbulence",
|
|
1128
|
+
"filter",
|
|
1129
|
+
"foreignObject",
|
|
1130
|
+
"g",
|
|
1131
|
+
"image",
|
|
1132
|
+
"line",
|
|
1133
|
+
"linearGradient",
|
|
1134
|
+
"marker",
|
|
1135
|
+
"mask",
|
|
1136
|
+
"metadata",
|
|
1137
|
+
"mpath",
|
|
1138
|
+
"path",
|
|
1139
|
+
"pattern",
|
|
1140
|
+
"polygon",
|
|
1141
|
+
"polyline",
|
|
1142
|
+
"radialGradient",
|
|
1143
|
+
"rect",
|
|
1144
|
+
"set",
|
|
1145
|
+
"stop",
|
|
1146
|
+
"switch",
|
|
1147
|
+
"symbol",
|
|
1148
|
+
"text",
|
|
1149
|
+
"textPath",
|
|
1150
|
+
"tspan",
|
|
1151
|
+
"use",
|
|
1152
|
+
"view"
|
|
1153
|
+
];
|
|
1154
|
+
const MATHML_ELEMENTS = [
|
|
1155
|
+
"annotation",
|
|
1156
|
+
"annotation-xml",
|
|
1157
|
+
"maction",
|
|
1158
|
+
"math",
|
|
1159
|
+
"merror",
|
|
1160
|
+
"mfrac",
|
|
1161
|
+
"mi",
|
|
1162
|
+
"mmultiscripts",
|
|
1163
|
+
"mn",
|
|
1164
|
+
"mo",
|
|
1165
|
+
"mover",
|
|
1166
|
+
"mpadded",
|
|
1167
|
+
"mphantom",
|
|
1168
|
+
"mprescripts",
|
|
1169
|
+
"mroot",
|
|
1170
|
+
"mrow",
|
|
1171
|
+
"ms",
|
|
1172
|
+
"mspace",
|
|
1173
|
+
"msqrt",
|
|
1174
|
+
"mstyle",
|
|
1175
|
+
"msub",
|
|
1176
|
+
"msubsup",
|
|
1177
|
+
"msup",
|
|
1178
|
+
"mtable",
|
|
1179
|
+
"mtd",
|
|
1180
|
+
"mtext",
|
|
1181
|
+
"mtr",
|
|
1182
|
+
"munder",
|
|
1183
|
+
"munderover",
|
|
1184
|
+
"semantics"
|
|
1185
|
+
];
|
|
1186
|
+
const NON_HTML_HOST_ELEMENTS = /* @__PURE__ */ (() => {
|
|
1187
|
+
const tags = new Set(SVG_ELEMENTS);
|
|
1188
|
+
for (const tag of MATHML_ELEMENTS) tags.add(tag);
|
|
1189
|
+
return tags;
|
|
1190
|
+
})();
|
|
1191
|
+
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
1192
|
+
/**
|
|
1193
|
+
* A leaf that lets React own reconciliation while its signal dependency stays
|
|
1194
|
+
* local to the leaf. A signal resolving to another signal/array is normalized
|
|
1195
|
+
* again so nested reactive children also work.
|
|
1196
|
+
*/
|
|
1197
|
+
function SignalValue({ source }) {
|
|
1198
|
+
return normalizeChild(useSignalValue(source));
|
|
1199
|
+
}
|
|
1200
|
+
function normalizeChild(value) {
|
|
1201
|
+
if (isSignal(value)) return createElement(SignalValue, { source: value });
|
|
1202
|
+
if (Array.isArray(value)) return value.map(normalizeArrayItem);
|
|
1203
|
+
return value;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* A signal inside an array child has no way to carry a key of its own, so the
|
|
1207
|
+
* leaf it becomes is keyed by position. That is the identity such an array
|
|
1208
|
+
* already has (a literal `[a, b]` never reorders), and it keeps React's
|
|
1209
|
+
* missing-key warning for genuinely dynamic element lists intact. The prefix
|
|
1210
|
+
* keeps these keys out of the way of keys on the array's other elements.
|
|
1211
|
+
*/
|
|
1212
|
+
function normalizeArrayItem(value, index) {
|
|
1213
|
+
if (isSignal(value)) return createElement(SignalValue, {
|
|
1214
|
+
source: value,
|
|
1215
|
+
key: `rfgs-signal:${index}`
|
|
1216
|
+
});
|
|
1217
|
+
return normalizeChild(value);
|
|
1218
|
+
}
|
|
1219
|
+
function isReactiveHostProp(name, value) {
|
|
1220
|
+
if (!isSignal(value)) return false;
|
|
1221
|
+
return REACTIVE_PROP_NAMES.has(name) || name.startsWith("data-") || name.startsWith("aria-");
|
|
1222
|
+
}
|
|
1223
|
+
function readInitialValue(source, kind) {
|
|
1224
|
+
const value = source.peek();
|
|
1225
|
+
if (kind === "style" && typeof value === "object" && value !== null && !Array.isArray(value)) return { ...value };
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Reads a signal on behalf of one of this module's direct DOM bindings (see
|
|
1230
|
+
* the module doc: these write straight to the DOM from a subscription mounted
|
|
1231
|
+
* alongside the element's ref, bypassing React's render, so the owning
|
|
1232
|
+
* component never re-renders). A `computed()` whose getter throws caches and
|
|
1233
|
+
* rethrows that error on every read (see `updateComputed` in
|
|
1234
|
+
* src/core/alien-derived-runtime-core.mts) —
|
|
1235
|
+
* if `source` is such a computed and it starts failing after the binding is
|
|
1236
|
+
* already mounted, an unguarded read here could throw from the subscription's
|
|
1237
|
+
* update callback while processing the write. The direct V1 watcher contains
|
|
1238
|
+
* computed dirty-check errors, but this binding read is a separate operation;
|
|
1239
|
+
* catching it here also keeps the structural-readable effect fallback local.
|
|
1240
|
+
*
|
|
1241
|
+
* Catching here keeps a failure local to this one binding: the DOM write for
|
|
1242
|
+
* this cycle is skipped (the DOM is left at its last successful value) and
|
|
1243
|
+
* the failure is reported with `console.error(message, { cause: error })` —
|
|
1244
|
+
* assert against `mock.calls[i][1].cause` in tests, the same shape used by
|
|
1245
|
+
* `effect()`'s failure report (`reportFailure` in
|
|
1246
|
+
* src/core/alien-derived-runtime-core.mts) and by `render-tracking.ts`. Reporting is intentional here too: a direct binding
|
|
1247
|
+
* has no Error Boundary or other surface to fall back on, and silence would
|
|
1248
|
+
* mean a binding that mysteriously stops updating with zero trace.
|
|
1249
|
+
*
|
|
1250
|
+
* Reported at most once per contiguous run of failures ("episode"): `episode`
|
|
1251
|
+
* latches after the first report and is cleared the moment a read next
|
|
1252
|
+
* succeeds, so a later, distinct failure episode logs again. This keeps a
|
|
1253
|
+
* `computed()` that fails on every keystroke from spamming one `console.error`
|
|
1254
|
+
* per keystroke.
|
|
1255
|
+
*/
|
|
1256
|
+
function readBoundSignal(read, episode) {
|
|
1257
|
+
try {
|
|
1258
|
+
const value = read();
|
|
1259
|
+
episode.hasReported = false;
|
|
1260
|
+
return {
|
|
1261
|
+
ok: true,
|
|
1262
|
+
value
|
|
1263
|
+
};
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
if (!episode.hasReported) {
|
|
1266
|
+
episode.hasReported = true;
|
|
1267
|
+
console.error("react-fine-grained-signals: a direct signal binding's read threw; skipping this update and leaving the DOM at its last value.", { cause: error });
|
|
1268
|
+
}
|
|
1269
|
+
return { ok: false };
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Reads via `readBoundSignal` and, on success, hands the value to `apply`; a
|
|
1274
|
+
* failed read is already reported by `readBoundSignal`, so this just skips
|
|
1275
|
+
* the write. Shared by every read-and-apply call site below, including the
|
|
1276
|
+
* one that isn't itself a subscription callback — the MutationObserver-triggered
|
|
1277
|
+
* re-apply in `bindSelectValue`, which reads outside the reactive graph via
|
|
1278
|
+
* `.peek()`.
|
|
1279
|
+
*/
|
|
1280
|
+
function applyBoundSignal(read, apply, episode) {
|
|
1281
|
+
const result = readBoundSignal(read, episode);
|
|
1282
|
+
if (result.ok) apply(result.value);
|
|
1283
|
+
}
|
|
1284
|
+
function createBindingSubscription(source, apply, episode = { hasReported: false }) {
|
|
1285
|
+
const update = () => applyBoundSignal(() => source.value, apply, episode);
|
|
1286
|
+
const refresh = () => untracked(update);
|
|
1287
|
+
const unsubscribe = subscribeReadableV1(source, update);
|
|
1288
|
+
if (unsubscribe !== void 0) {
|
|
1289
|
+
untracked(() => notifyListener(update));
|
|
1290
|
+
return {
|
|
1291
|
+
dispose: unsubscribe,
|
|
1292
|
+
refresh
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
dispose: detachedEffect(update),
|
|
1297
|
+
refresh
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function setAttribute(node, name, value) {
|
|
1301
|
+
if (value == null) node.removeAttribute(name);
|
|
1302
|
+
else node.setAttribute(name, String(value));
|
|
1303
|
+
}
|
|
1304
|
+
function setDomProp(node, name, value) {
|
|
1305
|
+
if (node.namespaceURI !== HTML_NAMESPACE) {
|
|
1306
|
+
setAttribute(node, name === "className" ? "class" : name, value);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
switch (name) {
|
|
1310
|
+
case "title":
|
|
1311
|
+
node.title = value == null ? "" : String(value);
|
|
1312
|
+
return;
|
|
1313
|
+
case "id":
|
|
1314
|
+
node.id = value == null ? "" : String(value);
|
|
1315
|
+
return;
|
|
1316
|
+
case "className":
|
|
1317
|
+
node.className = value == null ? "" : String(value);
|
|
1318
|
+
return;
|
|
1319
|
+
case "hidden":
|
|
1320
|
+
if (value === "until-found") setAttribute(node, "hidden", value);
|
|
1321
|
+
else node.hidden = Boolean(value);
|
|
1322
|
+
return;
|
|
1323
|
+
case "disabled":
|
|
1324
|
+
if ("disabled" in node) node.disabled = Boolean(value);
|
|
1325
|
+
else if (value == null || value === false) node.removeAttribute(name);
|
|
1326
|
+
else setAttribute(node, name, value);
|
|
1327
|
+
return;
|
|
1328
|
+
default: setAttribute(node, name, value);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Writes `value`/`checked` on a controlled-two-way element, skipping the DOM
|
|
1333
|
+
* write when it already holds the value being written. A binding update
|
|
1334
|
+
* follows signal writes, including keystrokes written by `onChange`, so without
|
|
1335
|
+
* this guard every keystroke would re-set a property the DOM already has,
|
|
1336
|
+
* moving the caret and disrupting IME composition for no reason.
|
|
1337
|
+
*/
|
|
1338
|
+
function setControlledProp(node, name, value) {
|
|
1339
|
+
if (name === "value") {
|
|
1340
|
+
const select = node;
|
|
1341
|
+
if (select.tagName === "SELECT") {
|
|
1342
|
+
setSelectValue(select, value);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const next = value == null ? "" : String(value);
|
|
1346
|
+
const input = node;
|
|
1347
|
+
if (input.value !== next) input.value = next;
|
|
1348
|
+
if (input.defaultValue !== next) input.defaultValue = next;
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const next = Boolean(value);
|
|
1352
|
+
const input = node;
|
|
1353
|
+
if (input.checked !== next) input.checked = next;
|
|
1354
|
+
if (input.defaultChecked !== next) input.defaultChecked = next;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* `<select multiple>` does not support assigning `.value` directly — the DOM
|
|
1358
|
+
* setter only ever selects a single option, silently corrupting the
|
|
1359
|
+
* selection instead of erroring. Its documented API for a multi-value select
|
|
1360
|
+
* is per-`<option>` `.selected`, so an array-valued signal (React's own
|
|
1361
|
+
* typing for a multi-select) is applied that way instead.
|
|
1362
|
+
*/
|
|
1363
|
+
function setSelectValue(select, value) {
|
|
1364
|
+
const multiple = select.multiple;
|
|
1365
|
+
const values = new Set(Array.isArray(value) && multiple ? value.map(String) : value == null ? multiple ? [] : [""] : [String(value)]);
|
|
1366
|
+
if (!multiple) {
|
|
1367
|
+
const next = [...values][0];
|
|
1368
|
+
if (select.value !== next) select.value = next;
|
|
1369
|
+
}
|
|
1370
|
+
for (const option of select.options) {
|
|
1371
|
+
const selected = values.has(option.value);
|
|
1372
|
+
if (multiple && option.selected !== selected) option.selected = selected;
|
|
1373
|
+
if (option.defaultSelected !== selected) option.defaultSelected = selected;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* A `<select>` whose matching `<option>` does not exist yet (for example when
|
|
1378
|
+
* the options themselves are rendered from a signal or other state, mounted
|
|
1379
|
+
* after this one) silently ends up with nothing selected: the DOM neither
|
|
1380
|
+
* errors nor retroactively applies `.value`/`.selected` once a matching
|
|
1381
|
+
* `<option>` is later added. The value subscription below only updates when
|
|
1382
|
+
* the bound signal itself changes, so it cannot see that the option list changed
|
|
1383
|
+
* out from under it. A MutationObserver on the select's subtree re-applies
|
|
1384
|
+
* the signal's current value whenever its `<option>` list changes, closing
|
|
1385
|
+
* that gap without requiring the signal to change too.
|
|
1386
|
+
*/
|
|
1387
|
+
function bindSelectValue(select, source, apply) {
|
|
1388
|
+
const episode = { hasReported: false };
|
|
1389
|
+
const subscription = createBindingSubscription(source, apply, episode);
|
|
1390
|
+
const observer = new MutationObserver(() => {
|
|
1391
|
+
applyBoundSignal(() => source.peek(), apply, episode);
|
|
1392
|
+
});
|
|
1393
|
+
observer.observe(select, {
|
|
1394
|
+
childList: true,
|
|
1395
|
+
subtree: true
|
|
1396
|
+
});
|
|
1397
|
+
return {
|
|
1398
|
+
dispose() {
|
|
1399
|
+
subscription.dispose();
|
|
1400
|
+
observer.disconnect();
|
|
1401
|
+
},
|
|
1402
|
+
refresh: subscription.refresh
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Forcing a `value` write while an IME composition is in progress can abort
|
|
1407
|
+
* the composition outright, independent of whether the string being written
|
|
1408
|
+
* happens to match what was typed — the write-skip guard in
|
|
1409
|
+
* `setControlledProp` only protects the same-value echo case, not this one.
|
|
1410
|
+
* `compositionstart`/`compositionend` listeners on the node itself track
|
|
1411
|
+
* composition state directly, regardless of whether the component declares
|
|
1412
|
+
* its own composition handlers, and a write requested while composing is
|
|
1413
|
+
* deferred until composition ends instead of applied immediately.
|
|
1414
|
+
*/
|
|
1415
|
+
function bindTextValue(node, source, write) {
|
|
1416
|
+
let composing = false;
|
|
1417
|
+
let hasPending = false;
|
|
1418
|
+
let pending;
|
|
1419
|
+
const episode = { hasReported: false };
|
|
1420
|
+
const onCompositionStart = () => {
|
|
1421
|
+
composing = true;
|
|
1422
|
+
};
|
|
1423
|
+
const onCompositionEnd = () => {
|
|
1424
|
+
composing = false;
|
|
1425
|
+
if (hasPending) {
|
|
1426
|
+
hasPending = false;
|
|
1427
|
+
write(pending);
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
node.addEventListener("compositionstart", onCompositionStart);
|
|
1431
|
+
node.addEventListener("compositionend", onCompositionEnd);
|
|
1432
|
+
const subscription = createBindingSubscription(source, (next) => {
|
|
1433
|
+
if (composing) {
|
|
1434
|
+
hasPending = true;
|
|
1435
|
+
pending = next;
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
write(next);
|
|
1439
|
+
}, episode);
|
|
1440
|
+
return {
|
|
1441
|
+
dispose() {
|
|
1442
|
+
subscription.dispose();
|
|
1443
|
+
node.removeEventListener("compositionstart", onCompositionStart);
|
|
1444
|
+
node.removeEventListener("compositionend", onCompositionEnd);
|
|
1445
|
+
},
|
|
1446
|
+
refresh: subscription.refresh
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
function clearStyleProperty(style, key) {
|
|
1450
|
+
if (key.startsWith("--")) style.removeProperty(key);
|
|
1451
|
+
else style[key] = "";
|
|
1452
|
+
}
|
|
1453
|
+
function setStyleProperty(style, key, value) {
|
|
1454
|
+
if (value == null) {
|
|
1455
|
+
clearStyleProperty(style, key);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (key.startsWith("--")) {
|
|
1459
|
+
style.setProperty(key, String(value));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
style[key] = typeof value === "number" && !UNITLESS_CSS_PROPERTIES.has(key) ? `${value}px` : String(value);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Applies a whole style object to an element, clearing keys that were present
|
|
1466
|
+
* in a previous call but are absent from this one. Only the coarse
|
|
1467
|
+
* `style={signal}` form is bound this way — an object whose individual entries
|
|
1468
|
+
* are themselves signals is out of scope (see development/design/direct-binding-value-checked-style.md).
|
|
1469
|
+
*/
|
|
1470
|
+
function applyStyle(node, value, previousKeys) {
|
|
1471
|
+
const nextStyle = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1472
|
+
const nextKeys = Object.keys(nextStyle);
|
|
1473
|
+
const nextKeySet = new Set(nextKeys);
|
|
1474
|
+
for (const key of previousKeys) if (!nextKeySet.has(key)) clearStyleProperty(node.style, key);
|
|
1475
|
+
for (const key of nextKeys) setStyleProperty(node.style, key, nextStyle[key]);
|
|
1476
|
+
return nextKeys;
|
|
1477
|
+
}
|
|
1478
|
+
function applyRef(ref, node) {
|
|
1479
|
+
if (typeof ref === "function") return ref(node);
|
|
1480
|
+
if (ref != null) ref.current = node;
|
|
1481
|
+
}
|
|
1482
|
+
/** A binding's identity for the re-render diff: same name, source, and kind. */
|
|
1483
|
+
function isSameBinding(a, b) {
|
|
1484
|
+
return b !== void 0 && a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
1485
|
+
}
|
|
1486
|
+
/** Whether two style values set the same properties to the same values. */
|
|
1487
|
+
function isSameStyle(a, b) {
|
|
1488
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return Object.is(a, b);
|
|
1489
|
+
const keys = Object.keys(a);
|
|
1490
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.is(a[key], b[key]));
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Whether React was handed the same value for this binding's prop. A two-way
|
|
1494
|
+
* binding hands React an uncontrolled default that it reads only at mount, so
|
|
1495
|
+
* only `"prop"` and `"style"` snapshots count; a style snapshot is a fresh copy
|
|
1496
|
+
* every render (see `readInitialValue`), so it is compared by content.
|
|
1497
|
+
*/
|
|
1498
|
+
function hasSameSnapshot(a, b) {
|
|
1499
|
+
return isTwoWayBindingKind(a[2]) || (a[2] === "style" ? isSameStyle(a[3], b[3]) : Object.is(a[3], b[3]));
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Subscribes a single binding to `node` and returns its teardown. Split out of
|
|
1503
|
+
* the ref callback so `NodeBinder` can rebuild one binding on
|
|
1504
|
+
* its own, without disturbing the siblings that did not change.
|
|
1505
|
+
*
|
|
1506
|
+
* `initialStyleKeys` seeds a `"style"` binding's own `previousKeys` (see
|
|
1507
|
+
* `getInitialStyleKeys`), so the fresh binding starts already knowing which
|
|
1508
|
+
* CSS properties may be on the node, instead of starting from an empty set
|
|
1509
|
+
* and never clearing them. Ignored for every other kind.
|
|
1510
|
+
*/
|
|
1511
|
+
function subscribeBinding(node, name, source, kind, initialStyleKeys, binder) {
|
|
1512
|
+
const whileAttached = (write) => (value) => {
|
|
1513
|
+
if (binder.attached) write(value);
|
|
1514
|
+
};
|
|
1515
|
+
switch (kind) {
|
|
1516
|
+
case "style": {
|
|
1517
|
+
let previousKeys = initialStyleKeys ?? [];
|
|
1518
|
+
let applied;
|
|
1519
|
+
const write = (value) => {
|
|
1520
|
+
applied = value;
|
|
1521
|
+
previousKeys = applyStyle(node, value, previousKeys);
|
|
1522
|
+
};
|
|
1523
|
+
const subscription = createBindingSubscription(source, whileAttached(write));
|
|
1524
|
+
return {
|
|
1525
|
+
...subscription,
|
|
1526
|
+
refresh: (snapshot) => {
|
|
1527
|
+
previousKeys = getInitialStyleKeys(previousKeys, snapshot);
|
|
1528
|
+
subscription.refresh();
|
|
1529
|
+
},
|
|
1530
|
+
getStyleKeys: () => previousKeys,
|
|
1531
|
+
restore: (snapshot) => {
|
|
1532
|
+
if (!isSameStyle(applied, snapshot)) write(snapshot);
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
case "select-value": return {
|
|
1537
|
+
...bindSelectValue(node, source, whileAttached((value) => setControlledProp(node, "value", value))),
|
|
1538
|
+
getStyleKeys: void 0,
|
|
1539
|
+
restore: void 0
|
|
1540
|
+
};
|
|
1541
|
+
case "text-value": return {
|
|
1542
|
+
...bindTextValue(node, source, whileAttached((value) => setControlledProp(node, "value", value))),
|
|
1543
|
+
getStyleKeys: void 0,
|
|
1544
|
+
restore: void 0
|
|
1545
|
+
};
|
|
1546
|
+
case "checked": return {
|
|
1547
|
+
...createBindingSubscription(source, whileAttached((value) => setControlledProp(node, name, value))),
|
|
1548
|
+
getStyleKeys: void 0,
|
|
1549
|
+
restore: void 0
|
|
1550
|
+
};
|
|
1551
|
+
case "prop": {
|
|
1552
|
+
let applied;
|
|
1553
|
+
const write = (value) => {
|
|
1554
|
+
applied = value;
|
|
1555
|
+
setDomProp(node, name, value);
|
|
1556
|
+
};
|
|
1557
|
+
return {
|
|
1558
|
+
...createBindingSubscription(source, whileAttached(write)),
|
|
1559
|
+
getStyleKeys: void 0,
|
|
1560
|
+
restore: (snapshot) => {
|
|
1561
|
+
if (!Object.is(applied, snapshot)) write(snapshot);
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* The CSS properties a new `"style"` binding may find on the node: React wrote
|
|
1569
|
+
* this binding's render-time snapshot, and a replaced binding (`staleKeys`) may
|
|
1570
|
+
* have written keys React never saw. The signal can drop any of them before the
|
|
1571
|
+
* binding attaches (a write between render and commit, an Activity or Suspense
|
|
1572
|
+
* reveal), so the first write must be able to clear all of them. A kept
|
|
1573
|
+
* binding re-attached for a newer render widens its own keys the same way.
|
|
1574
|
+
*/
|
|
1575
|
+
function getInitialStyleKeys(staleKeys, snapshot) {
|
|
1576
|
+
const keys = typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? Object.keys(snapshot) : [];
|
|
1577
|
+
if (staleKeys === void 0) return keys;
|
|
1578
|
+
for (const key of staleKeys) if (!keys.includes(key)) keys.push(key);
|
|
1579
|
+
return keys;
|
|
1580
|
+
}
|
|
1581
|
+
function mountBinding(node, binding, binder, staleStyleKeys) {
|
|
1582
|
+
const initialStyleKeys = binding[2] === "style" ? getInitialStyleKeys(staleStyleKeys, binding[3]) : void 0;
|
|
1583
|
+
const { dispose, refresh, getStyleKeys, restore } = subscribeBinding(node, binding[0], binding[1], binding[2], initialStyleKeys, binder);
|
|
1584
|
+
return {
|
|
1585
|
+
binding,
|
|
1586
|
+
dispose,
|
|
1587
|
+
refresh,
|
|
1588
|
+
getStyleKeys,
|
|
1589
|
+
restore
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Owns the direct bindings mounted on one DOM node.
|
|
1594
|
+
*
|
|
1595
|
+
* Bindings are attached through a callback ref on the host element itself
|
|
1596
|
+
* rather than through a wrapper component. A wrapper would change the element
|
|
1597
|
+
* type whenever a prop switched between a signal and a plain value, and React
|
|
1598
|
+
* remounts a subtree whose element type changes, losing descendant state and
|
|
1599
|
+
* focus. With the ref, the element stays the same host type in both cases.
|
|
1600
|
+
*
|
|
1601
|
+
* The binder is keyed by the node, not by the ref callback, because React
|
|
1602
|
+
* replaces a callback ref whose identity changed by detaching the old one and
|
|
1603
|
+
* attaching the new one to the same node within a single commit. Tearing the
|
|
1604
|
+
* bindings down on that detach would recreate every subscription — and reset
|
|
1605
|
+
* `bindTextValue`'s in-flight IME composition state — on unrelated re-renders.
|
|
1606
|
+
* So a detach only schedules the teardown, and a re-attach of the same node in
|
|
1607
|
+
* the same commit reconciles the bindings in place instead (`syncBindings`
|
|
1608
|
+
* diffs by `(name, source, kind)`). A detach that is not followed by an attach
|
|
1609
|
+
* (unmount, a hidden Activity or Suspense boundary) tears down in a microtask.
|
|
1610
|
+
*
|
|
1611
|
+
* The user's own ref is forwarded synchronously on every attach and detach,
|
|
1612
|
+
* which is exactly what React would do with it directly; `getBindingRef` keeps
|
|
1613
|
+
* the callback's identity stable whenever the inputs are, so a stable user ref
|
|
1614
|
+
* is not churned.
|
|
1615
|
+
*/
|
|
1616
|
+
var NodeBinder = class {
|
|
1617
|
+
#node;
|
|
1618
|
+
#mounted = [];
|
|
1619
|
+
#token = 0;
|
|
1620
|
+
#detached = false;
|
|
1621
|
+
constructor(node) {
|
|
1622
|
+
this.#node = node;
|
|
1623
|
+
}
|
|
1624
|
+
/** Whether React currently has this node attached through a binding ref. */
|
|
1625
|
+
get attached() {
|
|
1626
|
+
return !this.#detached;
|
|
1627
|
+
}
|
|
1628
|
+
attach(bindings, userRef) {
|
|
1629
|
+
const token = ++this.#token;
|
|
1630
|
+
const wasDetached = this.#detached;
|
|
1631
|
+
this.#detached = false;
|
|
1632
|
+
if (wasDetached) for (const entry of this.#mounted) {
|
|
1633
|
+
const next = bindings.find((binding) => binding[0] === entry.binding[0]);
|
|
1634
|
+
if (isSameBinding(entry.binding, next)) entry.refresh(next[3]);
|
|
1635
|
+
}
|
|
1636
|
+
this.#sync(bindings);
|
|
1637
|
+
let userCleanup;
|
|
1638
|
+
try {
|
|
1639
|
+
userCleanup = applyRef(userRef, this.#node);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
this.#detach(token, bindings);
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
return () => {
|
|
1645
|
+
try {
|
|
1646
|
+
if (typeof userCleanup === "function") userCleanup();
|
|
1647
|
+
else applyRef(userRef, null);
|
|
1648
|
+
} finally {
|
|
1649
|
+
this.#detach(token, bindings);
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Hands the node back to React for the attach identified by `token`: writes
|
|
1655
|
+
* are held back from now on, and the bindings are disposed in a microtask
|
|
1656
|
+
* unless the same node is attached again first (see the class doc).
|
|
1657
|
+
*/
|
|
1658
|
+
#detach(token, bindings) {
|
|
1659
|
+
if (token !== this.#token) return;
|
|
1660
|
+
this.#detached = true;
|
|
1661
|
+
this.#mounted.forEach((entry, index) => entry.restore?.(bindings[index][3]));
|
|
1662
|
+
queueMicrotask(() => {
|
|
1663
|
+
if (this.#detached && token === this.#token) this.#dispose();
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
#dispose() {
|
|
1667
|
+
const mounted = this.#mounted;
|
|
1668
|
+
this.#mounted = [];
|
|
1669
|
+
for (const binding of mounted) binding.dispose();
|
|
1670
|
+
}
|
|
1671
|
+
#sync(bindings) {
|
|
1672
|
+
const mounted = this.#mounted;
|
|
1673
|
+
if (mounted.length === bindings.length && mounted.every((entry, index) => isSameBinding(entry.binding, bindings[index]))) return;
|
|
1674
|
+
const reusable = /* @__PURE__ */ new Map();
|
|
1675
|
+
for (const entry of mounted) reusable.set(entry.binding[0], entry);
|
|
1676
|
+
const reused = bindings.map((binding) => {
|
|
1677
|
+
const candidate = reusable.get(binding[0]);
|
|
1678
|
+
if (candidate === void 0 || !isSameBinding(candidate.binding, binding)) return void 0;
|
|
1679
|
+
reusable.delete(binding[0]);
|
|
1680
|
+
return candidate;
|
|
1681
|
+
});
|
|
1682
|
+
const staleStyleKeys = /* @__PURE__ */ new Map();
|
|
1683
|
+
for (const stale of reusable.values()) if (stale.getStyleKeys !== void 0) staleStyleKeys.set(stale.binding[0], stale.getStyleKeys());
|
|
1684
|
+
for (const stale of reusable.values()) stale.dispose();
|
|
1685
|
+
const node = this.#node;
|
|
1686
|
+
this.#mounted = bindings.map((binding, index) => reused[index] ?? mountBinding(node, binding, this, staleStyleKeys.get(binding[0])));
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
const nodeBinders = /* @__PURE__ */ new WeakMap();
|
|
1690
|
+
function createBindingRef(bindings, userRef) {
|
|
1691
|
+
return (node) => {
|
|
1692
|
+
if (node == null) return;
|
|
1693
|
+
let binder = nodeBinders.get(node);
|
|
1694
|
+
if (binder === void 0) {
|
|
1695
|
+
binder = new NodeBinder(node);
|
|
1696
|
+
nodeBinders.set(node, binder);
|
|
1697
|
+
}
|
|
1698
|
+
return binder.attach(bindings, userRef);
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
const bindingRefs = /* @__PURE__ */ new WeakMap();
|
|
1702
|
+
const BINDING_REF_CACHE_LIMIT = 8;
|
|
1703
|
+
function haveSameBindings(a, b) {
|
|
1704
|
+
return a.length === b.length && a.every((binding, index) => isSameBinding(binding, b[index]) && hasSameSnapshot(binding, b[index]));
|
|
1705
|
+
}
|
|
1706
|
+
function getBindingRef(bindings, userRef) {
|
|
1707
|
+
const cacheKey = userRef ?? bindings[0][1];
|
|
1708
|
+
const cache = bindingRefs;
|
|
1709
|
+
let entries = cache.get(cacheKey);
|
|
1710
|
+
if (entries !== void 0) {
|
|
1711
|
+
for (const entry of entries) if (entry.userRef === userRef && haveSameBindings(entry.bindings, bindings)) return entry.ref;
|
|
1712
|
+
} else {
|
|
1713
|
+
entries = [];
|
|
1714
|
+
cache.set(cacheKey, entries);
|
|
1715
|
+
}
|
|
1716
|
+
const ref = createBindingRef(bindings, userRef);
|
|
1717
|
+
if (entries.length >= BINDING_REF_CACHE_LIMIT) entries.shift();
|
|
1718
|
+
entries.push({
|
|
1719
|
+
bindings,
|
|
1720
|
+
userRef,
|
|
1721
|
+
ref
|
|
1722
|
+
});
|
|
1723
|
+
return ref;
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Scans a native host element's props for anything reactive (see
|
|
1727
|
+
* `isReactiveHostProp`). Kept separate from building the transformed props
|
|
1728
|
+
* copy in `transformHostProps` so that function can decide whether a copy is
|
|
1729
|
+
* needed at all before ever allocating one.
|
|
1730
|
+
*/
|
|
1731
|
+
function findHostBindings(props, tagName) {
|
|
1732
|
+
const bindings = [];
|
|
1733
|
+
for (const [name, value] of Object.entries(props)) if (isReactiveHostProp(name, value)) {
|
|
1734
|
+
const kind = resolveBindingKind(tagName, name);
|
|
1735
|
+
bindings.push([
|
|
1736
|
+
name,
|
|
1737
|
+
value,
|
|
1738
|
+
kind,
|
|
1739
|
+
readInitialValue(value, kind)
|
|
1740
|
+
]);
|
|
1741
|
+
}
|
|
1742
|
+
return bindings;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Transforms a native host element's (`<div>`, `<svg>`, ...) props: replaces
|
|
1746
|
+
* any directly-bound prop with its initial, non-reactive value (`bindings`
|
|
1747
|
+
* carries the reactive ones for the binding ref to mount later — see
|
|
1748
|
+
* `createJsxWrapper`) and normalizes a signal/array `children` the same way
|
|
1749
|
+
* `Fragment` does.
|
|
1750
|
+
*
|
|
1751
|
+
* The overwhelmingly common host element has neither. Scanning first and only
|
|
1752
|
+
* allocating a copy when something is actually found means a plain
|
|
1753
|
+
* `<div className="card">…</div>` costs one prop walk and zero extra
|
|
1754
|
+
* allocations — the original props object is returned as-is (see the
|
|
1755
|
+
* `bindings.length === 0 && !childrenNeedNormalization` branch below), and
|
|
1756
|
+
* `createJsxWrapper` passes it straight to `factory` unchanged.
|
|
1757
|
+
*/
|
|
1758
|
+
function transformHostProps(type, input) {
|
|
1759
|
+
const inputProps = input ?? {};
|
|
1760
|
+
const bindings = !NON_HTML_HOST_ELEMENTS.has(type) ? findHostBindings(inputProps, type) : [];
|
|
1761
|
+
const rawChildren = inputProps.children;
|
|
1762
|
+
const childrenNeedNormalization = "children" in inputProps && (isSignal(rawChildren) || Array.isArray(rawChildren));
|
|
1763
|
+
if (bindings.length === 0 && !childrenNeedNormalization) return {
|
|
1764
|
+
props: inputProps,
|
|
1765
|
+
bindings
|
|
1766
|
+
};
|
|
1767
|
+
const props = { ...inputProps };
|
|
1768
|
+
if (childrenNeedNormalization) props.children = normalizeChild(rawChildren);
|
|
1769
|
+
for (const [name, , kind, snapshot] of bindings) {
|
|
1770
|
+
const uncontrolledName = isTwoWayBindingKind(kind) ? UNCONTROLLED_PROP_NAMES[name] : void 0;
|
|
1771
|
+
if (uncontrolledName !== void 0) {
|
|
1772
|
+
delete props[name];
|
|
1773
|
+
props[uncontrolledName] = snapshot;
|
|
1774
|
+
} else props[name] = snapshot;
|
|
1775
|
+
}
|
|
1776
|
+
return {
|
|
1777
|
+
props,
|
|
1778
|
+
bindings
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* `Fragment`'s only prop worth transforming is `children`, and only when it
|
|
1783
|
+
* is itself a signal or array — `normalizeChild` is the identity for
|
|
1784
|
+
* anything else (see its definition) — so a plain `<>...</>` with ordinary
|
|
1785
|
+
* children is passed straight through, uncopied, the same way a custom
|
|
1786
|
+
* component is below.
|
|
1787
|
+
*/
|
|
1788
|
+
function transformFragmentProps(input) {
|
|
1789
|
+
const props = input;
|
|
1790
|
+
if (props == null || !("children" in props) || !isSignal(props.children) && !Array.isArray(props.children)) return input;
|
|
1791
|
+
return {
|
|
1792
|
+
...props,
|
|
1793
|
+
children: normalizeChild(props.children)
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* A host element's final props: the transformed copy with the binding ref in
|
|
1798
|
+
* place of the user's own ref, which the binding ref forwards to. `props` is
|
|
1799
|
+
* always a fresh copy whenever there are bindings (see `transformHostProps`).
|
|
1800
|
+
*/
|
|
1801
|
+
function withBindingRef(props, bindings) {
|
|
1802
|
+
if (bindings.length > 0) props.ref = getBindingRef(bindings, props.ref);
|
|
1803
|
+
return props;
|
|
1804
|
+
}
|
|
1805
|
+
/** Creates a JSX wrapper while letting each module supply React's JSX factory. */
|
|
1806
|
+
function createJsxWrapper(factory) {
|
|
1807
|
+
return (type, input, key) => {
|
|
1808
|
+
if (typeof type === "string") {
|
|
1809
|
+
const { props, bindings } = transformHostProps(type, input);
|
|
1810
|
+
return factory(type, withBindingRef(props, bindings), key);
|
|
1811
|
+
}
|
|
1812
|
+
if (type === Fragment) return factory(type, transformFragmentProps(input), key);
|
|
1813
|
+
return factory(type, input, key);
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* The classic-runtime `createElement` that the automatic runtime falls back to
|
|
1818
|
+
* for `<div {...props} key="k" />` — TypeScript, Babel and Oxc all import it
|
|
1819
|
+
* from the package root of `jsxImportSource`. It applies the same host-prop
|
|
1820
|
+
* bindings and child normalization as `jsx`, and leaves `key` (and `ref`) in
|
|
1821
|
+
* the config for React's own `createElement` to extract.
|
|
1822
|
+
*/
|
|
1823
|
+
function createSignalAwareElement(type, config, ...children) {
|
|
1824
|
+
if (typeof type !== "string" && type !== Fragment) return createElement(type, config, ...children);
|
|
1825
|
+
const normalizedChildren = children.map((child) => normalizeChild(child));
|
|
1826
|
+
if (typeof type !== "string") return createElement(type, config, ...normalizedChildren);
|
|
1827
|
+
const { props, bindings } = transformHostProps(type, config ?? {});
|
|
1828
|
+
return createElement(type, withBindingRef(props, bindings), ...normalizedChildren);
|
|
1829
|
+
}
|
|
1830
|
+
//#endregion
|
|
1831
|
+
export { useDeepSignalValue as a, useSignalValue as c, useDeepSignal as i, deepSignal as l, createSignalAwareElement as n, useSignal as o, useComputed as r, useSignalEffect as s, createJsxWrapper as t };
|
|
1832
|
+
|
|
1833
|
+
//# sourceMappingURL=jsx-P6volZdr.js.map
|