@govela/runtime 0.0.0-stage → 0.3.1
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 +21 -0
- package/README.md +17 -2
- package/package.json +19 -4
- package/runtime-blocks.js +875 -0
- package/runtime-hydrate.js +415 -0
- package/runtime.js +562 -0
package/runtime.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vela Runtime
|
|
3
|
+
* The only JS that ships to the browser — everything else is compiled away.
|
|
4
|
+
* ~120 lines. No dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────
|
|
8
|
+
// Batch update mechanism
|
|
9
|
+
//
|
|
10
|
+
// When a store fires (set/update), all subscribers run inside _batchRun().
|
|
11
|
+
// Generated component setters call $$defer(fn) for DOM-touching work.
|
|
12
|
+
// $$defer pushes fn to the shared queue. Two flush paths drain it:
|
|
13
|
+
//
|
|
14
|
+
// 1. _batchRun() — when explicit batching is in progress (depth > 0),
|
|
15
|
+
// flush runs synchronously after the outer-most _batchRun returns.
|
|
16
|
+
// Used by store subscribe/notify cycles.
|
|
17
|
+
//
|
|
18
|
+
// 2. queueMicrotask — when no batch is in progress (depth === 0), the
|
|
19
|
+
// first $$defer schedules a microtask that drains the queue at the
|
|
20
|
+
// end of the current task. Subsequent synchronous $$defer calls
|
|
21
|
+
// reuse the already-scheduled microtask (the `scheduled` flag),
|
|
22
|
+
// coalescing a chain of state changes into one DOM-update pass.
|
|
23
|
+
//
|
|
24
|
+
// Why microtasks instead of synchronous fallback: a single user action
|
|
25
|
+
// often updates several pieces of state in a row (e.g. `loading=true;
|
|
26
|
+
// error=''; data=null` at the start of an async fetch). Synchronous
|
|
27
|
+
// $$defer would redraw three times, with intermediate visual states
|
|
28
|
+
// flashing. Microtask coalescing redraws once after all three setters
|
|
29
|
+
// return.
|
|
30
|
+
//
|
|
31
|
+
// Both flush paths share the same queue and clear `scheduled` after
|
|
32
|
+
// draining, so only one of them runs per coalesced batch even if
|
|
33
|
+
// _batchRun fires before the microtask gets its turn.
|
|
34
|
+
//
|
|
35
|
+
// The context is shared via Symbol.for only for the deprecated legacy
|
|
36
|
+
// generator. AST-generated components import $$defer directly from this
|
|
37
|
+
// module, so they never change behaviour based on global registration order.
|
|
38
|
+
// ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
// $$runtimeContract1 is the runtime/compiler contract marker (R-12.2d).
|
|
41
|
+
// Every generated module that uses the runtime imports it, so a compiler and
|
|
42
|
+
// runtime of different contract versions fail at module load. The compiler
|
|
43
|
+
// changes the name when generated code needs a new or changed runtime API.
|
|
44
|
+
export const $$runtimeContract1 = true;
|
|
45
|
+
|
|
46
|
+
const _B = Symbol.for('vela.batch');
|
|
47
|
+
if (!globalThis[_B]) {
|
|
48
|
+
globalThis[_B] = { depth: 0, queue: [], scheduled: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// _flushQueue drains b.queue, running each deferred fn in FIFO order.
|
|
52
|
+
// Reentrant pushes (a deferred fn that queues another fn) are handled by
|
|
53
|
+
// looping until the queue is empty, so the entire transitive closure of
|
|
54
|
+
// updates triggered by one batch flushes in one pass.
|
|
55
|
+
function _flushQueue() {
|
|
56
|
+
const b = globalThis[_B];
|
|
57
|
+
b.scheduled = false;
|
|
58
|
+
let firstError;
|
|
59
|
+
while (b.queue.length) {
|
|
60
|
+
const q = b.queue.splice(0);
|
|
61
|
+
for (const f of q) {
|
|
62
|
+
try {
|
|
63
|
+
f();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (firstError === undefined) firstError = error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (firstError !== undefined) throw firstError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _batchRun(fn) {
|
|
73
|
+
const b = globalThis[_B];
|
|
74
|
+
b.depth++;
|
|
75
|
+
try { fn(); }
|
|
76
|
+
finally {
|
|
77
|
+
if (--b.depth === 0 && b.queue.length) {
|
|
78
|
+
_flushQueue();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────
|
|
84
|
+
// Select multiple bindings
|
|
85
|
+
//
|
|
86
|
+
// A multi-select is intentionally modelled as an array of strings. Its DOM
|
|
87
|
+
// semantics are membership-based: duplicate values in the model do not create
|
|
88
|
+
// duplicate selections, while duplicate option values (if authored) receive
|
|
89
|
+
// the same membership. Reading preserves DOM order, which gives application
|
|
90
|
+
// code a stable snapshot after a user-driven change.
|
|
91
|
+
//
|
|
92
|
+
// $$writeSelectValues is guarded at the per-option property level. That makes
|
|
93
|
+
// it safe for hydration and for owner-local keyed option reconciliation: a
|
|
94
|
+
// retained option is not touched unless its selected state actually changes.
|
|
95
|
+
|
|
96
|
+
export function $$readSelectValues(select) {
|
|
97
|
+
const values = [];
|
|
98
|
+
for (const option of select.options) {
|
|
99
|
+
if (option.selected) values.push(String(option.value));
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Hydration compares a multiple select by membership, not by the raw array
|
|
105
|
+
// returned from the DOM. Duplicate option values therefore do not replace an
|
|
106
|
+
// equivalent application model merely because the document contains the same
|
|
107
|
+
// selected value more than once.
|
|
108
|
+
export function $$selectValuesEqual(select, values) {
|
|
109
|
+
const expected = new Set(Array.isArray(values) ? values.map(String) : []);
|
|
110
|
+
const actual = new Set();
|
|
111
|
+
for (const option of select.options) {
|
|
112
|
+
if (!option.selected) continue;
|
|
113
|
+
const value = String(option.value);
|
|
114
|
+
if (!expected.has(value)) return false;
|
|
115
|
+
actual.add(value);
|
|
116
|
+
}
|
|
117
|
+
return actual.size === expected.size;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function $$writeSelectValues(select, values) {
|
|
121
|
+
const selected = new Set(Array.isArray(values) ? values.map(String) : []);
|
|
122
|
+
let changed = false;
|
|
123
|
+
for (const option of select.options) {
|
|
124
|
+
const next = selected.has(String(option.value));
|
|
125
|
+
if (option.selected !== next) {
|
|
126
|
+
option.selected = next;
|
|
127
|
+
changed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return changed;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────────
|
|
134
|
+
// Stores
|
|
135
|
+
//
|
|
136
|
+
// Store ownership is centralised here: subscriptions are synchronous,
|
|
137
|
+
// unsubscription is idempotent, and fan-out iterates a snapshot.
|
|
138
|
+
|
|
139
|
+
function createWritable(value) {
|
|
140
|
+
const subscribers = new Set();
|
|
141
|
+
|
|
142
|
+
function notify() {
|
|
143
|
+
let firstError;
|
|
144
|
+
for (const run of [...subscribers]) {
|
|
145
|
+
try {
|
|
146
|
+
run(value);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (firstError === undefined) firstError = error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (firstError !== undefined) throw firstError;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function set(next) {
|
|
155
|
+
if (next === value) return;
|
|
156
|
+
value = next;
|
|
157
|
+
_batchRun(notify);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
subscribe(run) {
|
|
162
|
+
subscribers.add(run);
|
|
163
|
+
try {
|
|
164
|
+
run(value);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
subscribers.delete(run);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
let active = true;
|
|
170
|
+
return () => {
|
|
171
|
+
if (!active) return;
|
|
172
|
+
active = false;
|
|
173
|
+
subscribers.delete(run);
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
set,
|
|
177
|
+
update(fn) {
|
|
178
|
+
set(fn(value));
|
|
179
|
+
},
|
|
180
|
+
get() {
|
|
181
|
+
return value;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A store whose value can be set from outside. */
|
|
187
|
+
export function writable(value) {
|
|
188
|
+
return createWritable(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A read-only store whose start callback runs once for the first subscriber
|
|
193
|
+
* and whose cleanup runs once after the final unsubscribe.
|
|
194
|
+
*/
|
|
195
|
+
export function readable(value, start) {
|
|
196
|
+
const store = createWritable(value);
|
|
197
|
+
let subscriberCount = 0;
|
|
198
|
+
let stop = null;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
subscribe(run) {
|
|
202
|
+
const firstSubscriber = subscriberCount === 0;
|
|
203
|
+
if (firstSubscriber && start) {
|
|
204
|
+
const cleanup = start(store.set);
|
|
205
|
+
stop = typeof cleanup === 'function' ? cleanup : null;
|
|
206
|
+
}
|
|
207
|
+
let unsubscribe;
|
|
208
|
+
try {
|
|
209
|
+
unsubscribe = store.subscribe(run);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (firstSubscriber && stop) {
|
|
212
|
+
const cleanup = stop;
|
|
213
|
+
stop = null;
|
|
214
|
+
cleanup();
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
subscriberCount++;
|
|
219
|
+
let active = true;
|
|
220
|
+
return () => {
|
|
221
|
+
if (!active) return;
|
|
222
|
+
active = false;
|
|
223
|
+
unsubscribe();
|
|
224
|
+
subscriberCount--;
|
|
225
|
+
if (subscriberCount === 0 && stop) {
|
|
226
|
+
const cleanup = stop;
|
|
227
|
+
stop = null;
|
|
228
|
+
cleanup();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
get: store.get,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A read-only value derived from one or more stores. Source subscriptions stay
|
|
238
|
+
* live only while it has subscribers; destroy() is final and idempotent.
|
|
239
|
+
*/
|
|
240
|
+
export function derived(stores, fn, initial) {
|
|
241
|
+
const single = !Array.isArray(stores);
|
|
242
|
+
const sources = single ? [stores] : stores;
|
|
243
|
+
const output = createWritable(initial);
|
|
244
|
+
let subscriberCount = 0;
|
|
245
|
+
let sourceUnsubs = [];
|
|
246
|
+
let active = false;
|
|
247
|
+
let destroyed = false;
|
|
248
|
+
|
|
249
|
+
function start() {
|
|
250
|
+
if (active || destroyed) return;
|
|
251
|
+
active = true;
|
|
252
|
+
const values = new Array(sources.length);
|
|
253
|
+
const inited = new Array(sources.length).fill(false);
|
|
254
|
+
try {
|
|
255
|
+
sourceUnsubs = [];
|
|
256
|
+
for (let index = 0; index < sources.length; index++) {
|
|
257
|
+
const source = sources[index];
|
|
258
|
+
sourceUnsubs.push(source.subscribe(value => {
|
|
259
|
+
values[index] = value;
|
|
260
|
+
inited[index] = true;
|
|
261
|
+
if (inited.every(Boolean)) {
|
|
262
|
+
output.set(single ? fn(values[0]) : fn(values));
|
|
263
|
+
}
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
for (const unsubscribe of sourceUnsubs) unsubscribe();
|
|
268
|
+
sourceUnsubs = [];
|
|
269
|
+
active = false;
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function stop() {
|
|
275
|
+
if (!active) return;
|
|
276
|
+
active = false;
|
|
277
|
+
const unsubs = sourceUnsubs;
|
|
278
|
+
sourceUnsubs = [];
|
|
279
|
+
for (const unsubscribe of unsubs) unsubscribe();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
subscribe(run) {
|
|
284
|
+
const firstSubscriber = subscriberCount === 0;
|
|
285
|
+
if (firstSubscriber) start();
|
|
286
|
+
let unsubscribe;
|
|
287
|
+
try {
|
|
288
|
+
unsubscribe = output.subscribe(run);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (firstSubscriber) stop();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
subscriberCount++;
|
|
294
|
+
let subscribed = true;
|
|
295
|
+
return () => {
|
|
296
|
+
if (!subscribed) return;
|
|
297
|
+
subscribed = false;
|
|
298
|
+
unsubscribe();
|
|
299
|
+
subscriberCount--;
|
|
300
|
+
if (subscriberCount === 0) stop();
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
get: output.get,
|
|
304
|
+
destroy() {
|
|
305
|
+
if (destroyed) return;
|
|
306
|
+
destroyed = true;
|
|
307
|
+
stop();
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─────────────────────────────────────────────
|
|
313
|
+
// Component mounting helpers
|
|
314
|
+
// ─────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* mount(Component, { target, props }) — mounts a compiled Vela component.
|
|
318
|
+
*
|
|
319
|
+
* import { mount } from './runtime.js';
|
|
320
|
+
* import { Counter } from './Counter.js';
|
|
321
|
+
*
|
|
322
|
+
* const app = mount(Counter, { target: document.body, props: { start: 5 } });
|
|
323
|
+
* // later:
|
|
324
|
+
* app.destroy();
|
|
325
|
+
*/
|
|
326
|
+
export function mount(Component, { target, props = {}, slots = {} } = {}) {
|
|
327
|
+
if (!target) throw new Error('[Vela] mount() requires a target element');
|
|
328
|
+
return Component(target, props, slots);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* tick() — returns a Promise that resolves after the current microtask queue
|
|
333
|
+
* drains. Useful in tests or after imperative state changes.
|
|
334
|
+
*/
|
|
335
|
+
export function tick() {
|
|
336
|
+
return Promise.resolve();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─────────────────────────────────────────────
|
|
340
|
+
// Component helper: $$defer
|
|
341
|
+
//
|
|
342
|
+
// AST-generated components import this helper explicitly. It is also
|
|
343
|
+
// registered on globalThis for generated modules from the deprecated legacy
|
|
344
|
+
// code generator, which did not emit a runtime import.
|
|
345
|
+
//
|
|
346
|
+
// Behavior summary:
|
|
347
|
+
// depth > 0 → push to queue; _batchRun's flush will drain it
|
|
348
|
+
// depth === 0 → push to queue; schedule a microtask flush if not already
|
|
349
|
+
// scheduled. Coalesces a chain of state changes into one
|
|
350
|
+
// redraw at the end of the current task.
|
|
351
|
+
//
|
|
352
|
+
// The deprecated generator retains its own synchronous fallback when no
|
|
353
|
+
// runtime module is loaded. That compatibility behaviour does not apply to
|
|
354
|
+
// the AST generator: its named import guarantees this implementation runs.
|
|
355
|
+
// ─────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
// _scheduleFlush asks the JS engine to run _flushQueue at the end of the
|
|
358
|
+
// current task. queueMicrotask is the standard API; the Promise-then form
|
|
359
|
+
// is a fallback for older runtimes that lack it. Both are microtask-class:
|
|
360
|
+
// they fire before the next macrotask (setTimeout, I/O), so flushed updates
|
|
361
|
+
// are visible before any subsequent user interaction.
|
|
362
|
+
const _scheduleFlush = (typeof queueMicrotask === 'function')
|
|
363
|
+
? queueMicrotask
|
|
364
|
+
: (fn) => Promise.resolve().then(fn);
|
|
365
|
+
|
|
366
|
+
export function $$defer(fn) {
|
|
367
|
+
const b = globalThis[Symbol.for('vela.batch')];
|
|
368
|
+
b.queue.push(fn);
|
|
369
|
+
if (b.depth > 0) {
|
|
370
|
+
// Inside an explicit batch — _batchRun's depth-decrement will flush.
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!b.scheduled) {
|
|
374
|
+
b.scheduled = true;
|
|
375
|
+
_scheduleFlush(_flushQueue);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
globalThis[Symbol.for('vela.defer')] = $$defer;
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
const DEV = process.env.NODE_ENV !== 'production';
|
|
382
|
+
|
|
383
|
+
export function warn(msg) {
|
|
384
|
+
if (DEV) console.warn('[Vela]', msg);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// $$isDev reports a development build; production builds replace DEV with
|
|
388
|
+
// a constant, so development-only code becomes dead code.
|
|
389
|
+
export function $$isDev() {
|
|
390
|
+
return DEV;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// ── Compiler runtime helpers ────────────────────────────────────
|
|
395
|
+
// These were previously inlined into every generated component.
|
|
396
|
+
// A compiled component imports whichever ones it uses, so the
|
|
397
|
+
// helper body ships once instead of per file.
|
|
398
|
+
|
|
399
|
+
// $$insertHTML parses an HTML string into `target`. A
|
|
400
|
+
// DocumentFragment (nodeType 11) has no innerHTML setter, so the
|
|
401
|
+
// HTML is routed through a <template>; an element uses the faster
|
|
402
|
+
// insertAdjacentHTML.
|
|
403
|
+
export function $$insertHTML($$t, $$h) {
|
|
404
|
+
if ($$t.nodeType === 11) {
|
|
405
|
+
const $$tmp = document.createElement('template');
|
|
406
|
+
$$tmp.innerHTML = $$h;
|
|
407
|
+
$$t.appendChild($$tmp.content);
|
|
408
|
+
} else {
|
|
409
|
+
$$t.insertAdjacentHTML('beforeend', $$h);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// $$proxy wraps a value in a recursive, WeakMap-cached Proxy so a
|
|
414
|
+
// nested mutation fires the same `notify` path as a whole-value
|
|
415
|
+
// reassignment. Backs $state.deep.
|
|
416
|
+
//
|
|
417
|
+
// Notification is value-aware: assigning an equal value or deleting
|
|
418
|
+
// a missing key is a no-op. Native mutating Array methods use one
|
|
419
|
+
// local transaction, so their several Proxy traps publish at most
|
|
420
|
+
// one component invalidation.
|
|
421
|
+
export function $$proxy(val, notify) {
|
|
422
|
+
if (val === null || typeof val !== 'object') return val;
|
|
423
|
+
const cache = new WeakMap();
|
|
424
|
+
const rawByProxy = new WeakMap();
|
|
425
|
+
const arrayMethods = new WeakMap();
|
|
426
|
+
let mutationDepth = 0;
|
|
427
|
+
let mutationPending = false;
|
|
428
|
+
|
|
429
|
+
const mutatingArrayMethods = new Set([
|
|
430
|
+
'copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort',
|
|
431
|
+
'splice', 'unshift',
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
const unwrap = (value) => rawByProxy.get(value) || value;
|
|
435
|
+
const changed = (mutation) => {
|
|
436
|
+
if (mutationDepth > 0) {
|
|
437
|
+
mutationPending = true;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
notify(mutation);
|
|
441
|
+
};
|
|
442
|
+
const runArrayMutation = (target, key, receiver, method, path) => {
|
|
443
|
+
let methods = arrayMethods.get(target);
|
|
444
|
+
if (!methods) {
|
|
445
|
+
methods = new Map();
|
|
446
|
+
arrayMethods.set(target, methods);
|
|
447
|
+
}
|
|
448
|
+
const cached = methods.get(key);
|
|
449
|
+
if (cached && cached.method === method) return cached.wrapped;
|
|
450
|
+
const wrapped = function(...args) {
|
|
451
|
+
// Preserve native extraction/call semantics. Only a method invoked with
|
|
452
|
+
// this proxy as its receiver owns this deep-state notification batch;
|
|
453
|
+
// `const push = list.push; push()` must still throw like Array#push.
|
|
454
|
+
if (this !== receiver) return Reflect.apply(method, this, args.map(unwrap));
|
|
455
|
+
mutationDepth++;
|
|
456
|
+
try {
|
|
457
|
+
return Reflect.apply(method, receiver, args.map(unwrap));
|
|
458
|
+
} finally {
|
|
459
|
+
mutationDepth--;
|
|
460
|
+
if (mutationDepth === 0 && mutationPending) {
|
|
461
|
+
mutationPending = false;
|
|
462
|
+
notify({ kind: 'array', path: path.slice(), method: key });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
methods.set(key, { method, wrapped });
|
|
467
|
+
return wrapped;
|
|
468
|
+
};
|
|
469
|
+
const wrap = (v, path = []) => {
|
|
470
|
+
if (v === null || typeof v !== 'object') return v;
|
|
471
|
+
// Exotic built-ins (Promise, Date, RegExp, Map, Set, WeakMap,
|
|
472
|
+
// WeakSet, Error, ArrayBuffer / TypedArrays) rely on internal
|
|
473
|
+
// slots that Proxy receivers cannot satisfy — wrapping them
|
|
474
|
+
// breaks any method call ("Method Promise.prototype.then
|
|
475
|
+
// called on incompatible receiver"). Return such values
|
|
476
|
+
// unwrapped; deep reactivity for plain objects and arrays is
|
|
477
|
+
// unaffected.
|
|
478
|
+
if (v instanceof Promise) return v;
|
|
479
|
+
if (v instanceof Date) return v;
|
|
480
|
+
if (v instanceof RegExp) return v;
|
|
481
|
+
if (v instanceof Map) return v;
|
|
482
|
+
if (v instanceof Set) return v;
|
|
483
|
+
if (v instanceof WeakMap) return v;
|
|
484
|
+
if (v instanceof WeakSet) return v;
|
|
485
|
+
if (v instanceof Error) return v;
|
|
486
|
+
if (v instanceof ArrayBuffer) return v;
|
|
487
|
+
if (ArrayBuffer.isView && ArrayBuffer.isView(v)) return v;
|
|
488
|
+
let p = cache.get(v);
|
|
489
|
+
if (p) return p;
|
|
490
|
+
p = new Proxy(v, {
|
|
491
|
+
get(t, k, receiver) {
|
|
492
|
+
// Brand: lets a component prop setter recognise a deep
|
|
493
|
+
// ($state.deep) proxy and skip the identity-equality guard
|
|
494
|
+
// that would otherwise suppress a re-render after an
|
|
495
|
+
// in-place mutation (the reference is unchanged but the
|
|
496
|
+
// contents are not).
|
|
497
|
+
if (k === $$PROXY_BRAND) return true;
|
|
498
|
+
const value = Reflect.get(t, k, receiver);
|
|
499
|
+
if (Array.isArray(t) && typeof k === 'string' &&
|
|
500
|
+
mutatingArrayMethods.has(k) && typeof value === 'function') {
|
|
501
|
+
return runArrayMutation(t, k, receiver, value, path);
|
|
502
|
+
}
|
|
503
|
+
return wrap(value, path.concat(k));
|
|
504
|
+
},
|
|
505
|
+
set(t, k, x, receiver) {
|
|
506
|
+
const hadOwn = Object.prototype.hasOwnProperty.call(t, k);
|
|
507
|
+
const before = Reflect.get(t, k, receiver);
|
|
508
|
+
const ok = Reflect.set(t, k, unwrap(x), receiver);
|
|
509
|
+
const hasOwn = Object.prototype.hasOwnProperty.call(t, k);
|
|
510
|
+
if (ok && (hadOwn !== hasOwn ||
|
|
511
|
+
!Object.is(before, Reflect.get(t, k, receiver)))) changed({ kind: 'set', path: path.concat(k) });
|
|
512
|
+
return ok;
|
|
513
|
+
},
|
|
514
|
+
deleteProperty(t, k) {
|
|
515
|
+
if (!Object.prototype.hasOwnProperty.call(t, k)) return true;
|
|
516
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
517
|
+
if (ok) changed({ kind: 'delete', path: path.concat(k) });
|
|
518
|
+
return ok;
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
cache.set(v, p);
|
|
522
|
+
rawByProxy.set(p, v);
|
|
523
|
+
return p;
|
|
524
|
+
};
|
|
525
|
+
return wrap(val);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// $$PROXY_BRAND is a cross-realm symbol read by $$isProxy. Using
|
|
529
|
+
// Symbol.for keeps it stable across module instances.
|
|
530
|
+
const $$PROXY_BRAND = Symbol.for('vela.proxy');
|
|
531
|
+
|
|
532
|
+
// $$isProxy reports whether v is a deep-reactive proxy produced by
|
|
533
|
+
// $$proxy. A component prop setter uses this to decide whether an
|
|
534
|
+
// identity-equal new value might still have mutated in place.
|
|
535
|
+
export function $$isProxy(v) {
|
|
536
|
+
return v !== null && typeof v === 'object' && v[$$PROXY_BRAND] === true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// $$dispatch fires a bubbling CustomEvent from a component's mount
|
|
540
|
+
// target. Exported as a (target, …) function; a generated
|
|
541
|
+
// component binds it to its own $$target.
|
|
542
|
+
export function $$dispatch($$target, name, detail) {
|
|
543
|
+
$$target.dispatchEvent(
|
|
544
|
+
new CustomEvent(name, { detail, bubbles: true }));
|
|
545
|
+
}
|
|
546
|
+
// $$eachRowsForMutations returns the distinct row indexes that can be safely
|
|
547
|
+
// refreshed without a full keyed reconciliation. null is the conservative
|
|
548
|
+
// fallback: structural array changes, key writes, unknown records, and empty
|
|
549
|
+
// batches must retain the normal keyed diff.
|
|
550
|
+
export function $$eachRowsForMutations(mutations, keyField) {
|
|
551
|
+
if (!Array.isArray(mutations) || mutations.length === 0) return null;
|
|
552
|
+
const rows = new Set();
|
|
553
|
+
for (const mutation of mutations) {
|
|
554
|
+
if (!mutation || (mutation.kind !== "set" && mutation.kind !== "delete") ||
|
|
555
|
+
!Array.isArray(mutation.path) || mutation.path.length < 2) return null;
|
|
556
|
+
const [rawIndex, field] = mutation.path;
|
|
557
|
+
const index = typeof rawIndex === "string" ? Number(rawIndex) : rawIndex;
|
|
558
|
+
if (!Number.isInteger(index) || index < 0 || field === keyField) return null;
|
|
559
|
+
rows.add(index);
|
|
560
|
+
}
|
|
561
|
+
return [...rows];
|
|
562
|
+
}
|