@alloy-js/core 0.23.0-dev.10 → 0.23.0-dev.11
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/dist/devtools/index.html +29 -17
- package/dist/src/binder.d.ts.map +1 -1
- package/dist/src/binder.js +5 -0
- package/dist/src/binder.js.map +1 -1
- package/dist/src/components/For.d.ts.map +1 -1
- package/dist/src/components/For.js +1 -1
- package/dist/src/components/For.js.map +1 -1
- package/dist/src/components/List.d.ts.map +1 -1
- package/dist/src/components/List.js +1 -1
- package/dist/src/components/List.js.map +1 -1
- package/dist/src/components/Switch.d.ts.map +1 -1
- package/dist/src/components/Switch.js +1 -1
- package/dist/src/components/Switch.js.map +1 -1
- package/dist/src/debug/diagnostics.test.js +3 -2
- package/dist/src/debug/diagnostics.test.js.map +1 -1
- package/dist/src/debug/effects.d.ts +12 -4
- package/dist/src/debug/effects.d.ts.map +1 -1
- package/dist/src/debug/effects.js +182 -52
- package/dist/src/debug/effects.js.map +1 -1
- package/dist/src/debug/effects.test.js +213 -41
- package/dist/src/debug/effects.test.js.map +1 -1
- package/dist/src/debug/files.d.ts.map +1 -1
- package/dist/src/debug/files.js +7 -18
- package/dist/src/debug/files.js.map +1 -1
- package/dist/src/debug/files.test.js +13 -36
- package/dist/src/debug/files.test.js.map +1 -1
- package/dist/src/debug/index.d.ts +4 -2
- package/dist/src/debug/index.d.ts.map +1 -1
- package/dist/src/debug/index.js +4 -2
- package/dist/src/debug/index.js.map +1 -1
- package/dist/src/debug/message-format.test.d.ts +2 -0
- package/dist/src/debug/message-format.test.d.ts.map +1 -0
- package/dist/src/debug/message-format.test.js +700 -0
- package/dist/src/debug/message-format.test.js.map +1 -0
- package/dist/src/debug/render-tree-orphans.test.d.ts +2 -0
- package/dist/src/debug/render-tree-orphans.test.d.ts.map +1 -0
- package/dist/src/debug/render-tree-orphans.test.js +297 -0
- package/dist/src/debug/render-tree-orphans.test.js.map +1 -0
- package/dist/src/debug/render.d.ts.map +1 -1
- package/dist/src/debug/render.js +83 -130
- package/dist/src/debug/render.js.map +1 -1
- package/dist/src/debug/render.test.js +91 -128
- package/dist/src/debug/render.test.js.map +1 -1
- package/dist/src/debug/symbols.d.ts +6 -5
- package/dist/src/debug/symbols.d.ts.map +1 -1
- package/dist/src/debug/symbols.js +46 -23
- package/dist/src/debug/symbols.js.map +1 -1
- package/dist/src/debug/symbols.test.js +15 -26
- package/dist/src/debug/symbols.test.js.map +1 -1
- package/dist/src/debug/trace-writer.d.ts +55 -0
- package/dist/src/debug/trace-writer.d.ts.map +1 -0
- package/dist/src/debug/trace-writer.js +658 -0
- package/dist/src/debug/trace-writer.js.map +1 -0
- package/dist/src/debug/trace.d.ts +10 -10
- package/dist/src/debug/trace.d.ts.map +1 -1
- package/dist/src/debug/trace.js +23 -20
- package/dist/src/debug/trace.js.map +1 -1
- package/dist/src/devtools/devtools-protocol.d.ts +318 -161
- package/dist/src/devtools/devtools-protocol.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.browser.d.ts +0 -5
- package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.browser.js +0 -3
- package/dist/src/devtools/devtools-server.browser.js.map +1 -1
- package/dist/src/devtools/devtools-server.d.ts +0 -6
- package/dist/src/devtools/devtools-server.d.ts.map +1 -1
- package/dist/src/devtools/devtools-server.js +212 -24
- package/dist/src/devtools/devtools-server.js.map +1 -1
- package/dist/src/devtools/devtools-transport.d.ts +2 -2
- package/dist/src/devtools/devtools-transport.d.ts.map +1 -1
- package/dist/src/devtools/devtools-transport.js +2 -2
- package/dist/src/devtools/devtools-transport.js.map +1 -1
- package/dist/src/devtools-entry.browser.d.ts +1 -1
- package/dist/src/devtools-entry.browser.d.ts.map +1 -1
- package/dist/src/devtools-entry.browser.js.map +1 -1
- package/dist/src/devtools-entry.d.ts +1 -1
- package/dist/src/devtools-entry.d.ts.map +1 -1
- package/dist/src/devtools-entry.js.map +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +5 -5
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/reactivity.d.ts +13 -2
- package/dist/src/reactivity.d.ts.map +1 -1
- package/dist/src/reactivity.js +96 -13
- package/dist/src/reactivity.js.map +1 -1
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +84 -30
- package/dist/src/render.js.map +1 -1
- package/dist/src/scheduler.d.ts +5 -0
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +94 -23
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +11 -5
- package/dist/src/utils.js.map +1 -1
- package/dist/testing/devtools-utils.d.ts +12 -3
- package/dist/testing/devtools-utils.d.ts.map +1 -1
- package/dist/testing/devtools-utils.js +26 -4
- package/dist/testing/devtools-utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/binder.ts +47 -38
- package/src/components/For.tsx +14 -10
- package/src/components/List.tsx +7 -4
- package/src/components/Switch.tsx +11 -7
- package/src/debug/diagnostics.test.tsx +3 -2
- package/src/debug/effects.test.tsx +248 -36
- package/src/debug/effects.ts +276 -62
- package/src/debug/files.test.tsx +15 -35
- package/src/debug/files.ts +11 -11
- package/src/debug/index.ts +4 -0
- package/src/debug/message-format.test.tsx +759 -0
- package/src/debug/render-tree-orphans.test.tsx +344 -0
- package/src/debug/render.test.tsx +96 -118
- package/src/debug/render.ts +183 -124
- package/src/debug/symbols.test.tsx +19 -20
- package/src/debug/symbols.ts +106 -23
- package/src/debug/trace-writer.ts +969 -0
- package/src/debug/trace.ts +25 -28
- package/src/devtools/devtools-protocol.ts +361 -176
- package/src/devtools/devtools-server.browser.ts +0 -9
- package/src/devtools/devtools-server.ts +210 -32
- package/src/devtools/devtools-transport.ts +4 -4
- package/src/devtools-entry.browser.ts +11 -15
- package/src/devtools-entry.ts +9 -15
- package/src/diagnostics.ts +14 -5
- package/src/reactivity.ts +113 -17
- package/src/render.ts +104 -30
- package/src/scheduler.ts +145 -26
- package/src/utils.tsx +7 -4
- package/temp/api.json +142 -20
- package/testing/devtools-utils.ts +46 -4
package/src/reactivity.ts
CHANGED
|
@@ -14,14 +14,10 @@ import {
|
|
|
14
14
|
toRef as vueToRef,
|
|
15
15
|
toRefs as vueToRefs,
|
|
16
16
|
} from "@vue/reactivity";
|
|
17
|
-
import {
|
|
18
|
-
captureSourceLocation,
|
|
19
|
-
debug,
|
|
20
|
-
isDevtoolsEnabled,
|
|
21
|
-
} from "./debug/index.js";
|
|
17
|
+
import { captureSourceLocation, debug, isDebugEnabled } from "./debug/index.js";
|
|
22
18
|
import { RenderedTextTree } from "./render.js";
|
|
23
19
|
import { Children, ComponentCreator } from "./runtime/component.js";
|
|
24
|
-
import { scheduler } from "./scheduler.js";
|
|
20
|
+
import { scheduler, setLastTriggerRef } from "./scheduler.js";
|
|
25
21
|
import type { OutputSymbol } from "./symbols/output-symbol.js";
|
|
26
22
|
|
|
27
23
|
if ((globalThis as any).__ALLOY__) {
|
|
@@ -135,7 +131,9 @@ function resolveOwnerEffectContextId(context: Context): number | null {
|
|
|
135
131
|
* ContentSlot, mapJoin). Most contexts don't need an isEmpty ref.
|
|
136
132
|
*/
|
|
137
133
|
export function ensureIsEmpty(context: Context): Ref<boolean> {
|
|
138
|
-
context.isEmpty ??= ref(context.childrenWithContent === 0
|
|
134
|
+
context.isEmpty ??= ref(context.childrenWithContent === 0, {
|
|
135
|
+
isInfrastructure: true,
|
|
136
|
+
});
|
|
139
137
|
return context.isEmpty;
|
|
140
138
|
}
|
|
141
139
|
|
|
@@ -203,7 +201,10 @@ export function findCurrentEffectId(): number | undefined {
|
|
|
203
201
|
}
|
|
204
202
|
|
|
205
203
|
export function memo<T>(fn: () => T, equal?: boolean, name?: string): () => T {
|
|
206
|
-
const
|
|
204
|
+
const memoLabel = name ? `memo:${name}` : "memo";
|
|
205
|
+
const o = shallowRef<T>(undefined as T, {
|
|
206
|
+
label: memoLabel,
|
|
207
|
+
});
|
|
207
208
|
effect(
|
|
208
209
|
(prev) => {
|
|
209
210
|
const res = fn();
|
|
@@ -283,6 +284,23 @@ export function effect<T>(
|
|
|
283
284
|
refId: id,
|
|
284
285
|
targetKey,
|
|
285
286
|
});
|
|
287
|
+
} else if (
|
|
288
|
+
typeof event.target === "object" &&
|
|
289
|
+
event.target !== null &&
|
|
290
|
+
targetKey !== undefined
|
|
291
|
+
) {
|
|
292
|
+
const id = reactivePropertyRefId(event.target, targetKey);
|
|
293
|
+
debug.effect.ensureReactivePropertyRef({
|
|
294
|
+
id,
|
|
295
|
+
target: event.target,
|
|
296
|
+
key: targetKey,
|
|
297
|
+
});
|
|
298
|
+
debug.effect.track({
|
|
299
|
+
effectId,
|
|
300
|
+
target: event.target,
|
|
301
|
+
refId: id,
|
|
302
|
+
targetKey,
|
|
303
|
+
});
|
|
286
304
|
} else {
|
|
287
305
|
debug.effect.track({
|
|
288
306
|
effectId,
|
|
@@ -292,8 +310,21 @@ export function effect<T>(
|
|
|
292
310
|
}
|
|
293
311
|
};
|
|
294
312
|
effectOpts.onTrigger = (event: any) => {
|
|
313
|
+
if (!("target" in event)) {
|
|
314
|
+
// Vue Dep.notify() chain propagation — no actual reactive target.
|
|
315
|
+
// Skip recording: these are computed→subscriber notifications without
|
|
316
|
+
// a meaningful target reference.
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
295
319
|
const targetKey =
|
|
296
320
|
typeof event.key === "symbol" ? event.key.toString() : event.key;
|
|
321
|
+
// findCurrentEffectId() works because onTrigger fires synchronously
|
|
322
|
+
// during the mutation, so globalContext still points to the producer.
|
|
323
|
+
const producerEffectId = findCurrentEffectId();
|
|
324
|
+
const causedBy =
|
|
325
|
+
producerEffectId !== undefined && producerEffectId !== effectId ?
|
|
326
|
+
producerEffectId
|
|
327
|
+
: undefined;
|
|
297
328
|
if (isRef(event.target)) {
|
|
298
329
|
const id = refId(event.target);
|
|
299
330
|
debug.effect.ensureRef({ id, kind: "ref" });
|
|
@@ -302,14 +333,34 @@ export function effect<T>(
|
|
|
302
333
|
target: event.target,
|
|
303
334
|
refId: id,
|
|
304
335
|
targetKey,
|
|
305
|
-
|
|
336
|
+
causedBy,
|
|
306
337
|
});
|
|
338
|
+
setLastTriggerRef(effectId, id);
|
|
339
|
+
} else if (
|
|
340
|
+
typeof event.target === "object" &&
|
|
341
|
+
event.target !== null &&
|
|
342
|
+
targetKey !== undefined
|
|
343
|
+
) {
|
|
344
|
+
const id = reactivePropertyRefId(event.target, targetKey);
|
|
345
|
+
debug.effect.ensureReactivePropertyRef({
|
|
346
|
+
id,
|
|
347
|
+
target: event.target,
|
|
348
|
+
key: targetKey,
|
|
349
|
+
});
|
|
350
|
+
debug.effect.trigger({
|
|
351
|
+
effectId,
|
|
352
|
+
target: event.target,
|
|
353
|
+
refId: id,
|
|
354
|
+
targetKey,
|
|
355
|
+
causedBy,
|
|
356
|
+
});
|
|
357
|
+
setLastTriggerRef(effectId, id);
|
|
307
358
|
} else {
|
|
308
359
|
debug.effect.trigger({
|
|
309
360
|
effectId,
|
|
310
361
|
target: event.target,
|
|
311
362
|
targetKey,
|
|
312
|
-
|
|
363
|
+
causedBy,
|
|
313
364
|
});
|
|
314
365
|
}
|
|
315
366
|
};
|
|
@@ -393,7 +444,7 @@ export function ref<T>(
|
|
|
393
444
|
): Ref<T> {
|
|
394
445
|
const result = vueRef(value) as Ref<T>;
|
|
395
446
|
debug.effect.registerRef({
|
|
396
|
-
id: refId(result
|
|
447
|
+
id: refId(result),
|
|
397
448
|
kind: "ref",
|
|
398
449
|
createdAt: captureSourceLocation(),
|
|
399
450
|
createdByEffectId: globalContext?.meta?.effectId,
|
|
@@ -417,18 +468,19 @@ export function shallowReactive<T extends object>(
|
|
|
417
468
|
target: T,
|
|
418
469
|
): ShallowReactive<T> {
|
|
419
470
|
const result = vueShallowReactive(target);
|
|
420
|
-
if (
|
|
471
|
+
if (isDebugEnabled()) {
|
|
421
472
|
// Store by raw target — Vue's onTrack/onTrigger events pass the raw object, not the proxy.
|
|
422
473
|
reactiveCreationLocations.set(target, captureSourceLocation());
|
|
423
474
|
}
|
|
424
475
|
return result;
|
|
425
476
|
}
|
|
426
477
|
|
|
427
|
-
export function shallowRef<T>(value?: T): Ref<T> {
|
|
478
|
+
export function shallowRef<T>(value?: T, options?: { label?: string }): Ref<T> {
|
|
428
479
|
const result = vueShallowRef(value) as Ref<T>;
|
|
429
480
|
debug.effect.registerRef({
|
|
430
481
|
id: refId(result),
|
|
431
482
|
kind: "shallowRef",
|
|
483
|
+
label: options?.label,
|
|
432
484
|
createdAt: captureSourceLocation(),
|
|
433
485
|
createdByEffectId: globalContext?.meta?.effectId,
|
|
434
486
|
});
|
|
@@ -481,18 +533,63 @@ export function toRefs<T extends object>(
|
|
|
481
533
|
|
|
482
534
|
const seenRefs = new WeakMap<Ref<unknown>, number>();
|
|
483
535
|
let refIdCounter = 1;
|
|
484
|
-
let infraRefIdCounter = -1;
|
|
485
536
|
const effectIdMap = new WeakMap<object, number>();
|
|
486
537
|
|
|
487
|
-
|
|
538
|
+
// Stable ID mapping for (reactive, key) pairs — each property acts as a virtual ref.
|
|
539
|
+
const reactivePropertyIds = new WeakMap<object, Map<string | number, number>>();
|
|
540
|
+
|
|
541
|
+
export function refId(ref: Ref<unknown>): number {
|
|
488
542
|
let id = seenRefs.get(ref);
|
|
489
543
|
if (id === undefined) {
|
|
490
|
-
id =
|
|
544
|
+
id = refIdCounter++;
|
|
491
545
|
seenRefs.set(ref, id);
|
|
492
546
|
}
|
|
493
547
|
return id;
|
|
494
548
|
}
|
|
495
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Get a stable ref ID for a property of a reactive object.
|
|
552
|
+
* Each (target, key) pair gets a unique positive ID from the same counter as refs.
|
|
553
|
+
*/
|
|
554
|
+
export function reactivePropertyRefId(
|
|
555
|
+
target: object,
|
|
556
|
+
key: string | number,
|
|
557
|
+
): number {
|
|
558
|
+
let keys = reactivePropertyIds.get(target);
|
|
559
|
+
if (!keys) {
|
|
560
|
+
keys = new Map();
|
|
561
|
+
reactivePropertyIds.set(target, keys);
|
|
562
|
+
}
|
|
563
|
+
let id = keys.get(key);
|
|
564
|
+
if (id === undefined) {
|
|
565
|
+
id = refIdCounter++;
|
|
566
|
+
keys.set(key, id);
|
|
567
|
+
}
|
|
568
|
+
return id;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Build a human-readable label for a reactive property like `symbolName.prop`.
|
|
573
|
+
*/
|
|
574
|
+
export function formatReactivePropertyLabel(
|
|
575
|
+
target: object,
|
|
576
|
+
key: string | number,
|
|
577
|
+
): string {
|
|
578
|
+
let ownerLabel: string;
|
|
579
|
+
try {
|
|
580
|
+
const str = String(target);
|
|
581
|
+
// OutputSymbol toString() returns something like: TSOutputSymbol "MyInterface"[42]
|
|
582
|
+
// Trim to just the meaningful part
|
|
583
|
+
ownerLabel = str.length > 60 ? str.slice(0, 57) + "..." : str;
|
|
584
|
+
} catch {
|
|
585
|
+
ownerLabel = "reactive";
|
|
586
|
+
}
|
|
587
|
+
if (Array.isArray(target)) {
|
|
588
|
+
ownerLabel = "[]";
|
|
589
|
+
}
|
|
590
|
+
return `${ownerLabel}.${key}`;
|
|
591
|
+
}
|
|
592
|
+
|
|
496
593
|
/** Allocate a unique reactive target ID from the same counter space as ref IDs. */
|
|
497
594
|
export function nextReactiveId(): number {
|
|
498
595
|
return refIdCounter++;
|
|
@@ -500,7 +597,6 @@ export function nextReactiveId(): number {
|
|
|
500
597
|
|
|
501
598
|
export function resetRefIdCounter(): void {
|
|
502
599
|
refIdCounter = 1;
|
|
503
|
-
infraRefIdCounter = -1;
|
|
504
600
|
}
|
|
505
601
|
|
|
506
602
|
export function getEffectDebugId(effect: object): number | undefined {
|
package/src/render.ts
CHANGED
|
@@ -6,10 +6,17 @@ import { SourceFileContext } from "./context/source-file.js";
|
|
|
6
6
|
import {
|
|
7
7
|
debug,
|
|
8
8
|
getRenderNodeId,
|
|
9
|
+
isDevtoolsConnected,
|
|
9
10
|
isDevtoolsEnabled,
|
|
10
11
|
type RenderTreeNodeInfo,
|
|
11
12
|
} from "./debug/index.js";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
beginTransaction,
|
|
15
|
+
closeTrace,
|
|
16
|
+
commitTransaction,
|
|
17
|
+
notifyDiagnosticsReport,
|
|
18
|
+
} from "./debug/trace-writer.js";
|
|
19
|
+
import { isTraceEnabled } from "./debug/trace.js";
|
|
13
20
|
import {
|
|
14
21
|
attachDiagnosticsCollector,
|
|
15
22
|
DiagnosticsCollector,
|
|
@@ -52,6 +59,43 @@ import { IntrinsicElement, isIntrinsicElement } from "./runtime/intrinsic.js";
|
|
|
52
59
|
import { flushJobs, flushJobsAsync, waitForSignal } from "./scheduler.js";
|
|
53
60
|
|
|
54
61
|
const notifiedErrors = new WeakSet<object>();
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Deferred file printing: mark files dirty during render, print once at end
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
interface DirtyFileEntry {
|
|
67
|
+
renderNode: RenderedTextTree;
|
|
68
|
+
printOptions: {
|
|
69
|
+
printWidth?: number;
|
|
70
|
+
tabWidth?: number;
|
|
71
|
+
useTabs?: boolean;
|
|
72
|
+
insertFinalNewLine?: boolean;
|
|
73
|
+
};
|
|
74
|
+
path: string;
|
|
75
|
+
filetype: string;
|
|
76
|
+
}
|
|
77
|
+
const dirtyFiles = new Map<string, DirtyFileEntry>();
|
|
78
|
+
const lastFlushTimeByFile = new Map<string, number>();
|
|
79
|
+
const DEVTOOLS_FLUSH_INTERVAL_MS = 1000;
|
|
80
|
+
|
|
81
|
+
function flushDirtyFile(path: string): void {
|
|
82
|
+
const entry = dirtyFiles.get(path);
|
|
83
|
+
if (!entry) return;
|
|
84
|
+
dirtyFiles.delete(path);
|
|
85
|
+
const contents = printTree(entry.renderNode, {
|
|
86
|
+
...entry.printOptions,
|
|
87
|
+
insertFinalNewLine: entry.printOptions.insertFinalNewLine ?? true,
|
|
88
|
+
noFlush: true,
|
|
89
|
+
});
|
|
90
|
+
debug.files.updated({ path: entry.path, filetype: entry.filetype, contents });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function flushDirtyFiles(): void {
|
|
94
|
+
for (const path of [...dirtyFiles.keys()]) {
|
|
95
|
+
flushDirtyFile(path);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
55
99
|
let lastRenderError: {
|
|
56
100
|
error: { name: string; message: string; stack?: string };
|
|
57
101
|
componentStack: Array<{
|
|
@@ -275,10 +319,7 @@ function reportDiagnosticsForTree(tree: RenderedTextTree) {
|
|
|
275
319
|
const entries = diagnostics.getDiagnostics();
|
|
276
320
|
if (entries.length === 0) return;
|
|
277
321
|
reportDiagnostics(diagnostics);
|
|
278
|
-
|
|
279
|
-
type: "diagnostics:report",
|
|
280
|
-
diagnostics: entries,
|
|
281
|
-
});
|
|
322
|
+
notifyDiagnosticsReport(entries);
|
|
282
323
|
}
|
|
283
324
|
|
|
284
325
|
// Re-export from print-hook.ts to maintain backwards compatibility
|
|
@@ -313,9 +354,13 @@ export function render(
|
|
|
313
354
|
const tree = renderTree(children);
|
|
314
355
|
flushJobs();
|
|
315
356
|
const output = sourceFilesForTree(tree, options);
|
|
357
|
+
flushDirtyFiles();
|
|
316
358
|
reportDiagnosticsForTree(tree);
|
|
317
359
|
reportLastRenderError();
|
|
318
360
|
debug.render.complete();
|
|
361
|
+
// Only close the trace DB when devtools is NOT running. When devtools is
|
|
362
|
+
// active the DB must remain open for post-render reactive updates.
|
|
363
|
+
if (isTraceEnabled() && !isDevtoolsEnabled()) closeTrace();
|
|
319
364
|
if (isDevtoolsEnabled()) {
|
|
320
365
|
void waitForSignal();
|
|
321
366
|
}
|
|
@@ -335,9 +380,13 @@ export async function renderAsync(
|
|
|
335
380
|
// Ensure all reactive updates are flushed before printing.
|
|
336
381
|
await flushJobsAsync();
|
|
337
382
|
const output = sourceFilesForTree(tree, options);
|
|
383
|
+
flushDirtyFiles();
|
|
338
384
|
reportDiagnosticsForTree(tree);
|
|
339
385
|
reportLastRenderError();
|
|
340
386
|
debug.render.complete();
|
|
387
|
+
// Only close the trace DB when devtools is NOT running. When devtools is
|
|
388
|
+
// active the DB must remain open for post-render reactive updates.
|
|
389
|
+
if (isTraceEnabled() && !isDevtoolsEnabled()) closeTrace();
|
|
341
390
|
|
|
342
391
|
return output;
|
|
343
392
|
}
|
|
@@ -449,17 +498,23 @@ export function renderTree(children: Children) {
|
|
|
449
498
|
debug.effect.reset();
|
|
450
499
|
debug.symbols.reset();
|
|
451
500
|
debug.files.reset();
|
|
501
|
+
dirtyFiles.clear();
|
|
502
|
+
lastFlushTimeByFile.clear();
|
|
452
503
|
debug.render.initialize(rootElem);
|
|
504
|
+
if (isTraceEnabled()) beginTransaction();
|
|
453
505
|
try {
|
|
454
506
|
root(() => {
|
|
455
507
|
attachDiagnosticsCollector(diagnostics);
|
|
456
508
|
renderWorker(rootElem, children);
|
|
457
509
|
});
|
|
458
510
|
} catch (e) {
|
|
511
|
+
if (isTraceEnabled()) commitTransaction();
|
|
512
|
+
flushDirtyFiles();
|
|
459
513
|
notifyRenderError(e);
|
|
460
514
|
reportLastRenderError();
|
|
461
515
|
throw e;
|
|
462
516
|
}
|
|
517
|
+
if (isTraceEnabled()) commitTransaction();
|
|
463
518
|
|
|
464
519
|
diagnosticsByTree.set(rootElem, diagnostics);
|
|
465
520
|
|
|
@@ -761,8 +816,12 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
761
816
|
}
|
|
762
817
|
} else if (isComponentCreator(child)) {
|
|
763
818
|
const index = node.length;
|
|
764
|
-
const rerenderToken =
|
|
765
|
-
|
|
819
|
+
const rerenderToken =
|
|
820
|
+
isDevtoolsEnabled() ? ref(0, { isInfrastructure: true }) : undefined;
|
|
821
|
+
const breakNext =
|
|
822
|
+
isDevtoolsEnabled() ?
|
|
823
|
+
ref(false, { isInfrastructure: true })
|
|
824
|
+
: undefined;
|
|
766
825
|
// todo: remove this effect (only needed for context, not needed for anything else)
|
|
767
826
|
effect(
|
|
768
827
|
() => {
|
|
@@ -789,15 +848,21 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
789
848
|
source: child.source,
|
|
790
849
|
isExisting: Array.isArray(existing),
|
|
791
850
|
actions: {
|
|
792
|
-
rerender:
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
851
|
+
rerender:
|
|
852
|
+
rerenderToken ?
|
|
853
|
+
() => {
|
|
854
|
+
lastRenderError = null;
|
|
855
|
+
rerenderToken.value++;
|
|
856
|
+
}
|
|
857
|
+
: () => {},
|
|
858
|
+
rerenderAndBreak:
|
|
859
|
+
breakNext && rerenderToken ?
|
|
860
|
+
() => {
|
|
861
|
+
lastRenderError = null;
|
|
862
|
+
breakNext.value = true;
|
|
863
|
+
rerenderToken.value++;
|
|
864
|
+
}
|
|
865
|
+
: () => {},
|
|
801
866
|
},
|
|
802
867
|
});
|
|
803
868
|
if (Array.isArray(existing)) {
|
|
@@ -809,7 +874,7 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
809
874
|
let childResult: Children | undefined;
|
|
810
875
|
try {
|
|
811
876
|
childResult = untrack(() => {
|
|
812
|
-
const shouldBreak = breakNext?.value;
|
|
877
|
+
const shouldBreak = breakNext?.value ?? false;
|
|
813
878
|
if (shouldBreak) {
|
|
814
879
|
breakNext!.value = false;
|
|
815
880
|
// eslint-disable-next-line no-debugger
|
|
@@ -927,29 +992,38 @@ function findSourceFileContext(node: RenderedTextTree) {
|
|
|
927
992
|
}
|
|
928
993
|
|
|
929
994
|
function notifyFileUpdateForNode(node: RenderedTextTree) {
|
|
930
|
-
// Only
|
|
931
|
-
if (!isDevtoolsEnabled()) return;
|
|
995
|
+
// Only track when devtools or trace are actually enabled
|
|
996
|
+
if (!isDevtoolsEnabled() && !isTraceEnabled()) return;
|
|
932
997
|
const context = findSourceFileContext(node);
|
|
933
998
|
if (!context?.meta?.sourceFile) return;
|
|
934
999
|
if (context.meta.sourceFileReady === false) return;
|
|
935
1000
|
const sourceFile = context.meta.sourceFile;
|
|
936
1001
|
const renderNode: RenderedTextTree =
|
|
937
1002
|
(context.meta.renderNode as RenderedTextTree | undefined) ?? node;
|
|
938
|
-
// Pass noFlush here since it flushes jobs and can re-enter rendering
|
|
939
|
-
// during effect setup, triggering premature cleanup.
|
|
940
|
-
const contents = printTree(renderNode, {
|
|
941
|
-
printWidth: context.meta?.printOptions?.printWidth,
|
|
942
|
-
tabWidth: context.meta?.printOptions?.tabWidth,
|
|
943
|
-
useTabs: context.meta?.printOptions?.useTabs,
|
|
944
|
-
insertFinalNewLine: context.meta?.printOptions?.insertFinalNewLine ?? true,
|
|
945
|
-
noFlush: true,
|
|
946
|
-
});
|
|
947
1003
|
|
|
948
|
-
|
|
1004
|
+
// Mark this file as dirty — defer the expensive printTree to end of render
|
|
1005
|
+
dirtyFiles.set(sourceFile.path, {
|
|
1006
|
+
renderNode,
|
|
1007
|
+
printOptions: {
|
|
1008
|
+
printWidth: context.meta?.printOptions?.printWidth,
|
|
1009
|
+
tabWidth: context.meta?.printOptions?.tabWidth,
|
|
1010
|
+
useTabs: context.meta?.printOptions?.useTabs,
|
|
1011
|
+
insertFinalNewLine: context.meta?.printOptions?.insertFinalNewLine,
|
|
1012
|
+
},
|
|
949
1013
|
path: sourceFile.path,
|
|
950
1014
|
filetype: sourceFile.filetype,
|
|
951
|
-
contents,
|
|
952
1015
|
});
|
|
1016
|
+
|
|
1017
|
+
// When a devtools client is connected, throttle file flushing to ~1s per file
|
|
1018
|
+
// so the user can watch content build up during rendering.
|
|
1019
|
+
if (isDevtoolsConnected()) {
|
|
1020
|
+
const now = Date.now();
|
|
1021
|
+
const lastFlush = lastFlushTimeByFile.get(sourceFile.path) ?? 0;
|
|
1022
|
+
if (now - lastFlush >= DEVTOOLS_FLUSH_INTERVAL_MS) {
|
|
1023
|
+
lastFlushTimeByFile.set(sourceFile.path, now);
|
|
1024
|
+
flushDirtyFile(sourceFile.path);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
953
1027
|
}
|
|
954
1028
|
|
|
955
1029
|
type NormalizedChildren = NormalizedChild | NormalizedChildren[];
|
package/src/scheduler.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { ReactiveEffect } from "@vue/reactivity";
|
|
2
2
|
import { debug } from "./debug/index.js";
|
|
3
|
+
import {
|
|
4
|
+
beginTransaction,
|
|
5
|
+
commitTransaction,
|
|
6
|
+
insertEffectLifecycle,
|
|
7
|
+
insertSchedulerFlush,
|
|
8
|
+
insertSchedulerJob,
|
|
9
|
+
} from "./debug/trace-writer.js";
|
|
10
|
+
import { isTraceEnabled } from "./debug/trace.js";
|
|
11
|
+
import { getEffectDebugId } from "./reactivity.js";
|
|
3
12
|
|
|
4
13
|
export interface QueueJob {
|
|
5
14
|
run(): void;
|
|
6
15
|
}
|
|
7
16
|
const immediateQueue = new Set<QueueJob>();
|
|
8
17
|
const queue = new Set<QueueJob>();
|
|
18
|
+
|
|
9
19
|
function isJobActive(job: QueueJob): boolean {
|
|
10
20
|
// ReactiveEffect uses bit 0 (flags & 1) as the ACTIVE flag.
|
|
11
21
|
// Skip effects that were stopped after being queued.
|
|
@@ -18,12 +28,19 @@ let resolveWaitForSignal: (() => void) | null = null;
|
|
|
18
28
|
let jobSignalPromise: Promise<void> | null = null;
|
|
19
29
|
let resolveJobSignal: (() => void) | null = null;
|
|
20
30
|
|
|
21
|
-
// Maps effect debug IDs to the ref that most recently triggered them
|
|
31
|
+
// Maps effect debug IDs to the ref that most recently triggered them.
|
|
32
|
+
// Intentionally overwrites on repeated triggers for the same effect before flush —
|
|
33
|
+
// we only need the last trigger ref for lifecycle recording, not all intermediate ones.
|
|
22
34
|
const lastTriggerRef = new Map<number, number>();
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* Record which ref triggered an effect re-run.
|
|
26
38
|
* Called from the onTrigger debug hook before the effect is scheduled.
|
|
39
|
+
*
|
|
40
|
+
* Note: if an effect is triggered multiple times before flush, only the last
|
|
41
|
+
* trigger ref is retained. This is intentional — we care about the most
|
|
42
|
+
* recent cause, and tracking all triggers would add overhead with minimal
|
|
43
|
+
* diagnostic value since only the last mutation actually caused the re-run.
|
|
27
44
|
*/
|
|
28
45
|
export function setLastTriggerRef(effectDebugId: number, refId: number): void {
|
|
29
46
|
lastTriggerRef.set(effectDebugId, refId);
|
|
@@ -52,6 +69,18 @@ export function queueJob(job: QueueJob | (() => void), immediate = false) {
|
|
|
52
69
|
queue.add(job);
|
|
53
70
|
}
|
|
54
71
|
|
|
72
|
+
if (isTraceEnabled()) {
|
|
73
|
+
const effectId = getEffectDebugId(job as object);
|
|
74
|
+
if (effectId !== undefined) {
|
|
75
|
+
insertSchedulerJob(
|
|
76
|
+
"queue",
|
|
77
|
+
effectId,
|
|
78
|
+
immediate,
|
|
79
|
+
immediateQueue.size + queue.size,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
if (resolveJobSignal) {
|
|
56
85
|
resolveJobSignal();
|
|
57
86
|
resolveJobSignal = null;
|
|
@@ -72,19 +101,69 @@ export function trackPromise(promise: Promise<any>) {
|
|
|
72
101
|
|
|
73
102
|
export function flushJobs() {
|
|
74
103
|
// First, run all synchronous jobs
|
|
104
|
+
if (isTraceEnabled()) beginTransaction();
|
|
75
105
|
let job;
|
|
106
|
+
let jobCount = 0;
|
|
76
107
|
while ((job = takeJob()) !== null) {
|
|
77
|
-
if (
|
|
108
|
+
if (isTraceEnabled()) {
|
|
109
|
+
const effectId = getEffectDebugId(job as object);
|
|
110
|
+
if (effectId !== undefined) {
|
|
111
|
+
insertSchedulerJob(
|
|
112
|
+
"run",
|
|
113
|
+
effectId,
|
|
114
|
+
false,
|
|
115
|
+
immediateQueue.size + queue.size,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!isJobActive(job)) {
|
|
120
|
+
if (isTraceEnabled()) {
|
|
121
|
+
const effectId = getEffectDebugId(job as object);
|
|
122
|
+
if (effectId !== undefined) {
|
|
123
|
+
insertEffectLifecycle(
|
|
124
|
+
effectId,
|
|
125
|
+
"skipped",
|
|
126
|
+
undefined,
|
|
127
|
+
undefined,
|
|
128
|
+
undefined,
|
|
129
|
+
undefined,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (isTraceEnabled()) {
|
|
136
|
+
const effectId = getEffectDebugId(job as object);
|
|
137
|
+
if (effectId !== undefined) {
|
|
138
|
+
const triggerRefId = lastTriggerRef.get(effectId);
|
|
139
|
+
lastTriggerRef.delete(effectId);
|
|
140
|
+
insertEffectLifecycle(
|
|
141
|
+
effectId,
|
|
142
|
+
"ran",
|
|
143
|
+
triggerRefId,
|
|
144
|
+
undefined,
|
|
145
|
+
undefined,
|
|
146
|
+
undefined,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
78
150
|
job.run();
|
|
151
|
+
jobCount++;
|
|
79
152
|
}
|
|
80
153
|
|
|
81
154
|
// If there are no pending promises, we're done
|
|
82
155
|
if (pendingPromises.size > 0) {
|
|
156
|
+
if (isTraceEnabled()) commitTransaction();
|
|
83
157
|
throw new Error(
|
|
84
158
|
"Asynchronous jobs were found but render was called synchronously. Use `renderAsync` instead.",
|
|
85
159
|
);
|
|
86
160
|
}
|
|
87
161
|
|
|
162
|
+
if (isTraceEnabled()) {
|
|
163
|
+
insertSchedulerFlush(jobCount);
|
|
164
|
+
commitTransaction();
|
|
165
|
+
}
|
|
166
|
+
|
|
88
167
|
debug.render.flushJobsComplete();
|
|
89
168
|
}
|
|
90
169
|
|
|
@@ -115,34 +194,74 @@ export function isWaitingForSignal() {
|
|
|
115
194
|
|
|
116
195
|
export async function flushJobsAsync() {
|
|
117
196
|
// Keep running jobs until both the queues are empty and all promises are resolved
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
job
|
|
124
|
-
|
|
197
|
+
if (isTraceEnabled()) beginTransaction();
|
|
198
|
+
try {
|
|
199
|
+
while (true) {
|
|
200
|
+
// First, run all synchronous jobs
|
|
201
|
+
let job;
|
|
202
|
+
while ((job = takeJob()) !== null) {
|
|
203
|
+
if (!isJobActive(job)) {
|
|
204
|
+
if (isTraceEnabled()) {
|
|
205
|
+
const effectId = getEffectDebugId(job as object);
|
|
206
|
+
if (effectId !== undefined) {
|
|
207
|
+
insertEffectLifecycle(
|
|
208
|
+
effectId,
|
|
209
|
+
"skipped",
|
|
210
|
+
undefined,
|
|
211
|
+
undefined,
|
|
212
|
+
undefined,
|
|
213
|
+
undefined,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (isTraceEnabled()) {
|
|
220
|
+
const effectId = getEffectDebugId(job as object);
|
|
221
|
+
if (effectId !== undefined) {
|
|
222
|
+
const triggerRefId = lastTriggerRef.get(effectId);
|
|
223
|
+
lastTriggerRef.delete(effectId);
|
|
224
|
+
insertEffectLifecycle(
|
|
225
|
+
effectId,
|
|
226
|
+
"ran",
|
|
227
|
+
triggerRefId,
|
|
228
|
+
undefined,
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
job.run();
|
|
235
|
+
}
|
|
125
236
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
237
|
+
// If there are no pending promises, we're done
|
|
238
|
+
if (pendingPromises.size === 0) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
130
241
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
resolveJobSignal = resolve;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
242
|
+
// Commit before awaiting so writes are visible, then re-open after
|
|
243
|
+
if (isTraceEnabled()) commitTransaction();
|
|
136
244
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
245
|
+
if (!jobSignalPromise) {
|
|
246
|
+
jobSignalPromise = new Promise<void>((resolve) => {
|
|
247
|
+
resolveJobSignal = resolve;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
142
250
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
251
|
+
// Wait for either pending promises to complete or new jobs to arrive
|
|
252
|
+
await Promise.race([
|
|
253
|
+
Promise.allSettled(Array.from(pendingPromises)),
|
|
254
|
+
jobSignalPromise,
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
// Clear the job signal after each iteration so we create a new one next time
|
|
258
|
+
jobSignalPromise = null;
|
|
259
|
+
resolveJobSignal = null;
|
|
260
|
+
|
|
261
|
+
if (isTraceEnabled()) beginTransaction();
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
if (isTraceEnabled()) commitTransaction();
|
|
146
265
|
}
|
|
147
266
|
|
|
148
267
|
debug.render.flushJobsComplete();
|