jazz-tools 0.20.1 → 0.20.3

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 (115) hide show
  1. package/.turbo/turbo-build.log +49 -49
  2. package/CHANGELOG.md +19 -6
  3. package/dist/{chunk-2OPP7KWV.js → chunk-Q5RNSSUM.js} +121 -22
  4. package/dist/chunk-Q5RNSSUM.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/{chunk-MCTB5ZJC.js → chunk-6JPVMI3V.js} +302 -182
  7. package/dist/inspector/chunk-6JPVMI3V.js.map +1 -0
  8. package/dist/inspector/{custom-element-5YWVZBWA.js → custom-element-WOQY2M4W.js} +1337 -206
  9. package/dist/inspector/custom-element-WOQY2M4W.js.map +1 -0
  10. package/dist/inspector/in-app.d.ts +1 -0
  11. package/dist/inspector/in-app.d.ts.map +1 -1
  12. package/dist/inspector/index.d.ts +1 -0
  13. package/dist/inspector/index.d.ts.map +1 -1
  14. package/dist/inspector/index.js +1044 -17
  15. package/dist/inspector/index.js.map +1 -1
  16. package/dist/inspector/pages/home.d.ts +4 -1
  17. package/dist/inspector/pages/home.d.ts.map +1 -1
  18. package/dist/inspector/pages/performance/PerformancePage.d.ts +7 -0
  19. package/dist/inspector/pages/performance/PerformancePage.d.ts.map +1 -0
  20. package/dist/inspector/pages/performance/SubscriptionDetailPanel.d.ts +8 -0
  21. package/dist/inspector/pages/performance/SubscriptionDetailPanel.d.ts.map +1 -0
  22. package/dist/inspector/pages/performance/SubscriptionRow.d.ts +11 -0
  23. package/dist/inspector/pages/performance/SubscriptionRow.d.ts.map +1 -0
  24. package/dist/inspector/pages/performance/Timeline.d.ts +12 -0
  25. package/dist/inspector/pages/performance/Timeline.d.ts.map +1 -0
  26. package/dist/inspector/pages/performance/helpers.d.ts +5 -0
  27. package/dist/inspector/pages/performance/helpers.d.ts.map +1 -0
  28. package/dist/inspector/pages/performance/index.d.ts +3 -0
  29. package/dist/inspector/pages/performance/index.d.ts.map +1 -0
  30. package/dist/inspector/pages/performance/types.d.ts +13 -0
  31. package/dist/inspector/pages/performance/types.d.ts.map +1 -0
  32. package/dist/inspector/pages/performance/usePerformanceEntries.d.ts +3 -0
  33. package/dist/inspector/pages/performance/usePerformanceEntries.d.ts.map +1 -0
  34. package/dist/inspector/register-custom-element.js +3 -1
  35. package/dist/inspector/register-custom-element.js.map +1 -1
  36. package/dist/inspector/standalone.js +1 -1
  37. package/dist/inspector/tests/pages/performance/PerformancePage.test.d.ts +2 -0
  38. package/dist/inspector/tests/pages/performance/PerformancePage.test.d.ts.map +1 -0
  39. package/dist/inspector/tests/pages/performance/SubscriptionDetailPanel.test.d.ts +2 -0
  40. package/dist/inspector/tests/pages/performance/SubscriptionDetailPanel.test.d.ts.map +1 -0
  41. package/dist/inspector/tests/pages/performance/SubscriptionRow.test.d.ts +2 -0
  42. package/dist/inspector/tests/pages/performance/SubscriptionRow.test.d.ts.map +1 -0
  43. package/dist/inspector/tests/pages/performance/Timeline.test.d.ts +2 -0
  44. package/dist/inspector/tests/pages/performance/Timeline.test.d.ts.map +1 -0
  45. package/dist/inspector/tests/pages/performance/helpers.test.d.ts +2 -0
  46. package/dist/inspector/tests/pages/performance/helpers.test.d.ts.map +1 -0
  47. package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -1
  48. package/dist/inspector/viewer/header.d.ts +4 -2
  49. package/dist/inspector/viewer/header.d.ts.map +1 -1
  50. package/dist/inspector/viewer/page-stack.d.ts +3 -1
  51. package/dist/inspector/viewer/page-stack.d.ts.map +1 -1
  52. package/dist/react-core/hooks.d.ts +2 -2
  53. package/dist/react-core/hooks.d.ts.map +1 -1
  54. package/dist/react-core/index.js +50 -18
  55. package/dist/react-core/index.js.map +1 -1
  56. package/dist/react-core/subscription-provider.d.ts.map +1 -1
  57. package/dist/react-native-core/media/image.d.ts +1 -1
  58. package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
  59. package/dist/svelte/jazz.class.svelte.js +27 -22
  60. package/dist/testing.js +1 -1
  61. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  62. package/dist/tools/exports.d.ts +1 -1
  63. package/dist/tools/exports.d.ts.map +1 -1
  64. package/dist/tools/implementation/zodSchema/runtimeConverters/coValueSchemaTransformation.d.ts.map +1 -1
  65. package/dist/tools/subscribe/SubscriptionCache.d.ts +2 -2
  66. package/dist/tools/subscribe/SubscriptionCache.d.ts.map +1 -1
  67. package/dist/tools/subscribe/SubscriptionScope.d.ts +19 -12
  68. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  69. package/dist/tools/subscribe/errorReporting.d.ts +6 -0
  70. package/dist/tools/subscribe/errorReporting.d.ts.map +1 -1
  71. package/dist/tools/subscribe/index.d.ts +4 -4
  72. package/dist/tools/subscribe/index.d.ts.map +1 -1
  73. package/dist/tools/subscribe/types.d.ts +48 -3
  74. package/dist/tools/subscribe/types.d.ts.map +1 -1
  75. package/dist/tools/subscribe/utils.d.ts +1 -1
  76. package/dist/tools/subscribe/utils.d.ts.map +1 -1
  77. package/dist/tools/tests/SubscriptionScope.performance.test.d.ts +2 -0
  78. package/dist/tools/tests/SubscriptionScope.performance.test.d.ts.map +1 -0
  79. package/package.json +4 -4
  80. package/src/inspector/in-app.tsx +41 -3
  81. package/src/inspector/index.tsx +5 -1
  82. package/src/inspector/pages/home.tsx +26 -3
  83. package/src/inspector/pages/performance/PerformancePage.tsx +215 -0
  84. package/src/inspector/pages/performance/SubscriptionDetailPanel.tsx +182 -0
  85. package/src/inspector/pages/performance/SubscriptionRow.tsx +242 -0
  86. package/src/inspector/pages/performance/Timeline.tsx +513 -0
  87. package/src/inspector/pages/performance/helpers.ts +70 -0
  88. package/src/inspector/pages/performance/index.ts +2 -0
  89. package/src/inspector/pages/performance/types.ts +12 -0
  90. package/src/inspector/pages/performance/usePerformanceEntries.ts +53 -0
  91. package/src/inspector/register-custom-element.ts +3 -0
  92. package/src/inspector/tests/pages/performance/PerformancePage.test.tsx +83 -0
  93. package/src/inspector/tests/pages/performance/SubscriptionDetailPanel.test.tsx +68 -0
  94. package/src/inspector/tests/pages/performance/SubscriptionRow.test.tsx +93 -0
  95. package/src/inspector/tests/pages/performance/Timeline.test.tsx +57 -0
  96. package/src/inspector/tests/pages/performance/helpers.test.ts +91 -0
  97. package/src/inspector/viewer/delete-local-data.tsx +24 -5
  98. package/src/inspector/viewer/header.tsx +96 -17
  99. package/src/inspector/viewer/page-stack.tsx +22 -18
  100. package/src/react-core/hooks.ts +34 -4
  101. package/src/react-core/subscription-provider.tsx +17 -8
  102. package/src/svelte/jazz.class.svelte.ts +51 -33
  103. package/src/tools/coValues/interfaces.ts +3 -0
  104. package/src/tools/exports.ts +1 -0
  105. package/src/tools/implementation/zodSchema/runtimeConverters/coValueSchemaTransformation.ts +13 -0
  106. package/src/tools/subscribe/SubscriptionCache.ts +6 -4
  107. package/src/tools/subscribe/SubscriptionScope.ts +141 -23
  108. package/src/tools/subscribe/errorReporting.ts +1 -1
  109. package/src/tools/subscribe/index.ts +1 -1
  110. package/src/tools/subscribe/types.ts +62 -9
  111. package/src/tools/subscribe/utils.ts +2 -2
  112. package/src/tools/tests/SubscriptionScope.performance.test.ts +149 -0
  113. package/dist/chunk-2OPP7KWV.js.map +0 -1
  114. package/dist/inspector/chunk-MCTB5ZJC.js.map +0 -1
  115. package/dist/inspector/custom-element-5YWVZBWA.js.map +0 -1
@@ -0,0 +1,513 @@
1
+ import { styled } from "goober";
2
+ import {
3
+ type CSSProperties,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import type { SubscriptionEntry } from "./types.js";
10
+ import { formatDuration } from "./helpers.js";
11
+
12
+ // ============================================================================
13
+ // Styled Components
14
+ // ============================================================================
15
+
16
+ const TimelineContainer = styled("div")`
17
+ position: relative;
18
+ display: flex;
19
+ flex-direction: column;
20
+ background-color: var(--j-foreground);
21
+ border: 1px solid var(--j-border-color);
22
+ border-radius: var(--j-radius-sm);
23
+ overflow: hidden;
24
+ `;
25
+
26
+ const TimelineTrack = styled("div")`
27
+ position: relative;
28
+ height: 48px;
29
+ background-color: var(--j-background);
30
+ cursor: crosshair;
31
+ user-select: none;
32
+ `;
33
+
34
+ const TimeMarker = styled("div")`
35
+ position: absolute;
36
+ font-size: 0.5rem;
37
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
38
+ color: var(--j-text-color);
39
+ padding: 2px 4px;
40
+ white-space: nowrap;
41
+
42
+ @media (prefers-color-scheme: dark) {
43
+ color: var(--j-neutral-500);
44
+ }
45
+
46
+ &::after {
47
+ content: "";
48
+ position: absolute;
49
+ left: 0;
50
+ top: 100%;
51
+ width: 1px;
52
+ height: 32px;
53
+ background-color: var(--j-border-color);
54
+ }
55
+ `;
56
+
57
+ const TimelineBars = styled("div")`
58
+ position: absolute;
59
+ top: 16px;
60
+ left: 0;
61
+ right: 0;
62
+ bottom: 0;
63
+ pointer-events: none;
64
+ `;
65
+
66
+ const TimelineBar = styled("div")`
67
+ position: absolute;
68
+ border-radius: 1px;
69
+ min-width: 2px;
70
+ `;
71
+
72
+ const TimelineSelection = styled("div")`
73
+ position: absolute;
74
+ top: 0;
75
+ bottom: 0;
76
+ background-color: var(--j-primary-color);
77
+ opacity: 0.2;
78
+ cursor: grab;
79
+ pointer-events: auto;
80
+
81
+ &:active {
82
+ cursor: grabbing;
83
+ }
84
+ `;
85
+
86
+ const TimelineSelectionHandle = styled("div")`
87
+ position: absolute;
88
+ top: 0;
89
+ bottom: 0;
90
+ width: 2px;
91
+ background-color: var(--j-primary-color);
92
+ cursor: ew-resize;
93
+ pointer-events: auto;
94
+
95
+ &::after {
96
+ content: "";
97
+ position: absolute;
98
+ top: 50%;
99
+ left: 50%;
100
+ transform: translate(-50%, -50%);
101
+ width: 8px;
102
+ height: 16px;
103
+ background-color: var(--j-primary-color);
104
+ border-radius: 2px;
105
+ }
106
+ `;
107
+
108
+ const ClearSelectionButton = styled("button")`
109
+ position: absolute;
110
+ top: 4px;
111
+ right: 4px;
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 4px;
115
+ padding: 2px 6px;
116
+ font-size: 0.5rem;
117
+ background-color: var(--j-foreground);
118
+ border: 1px solid var(--j-border-color);
119
+ border-radius: var(--j-radius-sm);
120
+ cursor: pointer;
121
+ color: var(--j-neutral-500);
122
+ z-index: 10;
123
+
124
+ &:hover {
125
+ background-color: var(--j-background);
126
+ color: var(--j-text-color);
127
+ }
128
+ `;
129
+
130
+ // ============================================================================
131
+ // Component
132
+ // ============================================================================
133
+
134
+ export interface TimelineProps {
135
+ entries: SubscriptionEntry[];
136
+ timeRange: { min: number; max: number };
137
+ selection: [number, number] | null;
138
+ onSelectionChange: (selection: [number, number] | null) => void;
139
+ }
140
+
141
+ type DragMode = "creating" | "moving" | "resizing-left" | "resizing-right";
142
+
143
+ export function Timeline({
144
+ entries,
145
+ timeRange,
146
+ selection,
147
+ onSelectionChange,
148
+ }: TimelineProps) {
149
+ const trackRef = useRef<HTMLDivElement>(null);
150
+ const [dragMode, setDragMode] = useState<DragMode | null>(null);
151
+ const [dragStartTime, setDragStartTime] = useState<number | null>(null);
152
+ const [dragCurrentTime, setDragCurrentTime] = useState<number | null>(null);
153
+ const [dragInitialSelection, setDragInitialSelection] = useState<
154
+ [number, number] | null
155
+ >(null);
156
+
157
+ const duration = timeRange.max - timeRange.min;
158
+
159
+ // Generate time markers
160
+ const timeMarkers = useMemo(() => {
161
+ const markers: { time: number; label: string; position: number }[] = [];
162
+ if (duration <= 0) return markers;
163
+
164
+ const maxMarkers = 5;
165
+
166
+ // Calculate minimum interval to have at most maxMarkers
167
+ const minInterval = duration / maxMarkers;
168
+
169
+ // Round up to a "nice" number (1, 2, 5, 10, 20, 50, 100, ...)
170
+ const magnitude = Math.pow(10, Math.floor(Math.log10(minInterval)));
171
+ const normalized = minInterval / magnitude;
172
+ let niceMultiplier: number;
173
+ if (normalized <= 1) niceMultiplier = 1;
174
+ else if (normalized <= 2) niceMultiplier = 2;
175
+ else if (normalized <= 5) niceMultiplier = 5;
176
+ else niceMultiplier = 10;
177
+
178
+ const interval = niceMultiplier * magnitude;
179
+
180
+ const startMarker = Math.ceil(timeRange.min / interval) * interval;
181
+ for (let time = startMarker; time <= timeRange.max; time += interval) {
182
+ const position = ((time - timeRange.min) / duration) * 100;
183
+ markers.push({
184
+ time,
185
+ label: formatDuration(time),
186
+ position,
187
+ });
188
+ }
189
+ return markers;
190
+ }, [timeRange, duration]);
191
+
192
+ const getTimeFromPosition = (clientX: number): number => {
193
+ if (!trackRef.current) return 0;
194
+ const rect = trackRef.current.getBoundingClientRect();
195
+ const position = Math.max(
196
+ 0,
197
+ Math.min(1, (clientX - rect.left) / rect.width),
198
+ );
199
+ return timeRange.min + position * duration;
200
+ };
201
+
202
+ const handleTrackMouseDown = (e: React.MouseEvent) => {
203
+ const time = getTimeFromPosition(e.clientX);
204
+ setDragMode("creating");
205
+ setDragStartTime(time);
206
+ setDragCurrentTime(time);
207
+ };
208
+
209
+ const handleSelectionMouseDown = (e: React.MouseEvent) => {
210
+ e.stopPropagation();
211
+ if (!selection) return;
212
+ const time = getTimeFromPosition(e.clientX);
213
+ setDragMode("moving");
214
+ setDragStartTime(time);
215
+ setDragCurrentTime(time);
216
+ setDragInitialSelection(selection);
217
+ };
218
+
219
+ const handleLeftHandleMouseDown = (e: React.MouseEvent) => {
220
+ e.stopPropagation();
221
+ if (!selection) return;
222
+ setDragMode("resizing-left");
223
+ setDragStartTime(selection[0]);
224
+ setDragCurrentTime(selection[0]);
225
+ setDragInitialSelection(selection);
226
+ };
227
+
228
+ const handleRightHandleMouseDown = (e: React.MouseEvent) => {
229
+ e.stopPropagation();
230
+ if (!selection) return;
231
+ setDragMode("resizing-right");
232
+ setDragStartTime(selection[1]);
233
+ setDragCurrentTime(selection[1]);
234
+ setDragInitialSelection(selection);
235
+ };
236
+
237
+ useEffect(() => {
238
+ if (!dragMode) return;
239
+
240
+ const handleMouseMove = (e: MouseEvent) => {
241
+ const time = getTimeFromPosition(e.clientX);
242
+ setDragCurrentTime(time);
243
+ };
244
+
245
+ const handleMouseUp = () => {
246
+ if (
247
+ dragMode === "creating" &&
248
+ dragStartTime !== null &&
249
+ dragCurrentTime !== null
250
+ ) {
251
+ const start = Math.min(dragStartTime, dragCurrentTime);
252
+ const end = Math.max(dragStartTime, dragCurrentTime);
253
+ if ((end - start) / duration > 0.01) {
254
+ onSelectionChange([start, end]);
255
+ }
256
+ } else if (
257
+ dragMode === "moving" &&
258
+ dragInitialSelection &&
259
+ dragStartTime !== null &&
260
+ dragCurrentTime !== null
261
+ ) {
262
+ const delta = dragCurrentTime - dragStartTime;
263
+ const selectionWidth =
264
+ dragInitialSelection[1] - dragInitialSelection[0];
265
+ let newStart = dragInitialSelection[0] + delta;
266
+ let newEnd = dragInitialSelection[1] + delta;
267
+ // Clamp to time range
268
+ if (newStart < timeRange.min) {
269
+ newStart = timeRange.min;
270
+ newEnd = timeRange.min + selectionWidth;
271
+ }
272
+ if (newEnd > timeRange.max) {
273
+ newEnd = timeRange.max;
274
+ newStart = timeRange.max - selectionWidth;
275
+ }
276
+ onSelectionChange([newStart, newEnd]);
277
+ } else if (
278
+ dragMode === "resizing-left" &&
279
+ dragInitialSelection &&
280
+ dragCurrentTime !== null
281
+ ) {
282
+ const newStart = Math.min(
283
+ dragCurrentTime,
284
+ dragInitialSelection[1] - duration * 0.01,
285
+ );
286
+ onSelectionChange([
287
+ Math.max(timeRange.min, newStart),
288
+ dragInitialSelection[1],
289
+ ]);
290
+ } else if (
291
+ dragMode === "resizing-right" &&
292
+ dragInitialSelection &&
293
+ dragCurrentTime !== null
294
+ ) {
295
+ const newEnd = Math.max(
296
+ dragCurrentTime,
297
+ dragInitialSelection[0] + duration * 0.01,
298
+ );
299
+ onSelectionChange([
300
+ dragInitialSelection[0],
301
+ Math.min(timeRange.max, newEnd),
302
+ ]);
303
+ }
304
+
305
+ setDragMode(null);
306
+ setDragStartTime(null);
307
+ setDragCurrentTime(null);
308
+ setDragInitialSelection(null);
309
+ };
310
+
311
+ window.addEventListener("mousemove", handleMouseMove);
312
+ window.addEventListener("mouseup", handleMouseUp);
313
+ return () => {
314
+ window.removeEventListener("mousemove", handleMouseMove);
315
+ window.removeEventListener("mouseup", handleMouseUp);
316
+ };
317
+ }, [
318
+ dragMode,
319
+ dragStartTime,
320
+ dragCurrentTime,
321
+ dragInitialSelection,
322
+ duration,
323
+ timeRange,
324
+ onSelectionChange,
325
+ ]);
326
+
327
+ // Pre-calculate lane assignments to avoid overlaps
328
+ const laneAssignments = useMemo(() => {
329
+ const now = performance.now();
330
+ const barHeight = 3;
331
+ const barGap = 1;
332
+ const maxLanes = 8;
333
+
334
+ // Sort entries by start time for lane assignment
335
+ const sortedEntries = [...entries].sort(
336
+ (a, b) => a.startTime - b.startTime,
337
+ );
338
+
339
+ // Track end times for each lane (up to maxLanes)
340
+ const laneEndTimes: number[] = Array(maxLanes).fill(0);
341
+ const assignments = new Map<string, number>();
342
+
343
+ for (const entry of sortedEntries) {
344
+ const entryEnd = entry.endTime ?? now;
345
+
346
+ // Find the first lane where this entry fits (no overlap)
347
+ let assignedLane = laneEndTimes.findIndex(
348
+ (endTime) => entry.startTime >= endTime,
349
+ );
350
+
351
+ if (assignedLane === -1) {
352
+ // All lanes are occupied, find the one that ends earliest
353
+ const earliestEnd = Math.min(...laneEndTimes);
354
+ assignedLane = laneEndTimes.indexOf(earliestEnd);
355
+ }
356
+
357
+ // Update the lane's end time
358
+ laneEndTimes[assignedLane] = entryEnd;
359
+
360
+ // Calculate top position
361
+ assignments.set(entry.uuid, assignedLane * (barHeight + barGap));
362
+ }
363
+
364
+ return assignments;
365
+ }, [entries]);
366
+
367
+ const getBarStyle = (entry: SubscriptionEntry): CSSProperties => {
368
+ const now = performance.now();
369
+ const start = entry.startTime;
370
+ const end = entry.endTime ?? now;
371
+ const left = ((start - timeRange.min) / duration) * 100;
372
+ const width = Math.max(0.5, ((end - start) / duration) * 100);
373
+
374
+ const color =
375
+ entry.status === "pending"
376
+ ? "var(--j-warning-color)"
377
+ : entry.status === "error"
378
+ ? "var(--j-error-color)"
379
+ : "var(--j-success-color)";
380
+
381
+ const top = laneAssignments.get(entry.uuid) ?? 0;
382
+
383
+ return {
384
+ left: `${left}%`,
385
+ width: `${width}%`,
386
+ backgroundColor: color,
387
+ top: `${top}px`,
388
+ height: "3px",
389
+ };
390
+ };
391
+
392
+ // Calculate current selection during drag
393
+ const currentSelection = useMemo((): [number, number] | null => {
394
+ if (
395
+ dragMode === "creating" &&
396
+ dragStartTime !== null &&
397
+ dragCurrentTime !== null
398
+ ) {
399
+ return [
400
+ Math.min(dragStartTime, dragCurrentTime),
401
+ Math.max(dragStartTime, dragCurrentTime),
402
+ ];
403
+ }
404
+ if (
405
+ dragMode === "moving" &&
406
+ dragInitialSelection &&
407
+ dragStartTime !== null &&
408
+ dragCurrentTime !== null
409
+ ) {
410
+ const delta = dragCurrentTime - dragStartTime;
411
+ const selectionWidth = dragInitialSelection[1] - dragInitialSelection[0];
412
+ let newStart = dragInitialSelection[0] + delta;
413
+ let newEnd = dragInitialSelection[1] + delta;
414
+ if (newStart < timeRange.min) {
415
+ newStart = timeRange.min;
416
+ newEnd = timeRange.min + selectionWidth;
417
+ }
418
+ if (newEnd > timeRange.max) {
419
+ newEnd = timeRange.max;
420
+ newStart = timeRange.max - selectionWidth;
421
+ }
422
+ return [newStart, newEnd];
423
+ }
424
+ if (
425
+ dragMode === "resizing-left" &&
426
+ dragInitialSelection &&
427
+ dragCurrentTime !== null
428
+ ) {
429
+ const newStart = Math.max(
430
+ timeRange.min,
431
+ Math.min(dragCurrentTime, dragInitialSelection[1] - duration * 0.01),
432
+ );
433
+ return [newStart, dragInitialSelection[1]];
434
+ }
435
+ if (
436
+ dragMode === "resizing-right" &&
437
+ dragInitialSelection &&
438
+ dragCurrentTime !== null
439
+ ) {
440
+ const newEnd = Math.min(
441
+ timeRange.max,
442
+ Math.max(dragCurrentTime, dragInitialSelection[0] + duration * 0.01),
443
+ );
444
+ return [dragInitialSelection[0], newEnd];
445
+ }
446
+ return selection;
447
+ }, [
448
+ dragMode,
449
+ dragStartTime,
450
+ dragCurrentTime,
451
+ dragInitialSelection,
452
+ selection,
453
+ timeRange,
454
+ duration,
455
+ ]);
456
+
457
+ const selectionLeft = currentSelection
458
+ ? ((currentSelection[0] - timeRange.min) / duration) * 100
459
+ : 0;
460
+ const selectionWidth = currentSelection
461
+ ? ((currentSelection[1] - currentSelection[0]) / duration) * 100
462
+ : 0;
463
+
464
+ return (
465
+ <TimelineContainer>
466
+ <TimelineTrack ref={trackRef} onMouseDown={handleTrackMouseDown}>
467
+ {timeMarkers.map((marker) => (
468
+ <TimeMarker key={marker.time} style={{ left: `${marker.position}%` }}>
469
+ {marker.label}
470
+ </TimeMarker>
471
+ ))}
472
+ <TimelineBars>
473
+ {entries.map((entry) => (
474
+ <TimelineBar key={entry.uuid} style={getBarStyle(entry)} />
475
+ ))}
476
+ </TimelineBars>
477
+ {currentSelection && (
478
+ <>
479
+ <TimelineSelection
480
+ style={{
481
+ left: `${selectionLeft}%`,
482
+ width: `${selectionWidth}%`,
483
+ }}
484
+ onMouseDown={handleSelectionMouseDown}
485
+ />
486
+ {!dragMode && (
487
+ <>
488
+ <TimelineSelectionHandle
489
+ style={{ left: `${selectionLeft}%` }}
490
+ onMouseDown={handleLeftHandleMouseDown}
491
+ />
492
+ <TimelineSelectionHandle
493
+ style={{ left: `${selectionLeft + selectionWidth}%` }}
494
+ onMouseDown={handleRightHandleMouseDown}
495
+ />
496
+ </>
497
+ )}
498
+ </>
499
+ )}
500
+ </TimelineTrack>
501
+ {currentSelection && !dragMode && (
502
+ <ClearSelectionButton
503
+ onClick={(e) => {
504
+ e.stopPropagation();
505
+ onSelectionChange(null);
506
+ }}
507
+ >
508
+ Clear selection
509
+ </ClearSelectionButton>
510
+ )}
511
+ </TimelineContainer>
512
+ );
513
+ }
@@ -0,0 +1,70 @@
1
+ export function formatTime(startTime: number): string {
2
+ const date = new Date(performance.timeOrigin + startTime);
3
+ return date.toLocaleTimeString(undefined, {
4
+ hour: "2-digit",
5
+ minute: "2-digit",
6
+ second: "2-digit",
7
+ fractionalSecondDigits: 3,
8
+ });
9
+ }
10
+
11
+ export function formatDuration(duration: number): string {
12
+ if (duration < 1) {
13
+ return `${(duration * 1000).toFixed(0)}μs`;
14
+ }
15
+ if (duration < 1000) {
16
+ return `${duration.toFixed(2)}ms`;
17
+ }
18
+ return `${(duration / 1000).toFixed(2)}s`;
19
+ }
20
+
21
+ export function getCallerLocation(
22
+ stack: string | undefined,
23
+ ): string | undefined {
24
+ if (!stack) return undefined;
25
+
26
+ const lines = stack.split("\n").slice(2, 15);
27
+
28
+ const normalizeLine = (line: string) =>
29
+ line.replace(/https?:\/\/[^/\s)]+/g, "");
30
+
31
+ const userFrame = lines.find(
32
+ (line) =>
33
+ !line.includes("node_modules") &&
34
+ !line.includes("useCoValueSubscription") &&
35
+ !line.includes("useCoState") &&
36
+ !line.includes("useAccount") &&
37
+ !line.includes("useSuspenseCoState") &&
38
+ !line.includes("useSuspenseAccount") &&
39
+ !line.includes("jazz-tools") &&
40
+ !line.includes("trackLoadingPerformance"),
41
+ );
42
+
43
+ if (userFrame) {
44
+ const cleanedFrame = normalizeLine(userFrame).trim();
45
+ const match = cleanedFrame.match(/\(?([^)]+:\d+:\d+)\)?$/);
46
+ if (match) {
47
+ return match[1];
48
+ }
49
+ return cleanedFrame;
50
+ }
51
+
52
+ return lines[0] ? normalizeLine(lines[0]).trim() : undefined;
53
+ }
54
+
55
+ export function getCallerStack(stack: string | undefined): string | undefined {
56
+ if (!stack) return undefined;
57
+
58
+ const lines = stack.split("\n").slice(2, 15);
59
+
60
+ return lines
61
+ .filter(
62
+ (line) =>
63
+ !line.includes("Error:") &&
64
+ !line.includes("renderWithHooks") &&
65
+ !line.includes("react-stack-bottom-frame"),
66
+ )
67
+ .map((line) => line.replace(/https?:\/\/[^/\s)]+/g, "").trim())
68
+ .reverse()
69
+ .join("\n");
70
+ }
@@ -0,0 +1,2 @@
1
+ export { PerformancePage } from "./PerformancePage.js";
2
+ export type { PerformancePageProps } from "./PerformancePage.js";
@@ -0,0 +1,12 @@
1
+ export interface SubscriptionEntry {
2
+ uuid: string;
3
+ id: string;
4
+ source: string;
5
+ resolve: string;
6
+ status: "pending" | "loaded" | "error";
7
+ startTime: number;
8
+ endTime?: number;
9
+ duration?: number;
10
+ errorType?: string;
11
+ callerStack?: string;
12
+ }
@@ -0,0 +1,53 @@
1
+ import { useState, useEffect } from "react";
2
+ import { SubscriptionPerformanceDetail } from "jazz-tools";
3
+ import type { SubscriptionEntry } from "./types.js";
4
+
5
+ export function usePerformanceEntries(): SubscriptionEntry[] {
6
+ const [entries, setEntries] = useState<SubscriptionEntry[]>([]);
7
+
8
+ useEffect(() => {
9
+ const entriesByUuid = new Map<string, SubscriptionEntry>();
10
+
11
+ const handlePerformanceEntries = (entries: PerformanceEntry[]) => {
12
+ for (const mark of entries) {
13
+ const detail = (mark as PerformanceMark)
14
+ .detail as SubscriptionPerformanceDetail;
15
+
16
+ if (detail?.type !== "jazz-subscription") continue;
17
+
18
+ const prevEntry = entriesByUuid.get(detail.uuid);
19
+
20
+ if (mark.entryType === "mark" && prevEntry) continue;
21
+
22
+ entriesByUuid.set(detail.uuid, {
23
+ uuid: detail.uuid,
24
+ id: detail.id,
25
+ source: detail.source,
26
+ resolve: JSON.stringify(detail.resolve),
27
+ status: detail.status,
28
+ startTime: mark.startTime,
29
+ callerStack: detail.callerStack ?? prevEntry?.callerStack,
30
+ duration: mark.entryType === "mark" ? undefined : mark.duration,
31
+ endTime: mark.startTime + mark.duration,
32
+ errorType: detail.errorType,
33
+ });
34
+ }
35
+ };
36
+
37
+ handlePerformanceEntries(performance.getEntriesByType("mark"));
38
+ handlePerformanceEntries(performance.getEntriesByType("measure"));
39
+
40
+ setEntries(Array.from(entriesByUuid.values()));
41
+
42
+ const observer = new PerformanceObserver((list) => {
43
+ handlePerformanceEntries(list.getEntries());
44
+ setEntries(Array.from(entriesByUuid.values()));
45
+ });
46
+
47
+ observer.observe({ entryTypes: ["mark", "measure"] });
48
+
49
+ return () => observer.disconnect();
50
+ }, []);
51
+
52
+ return entries;
53
+ }
@@ -1,3 +1,6 @@
1
+ import { SubscriptionScope } from "jazz-tools";
2
+
1
3
  if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
4
+ SubscriptionScope.enableProfiling();
2
5
  import("./custom-element.js");
3
6
  }