@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.
Files changed (131) hide show
  1. package/dist/devtools/index.html +29 -17
  2. package/dist/src/binder.d.ts.map +1 -1
  3. package/dist/src/binder.js +5 -0
  4. package/dist/src/binder.js.map +1 -1
  5. package/dist/src/components/For.d.ts.map +1 -1
  6. package/dist/src/components/For.js +1 -1
  7. package/dist/src/components/For.js.map +1 -1
  8. package/dist/src/components/List.d.ts.map +1 -1
  9. package/dist/src/components/List.js +1 -1
  10. package/dist/src/components/List.js.map +1 -1
  11. package/dist/src/components/Switch.d.ts.map +1 -1
  12. package/dist/src/components/Switch.js +1 -1
  13. package/dist/src/components/Switch.js.map +1 -1
  14. package/dist/src/debug/diagnostics.test.js +3 -2
  15. package/dist/src/debug/diagnostics.test.js.map +1 -1
  16. package/dist/src/debug/effects.d.ts +12 -4
  17. package/dist/src/debug/effects.d.ts.map +1 -1
  18. package/dist/src/debug/effects.js +182 -52
  19. package/dist/src/debug/effects.js.map +1 -1
  20. package/dist/src/debug/effects.test.js +213 -41
  21. package/dist/src/debug/effects.test.js.map +1 -1
  22. package/dist/src/debug/files.d.ts.map +1 -1
  23. package/dist/src/debug/files.js +7 -18
  24. package/dist/src/debug/files.js.map +1 -1
  25. package/dist/src/debug/files.test.js +13 -36
  26. package/dist/src/debug/files.test.js.map +1 -1
  27. package/dist/src/debug/index.d.ts +4 -2
  28. package/dist/src/debug/index.d.ts.map +1 -1
  29. package/dist/src/debug/index.js +4 -2
  30. package/dist/src/debug/index.js.map +1 -1
  31. package/dist/src/debug/message-format.test.d.ts +2 -0
  32. package/dist/src/debug/message-format.test.d.ts.map +1 -0
  33. package/dist/src/debug/message-format.test.js +700 -0
  34. package/dist/src/debug/message-format.test.js.map +1 -0
  35. package/dist/src/debug/render-tree-orphans.test.d.ts +2 -0
  36. package/dist/src/debug/render-tree-orphans.test.d.ts.map +1 -0
  37. package/dist/src/debug/render-tree-orphans.test.js +297 -0
  38. package/dist/src/debug/render-tree-orphans.test.js.map +1 -0
  39. package/dist/src/debug/render.d.ts.map +1 -1
  40. package/dist/src/debug/render.js +83 -130
  41. package/dist/src/debug/render.js.map +1 -1
  42. package/dist/src/debug/render.test.js +91 -128
  43. package/dist/src/debug/render.test.js.map +1 -1
  44. package/dist/src/debug/symbols.d.ts +6 -5
  45. package/dist/src/debug/symbols.d.ts.map +1 -1
  46. package/dist/src/debug/symbols.js +46 -23
  47. package/dist/src/debug/symbols.js.map +1 -1
  48. package/dist/src/debug/symbols.test.js +15 -26
  49. package/dist/src/debug/symbols.test.js.map +1 -1
  50. package/dist/src/debug/trace-writer.d.ts +55 -0
  51. package/dist/src/debug/trace-writer.d.ts.map +1 -0
  52. package/dist/src/debug/trace-writer.js +658 -0
  53. package/dist/src/debug/trace-writer.js.map +1 -0
  54. package/dist/src/debug/trace.d.ts +10 -10
  55. package/dist/src/debug/trace.d.ts.map +1 -1
  56. package/dist/src/debug/trace.js +23 -20
  57. package/dist/src/debug/trace.js.map +1 -1
  58. package/dist/src/devtools/devtools-protocol.d.ts +318 -161
  59. package/dist/src/devtools/devtools-protocol.d.ts.map +1 -1
  60. package/dist/src/devtools/devtools-server.browser.d.ts +0 -5
  61. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -1
  62. package/dist/src/devtools/devtools-server.browser.js +0 -3
  63. package/dist/src/devtools/devtools-server.browser.js.map +1 -1
  64. package/dist/src/devtools/devtools-server.d.ts +0 -6
  65. package/dist/src/devtools/devtools-server.d.ts.map +1 -1
  66. package/dist/src/devtools/devtools-server.js +212 -24
  67. package/dist/src/devtools/devtools-server.js.map +1 -1
  68. package/dist/src/devtools/devtools-transport.d.ts +2 -2
  69. package/dist/src/devtools/devtools-transport.d.ts.map +1 -1
  70. package/dist/src/devtools/devtools-transport.js +2 -2
  71. package/dist/src/devtools/devtools-transport.js.map +1 -1
  72. package/dist/src/devtools-entry.browser.d.ts +1 -1
  73. package/dist/src/devtools-entry.browser.d.ts.map +1 -1
  74. package/dist/src/devtools-entry.browser.js.map +1 -1
  75. package/dist/src/devtools-entry.d.ts +1 -1
  76. package/dist/src/devtools-entry.d.ts.map +1 -1
  77. package/dist/src/devtools-entry.js.map +1 -1
  78. package/dist/src/diagnostics.d.ts.map +1 -1
  79. package/dist/src/diagnostics.js +5 -5
  80. package/dist/src/diagnostics.js.map +1 -1
  81. package/dist/src/reactivity.d.ts +13 -2
  82. package/dist/src/reactivity.d.ts.map +1 -1
  83. package/dist/src/reactivity.js +96 -13
  84. package/dist/src/reactivity.js.map +1 -1
  85. package/dist/src/render.d.ts.map +1 -1
  86. package/dist/src/render.js +84 -30
  87. package/dist/src/render.js.map +1 -1
  88. package/dist/src/scheduler.d.ts +5 -0
  89. package/dist/src/scheduler.d.ts.map +1 -1
  90. package/dist/src/scheduler.js +94 -23
  91. package/dist/src/scheduler.js.map +1 -1
  92. package/dist/src/utils.d.ts.map +1 -1
  93. package/dist/src/utils.js +11 -5
  94. package/dist/src/utils.js.map +1 -1
  95. package/dist/testing/devtools-utils.d.ts +12 -3
  96. package/dist/testing/devtools-utils.d.ts.map +1 -1
  97. package/dist/testing/devtools-utils.js +26 -4
  98. package/dist/testing/devtools-utils.js.map +1 -1
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +1 -1
  101. package/src/binder.ts +47 -38
  102. package/src/components/For.tsx +14 -10
  103. package/src/components/List.tsx +7 -4
  104. package/src/components/Switch.tsx +11 -7
  105. package/src/debug/diagnostics.test.tsx +3 -2
  106. package/src/debug/effects.test.tsx +248 -36
  107. package/src/debug/effects.ts +276 -62
  108. package/src/debug/files.test.tsx +15 -35
  109. package/src/debug/files.ts +11 -11
  110. package/src/debug/index.ts +4 -0
  111. package/src/debug/message-format.test.tsx +759 -0
  112. package/src/debug/render-tree-orphans.test.tsx +344 -0
  113. package/src/debug/render.test.tsx +96 -118
  114. package/src/debug/render.ts +183 -124
  115. package/src/debug/symbols.test.tsx +19 -20
  116. package/src/debug/symbols.ts +106 -23
  117. package/src/debug/trace-writer.ts +969 -0
  118. package/src/debug/trace.ts +25 -28
  119. package/src/devtools/devtools-protocol.ts +361 -176
  120. package/src/devtools/devtools-server.browser.ts +0 -9
  121. package/src/devtools/devtools-server.ts +210 -32
  122. package/src/devtools/devtools-transport.ts +4 -4
  123. package/src/devtools-entry.browser.ts +11 -15
  124. package/src/devtools-entry.ts +9 -15
  125. package/src/diagnostics.ts +14 -5
  126. package/src/reactivity.ts +113 -17
  127. package/src/render.ts +104 -30
  128. package/src/scheduler.ts +145 -26
  129. package/src/utils.tsx +7 -4
  130. package/temp/api.json +142 -20
  131. package/testing/devtools-utils.ts +46 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alloy-js/core",
3
- "version": "0.23.0-dev.10",
3
+ "version": "0.23.0-dev.11",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
package/src/binder.ts CHANGED
@@ -643,54 +643,63 @@ export function createOutputBinder(options: BinderOptions = {}): Binder {
643
643
  }
644
644
 
645
645
  function notifySymbolCreated(symbol: OutputSymbol): void {
646
- effect<Refkey[]>((oldRefkeys) => {
647
- if (symbol.refkeys) {
648
- debug.trace(
649
- TracePhase.resolve.pending,
650
- () => `Notifying resolutions for ${formatRefkeys(symbol.refkeys)}.`,
651
- );
652
- }
653
-
654
- if (oldRefkeys) {
655
- for (const refkey of oldRefkeys) {
656
- if (!symbol.refkeys.includes(refkey)) {
657
- // remove the old refkey from the known declarations
658
- knownDeclarations.delete(refkey);
646
+ effect<Refkey[]>(
647
+ (oldRefkeys) => {
648
+ if (symbol.refkeys) {
649
+ debug.trace(
650
+ TracePhase.resolve.pending,
651
+ () => `Notifying resolutions for ${formatRefkeys(symbol.refkeys)}.`,
652
+ );
653
+ }
659
654
 
660
- // reset any waiting declarations
661
- if (waitingDeclarations.has(refkey)) {
662
- const signal = waitingDeclarations.get(refkey)!;
663
- signal.value = undefined;
655
+ if (oldRefkeys) {
656
+ for (const refkey of oldRefkeys) {
657
+ if (!symbol.refkeys.includes(refkey)) {
658
+ // remove the old refkey from the known declarations
659
+ knownDeclarations.delete(refkey);
660
+
661
+ // reset any waiting declarations
662
+ if (waitingDeclarations.has(refkey)) {
663
+ const signal = waitingDeclarations.get(refkey)!;
664
+ signal.value = undefined;
665
+ }
664
666
  }
665
667
  }
666
668
  }
667
- }
668
669
 
669
- for (const refkey of symbol.refkeys) {
670
- // notify those waiting for this refkey
671
- knownDeclarations.set(refkey, symbol);
672
- if (waitingDeclarations.has(refkey)) {
673
- const signal = waitingDeclarations.get(refkey)!;
674
- signal.value = symbol;
675
- }
670
+ for (const refkey of symbol.refkeys) {
671
+ // notify those waiting for this refkey
672
+ knownDeclarations.set(refkey, symbol);
673
+ if (waitingDeclarations.has(refkey)) {
674
+ const signal = waitingDeclarations.get(refkey)!;
675
+ signal.value = symbol;
676
+ }
676
677
 
677
- const scope = symbol.scope;
678
- if (!scope) {
679
- continue;
680
- }
678
+ const scope = symbol.scope;
679
+ if (!scope) {
680
+ continue;
681
+ }
681
682
 
682
- // notify those waiting for this symbol name
683
- const waitingScope = waitingSymbolNames.get(scope);
684
- if (waitingScope) {
685
- const waitingName = waitingScope.get(symbol.name);
686
- if (waitingName) {
687
- waitingName.value = symbol;
683
+ // notify those waiting for this symbol name
684
+ const waitingScope = waitingSymbolNames.get(scope);
685
+ if (waitingScope) {
686
+ const waitingName = waitingScope.get(symbol.name);
687
+ if (waitingName) {
688
+ waitingName.value = symbol;
689
+ }
688
690
  }
689
691
  }
690
- }
691
692
 
692
- return [...symbol.refkeys];
693
- });
693
+ return [...symbol.refkeys];
694
+ },
695
+ undefined,
696
+ {
697
+ debug: {
698
+ name: "binder:notifySymbolCreated",
699
+ type: "binder",
700
+ },
701
+ },
702
+ );
694
703
  }
695
704
  }
696
705
 
@@ -80,15 +80,19 @@ export function For<
80
80
  const cb = props.children;
81
81
  const options = baseListPropsToMapJoinArgs(props);
82
82
  options.skipFalsy = props.skipFalsy;
83
- return memo(() => {
84
- const maybeRef = props.each;
83
+ return memo(
84
+ () => {
85
+ const maybeRef = props.each;
85
86
 
86
- return (mapJoin as any)(
87
- typeof maybeRef === "function" ? maybeRef : (
88
- () => (isRef(maybeRef) ? maybeRef.value : maybeRef)
89
- ),
90
- cb,
91
- options,
92
- );
93
- });
87
+ return (mapJoin as any)(
88
+ typeof maybeRef === "function" ? maybeRef : (
89
+ () => (isRef(maybeRef) ? maybeRef.value : maybeRef)
90
+ ),
91
+ cb,
92
+ options,
93
+ );
94
+ },
95
+ undefined,
96
+ "For",
97
+ );
94
98
  }
@@ -57,10 +57,13 @@ export interface ListProps extends BaseListProps {
57
57
  */
58
58
  export function List(props: ListProps) {
59
59
  const [rest, forProps] = splitProps(props, ["children"]);
60
- const resolvedChildren = memo(() =>
61
- childrenArray(() => rest.children, {
62
- preserveFragments: true,
63
- }),
60
+ const resolvedChildren = memo(
61
+ () =>
62
+ childrenArray(() => rest.children, {
63
+ preserveFragments: true,
64
+ }),
65
+ undefined,
66
+ "List:children",
64
67
  );
65
68
  return (
66
69
  <For each={resolvedChildren} {...forProps} skipFalsy>
@@ -27,15 +27,19 @@ export function Switch(props: SwitchProps) {
27
27
  const children = childrenArray(() => props.children);
28
28
  const matches = findKeyedChildren(children, matchTag);
29
29
 
30
- return memo(() => {
31
- for (const match of matches) {
32
- if (match.props.when || match.props.else) {
33
- return match.props.children;
30
+ return memo(
31
+ () => {
32
+ for (const match of matches) {
33
+ if (match.props.when || match.props.else) {
34
+ return match.props.children;
35
+ }
34
36
  }
35
- }
36
37
 
37
- return undefined;
38
- });
38
+ return undefined;
39
+ },
40
+ undefined,
41
+ "Switch",
42
+ );
39
43
  }
40
44
 
41
45
  export interface MatchProps {
@@ -34,7 +34,7 @@ afterEach(async () => {
34
34
  });
35
35
 
36
36
  it("emits diagnostics:report messages", async () => {
37
- const collector = createMessageCollector(socket!);
37
+ const collector = await createMessageCollector(socket!);
38
38
  const diagnostics = new DiagnosticsCollector();
39
39
 
40
40
  diagnostics.emit({ message: "Test diagnostic", severity: "warning" });
@@ -50,6 +50,7 @@ it("emits diagnostics:report messages", async () => {
50
50
 
51
51
  expect(diagnosticsMessages[0]).toMatchObject({
52
52
  type: "diagnostics:report",
53
- diagnostics: expect.any(Array),
53
+ message: "Test diagnostic",
54
+ severity: "warning",
54
55
  });
55
56
  });
@@ -1,4 +1,5 @@
1
- import { afterEach, beforeEach, expect, it } from "vitest";
1
+ import { shallowReactive } from "@vue/reactivity";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
3
  import WebSocket from "ws";
3
4
  import {
4
5
  createMessageCollector,
@@ -9,8 +10,9 @@ import {
9
10
  enableDevtools,
10
11
  resetDevtoolsServerForTests,
11
12
  } from "../devtools/devtools-server.js";
12
- import { effect, ref } from "../reactivity.js";
13
+ import { effect, memo, ref } from "../reactivity.js";
13
14
  import { renderAsync } from "../render.js";
15
+ import { flushJobsAsync } from "../scheduler.js";
14
16
  import { debug } from "./index.js";
15
17
 
16
18
  let socket: WebSocket | undefined;
@@ -36,54 +38,264 @@ afterEach(async () => {
36
38
  debug.effect.reset();
37
39
  });
38
40
 
39
- it("emits effect, ref, edge, and update messages", async () => {
40
- const collector = createMessageCollector(socket!);
41
+ it("emits effect, ref, and track messages", async () => {
42
+ const collector = await createMessageCollector(socket!);
41
43
  const r1 = ref(0);
44
+ const r2 = ref(1);
42
45
 
43
- // Create an effect that reads r1.
44
- let observed = 0;
45
46
  effect(() => {
46
- observed = r1.value;
47
+ r1.value = r2.value + 1;
47
48
  });
48
49
 
49
- // Mutate r1 to trigger the effect.
50
- r1.value = 42;
51
-
52
50
  await renderAsync(<Output>{"ok"}</Output>);
53
51
 
54
52
  const messages = await collector.waitForRender();
55
53
  const effectsMessages = filterEffectsMessages(messages);
56
54
  collector.stop();
57
55
 
58
- // Check that core message types are present
59
- const byType = (type: string) =>
60
- effectsMessages.filter((m: any) => m.type === type);
61
- expect(byType("effect:refAdded").length).toBeGreaterThanOrEqual(1);
62
- expect(byType("effect:effectAdded").length).toBeGreaterThanOrEqual(1);
63
- expect(byType("effect:track").length).toBeGreaterThanOrEqual(1);
64
- expect(byType("effect:trigger").length).toBeGreaterThanOrEqual(1);
65
- expect(byType("effect:effectUpdated").length).toBeGreaterThanOrEqual(1);
66
-
67
- // Verify message shapes
68
- expect(byType("effect:refAdded")[0]).toMatchObject({
69
- ref: expect.objectContaining({ id: expect.any(Number), kind: "ref" }),
56
+ expect(effectsMessages[0]).toMatchObject({
57
+ type: "ref:added",
58
+ id: expect.any(Number),
59
+ kind: "ref",
60
+ });
61
+ expect(effectsMessages[1]).toMatchObject({
62
+ type: "ref:added",
63
+ id: expect.any(Number),
64
+ kind: "ref",
65
+ });
66
+ expect(effectsMessages[2]).toMatchObject({
67
+ type: "effect:added",
68
+ id: expect.any(Number),
69
+ });
70
+ expect(effectsMessages[3]).toMatchObject({
71
+ type: "edge:track",
72
+ effect_id: expect.any(Number),
73
+ ref_id: expect.any(Number),
74
+ });
75
+ });
76
+
77
+ describe("unified trigger edges", () => {
78
+ it("ref trigger records consumer and producer (caused_by)", async () => {
79
+ const collector = await createMessageCollector(socket!);
80
+ const r = ref(0);
81
+
82
+ // Producer effect writes to the ref
83
+ const producerName = "test-producer";
84
+ effect(
85
+ () => {
86
+ r.value = 42;
87
+ },
88
+ undefined,
89
+ { debug: { name: producerName } },
90
+ );
91
+
92
+ // Consumer effect reads the ref
93
+ const consumerName = "test-consumer";
94
+ effect(
95
+ () => {
96
+ void r.value;
97
+ },
98
+ undefined,
99
+ { debug: { name: consumerName } },
100
+ );
101
+
102
+ await renderAsync(<Output>{"ok"}</Output>);
103
+ const renderMessages = await collector.waitForRender();
104
+
105
+ // Trigger the consumer by changing the ref
106
+ r.value = 99;
107
+ await flushJobsAsync();
108
+ const flushMessages = await collector.waitForFlush();
109
+ collector.stop();
110
+
111
+ // Find the trigger edge on the consumer
112
+ const triggerEdges = flushMessages.filter(
113
+ (m: any) => m.type === "edge:trigger" && m.ref_id !== undefined,
114
+ );
115
+ expect(triggerEdges.length).toBeGreaterThan(0);
116
+
117
+ // The trigger edge should be on the consumer effect
118
+ const effectsMessages = filterEffectsMessages(renderMessages);
119
+ const consumerEffect = effectsMessages.find(
120
+ (m: any) => m.type === "effect:added" && m.name === consumerName,
121
+ );
122
+ expect(consumerEffect).toBeDefined();
123
+
124
+ const consumerTrigger = triggerEdges.find(
125
+ (m: any) => m.effect_id === consumerEffect!.id,
126
+ );
127
+ expect(consumerTrigger).toBeDefined();
128
+ // caused_by is null since the mutation happens outside any effect (top-level r.value = 99)
129
+ expect(consumerTrigger!.caused_by).toBeNull();
70
130
  });
71
- expect(byType("effect:effectAdded")[0]).toMatchObject({
72
- effect: expect.objectContaining({ id: expect.any(Number) }),
131
+
132
+ it("reactive object trigger records consumer and producer", async () => {
133
+ const collector = await createMessageCollector(socket!);
134
+ const map = shallowReactive(new Map<string, number>());
135
+
136
+ // Consumer: memo reads the map
137
+ const sum = memo(() => {
138
+ let total = 0;
139
+ for (const v of map.values()) total += v;
140
+ return total;
141
+ });
142
+
143
+ // Producer: effect mutates the map
144
+ effect(
145
+ () => {
146
+ const _s = sum();
147
+ map.set("key", 42);
148
+ },
149
+ undefined,
150
+ { debug: { name: "producer-effect" } },
151
+ );
152
+
153
+ await renderAsync(<Output>{"ok"}</Output>);
154
+
155
+ const messages = await collector.waitForRender();
156
+ const effectsMessages = filterEffectsMessages(messages);
157
+ collector.stop();
158
+
159
+ // Find trigger edges for the reactive map
160
+ const triggerEdges = effectsMessages.filter(
161
+ (m: any) => m.type === "edge:trigger" && m.target_id !== undefined,
162
+ );
163
+ expect(triggerEdges.length).toBeGreaterThan(0);
164
+
165
+ // The trigger should have caused_by (the producer effect)
166
+ const producerEffect = effectsMessages.find(
167
+ (m: any) => m.type === "effect:added" && m.name === "producer-effect",
168
+ );
169
+ expect(producerEffect).toBeDefined();
170
+
171
+ const withCause = triggerEdges.filter(
172
+ (m: any) => m.caused_by === producerEffect!.id,
173
+ );
174
+ expect(withCause.length).toBeGreaterThan(0);
175
+ expect(withCause[0]).toMatchObject({
176
+ type: "edge:trigger",
177
+ effect_id: expect.any(Number),
178
+ caused_by: producerEffect!.id,
179
+ target_id: expect.any(Number),
180
+ });
73
181
  });
74
- expect(byType("effect:track")[0]).toMatchObject({
75
- edge: expect.objectContaining({
76
- type: "track",
77
- effectId: expect.any(Number),
78
- refId: expect.any(Number),
79
- }),
182
+
183
+ it("trigger outside any effect has no caused_by", async () => {
184
+ const collector = await createMessageCollector(socket!);
185
+ const r = ref(0);
186
+
187
+ // Consumer effect
188
+ effect(() => {
189
+ void r.value;
190
+ });
191
+
192
+ await renderAsync(<Output>{"ok"}</Output>);
193
+ await collector.waitForRender();
194
+
195
+ // Trigger from top-level (outside any effect)
196
+ r.value = 1;
197
+ await flushJobsAsync();
198
+ const flushMessages = await collector.waitForFlush();
199
+ collector.stop();
200
+
201
+ const triggerEdges = flushMessages.filter(
202
+ (m: any) => m.type === "edge:trigger",
203
+ );
204
+ expect(triggerEdges.length).toBeGreaterThan(0);
205
+ // caused_by should be null (no producer effect)
206
+ expect(triggerEdges[0].caused_by).toBeNull();
80
207
  });
81
- expect(byType("effect:trigger")[0]).toMatchObject({
82
- edge: expect.objectContaining({
83
- effectId: expect.any(Number),
84
- refId: expect.any(Number),
85
- }),
208
+
209
+ it("multiple consumers of same ref get separate trigger edges", async () => {
210
+ const collector = await createMessageCollector(socket!);
211
+ const r = ref(0);
212
+
213
+ // Two consumer effects
214
+ effect(
215
+ () => {
216
+ void r.value;
217
+ },
218
+ undefined,
219
+ { debug: { name: "consumer-a" } },
220
+ );
221
+ effect(
222
+ () => {
223
+ void r.value;
224
+ },
225
+ undefined,
226
+ { debug: { name: "consumer-b" } },
227
+ );
228
+
229
+ await renderAsync(<Output>{"ok"}</Output>);
230
+ const renderMessages = await collector.waitForRender();
231
+
232
+ // Find the effect IDs
233
+ const effectsMessages = filterEffectsMessages(renderMessages);
234
+ const consumerA = effectsMessages.find(
235
+ (m: any) => m.type === "effect:added" && m.name === "consumer-a",
236
+ );
237
+ const consumerB = effectsMessages.find(
238
+ (m: any) => m.type === "effect:added" && m.name === "consumer-b",
239
+ );
240
+ expect(consumerA).toBeDefined();
241
+ expect(consumerB).toBeDefined();
242
+
243
+ // Trigger both by changing the ref
244
+ r.value = 1;
245
+ await flushJobsAsync();
246
+ const flushMessages = await collector.waitForFlush();
247
+ collector.stop();
248
+
249
+ const triggerEdges = flushMessages.filter(
250
+ (m: any) => m.type === "edge:trigger",
251
+ );
252
+
253
+ // Each consumer gets its own trigger edge
254
+ const aTrigger = triggerEdges.find(
255
+ (m: any) => m.effect_id === consumerA!.id,
256
+ );
257
+ const bTrigger = triggerEdges.find(
258
+ (m: any) => m.effect_id === consumerB!.id,
259
+ );
260
+ expect(aTrigger).toBeDefined();
261
+ expect(bTrigger).toBeDefined();
262
+ // Both reference the same ref
263
+ expect(aTrigger!.ref_id).toBe(bTrigger!.ref_id);
86
264
  });
87
265
 
88
- expect(observed).toBe(42);
266
+ it("no triggered-by or duplicate trigger edge types exist", async () => {
267
+ const collector = await createMessageCollector(socket!);
268
+ const r = ref(0);
269
+ const map = shallowReactive(new Map<string, number>());
270
+
271
+ effect(() => {
272
+ void r.value;
273
+ for (const _v of map.values()) {
274
+ /* track */
275
+ }
276
+ });
277
+
278
+ effect(() => {
279
+ r.value = 1;
280
+ map.set("x", 1);
281
+ });
282
+
283
+ await renderAsync(<Output>{"ok"}</Output>);
284
+ const messages = await collector.waitForRender();
285
+ collector.stop();
286
+
287
+ // No edge:triggered-by messages should exist
288
+ const triggeredBy = messages.filter(
289
+ (m: any) => m.type === "edge:triggered-by",
290
+ );
291
+ expect(triggeredBy.length).toBe(0);
292
+
293
+ // All edge types should be edge:track or edge:trigger only
294
+ const edges = messages.filter(
295
+ (m: any) => typeof m.type === "string" && m.type.startsWith("edge:"),
296
+ );
297
+ for (const edge of edges) {
298
+ expect(["edge:track", "edge:trigger"]).toContain(edge.type);
299
+ }
300
+ });
89
301
  });