lopata 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/{chunk-4y88h3dc.js → chunk-5nxa3jfc.js} +252 -77
- package/dist/dashboard/{chunk-3q3dhs4j.css → chunk-pqnphvm2.css} +8 -0
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/src/bindings/do-websocket-bridge.ts +9 -0
- package/src/bindings/durable-object.ts +6 -0
- package/src/bindings/websocket-pair.ts +13 -0
- package/src/cli/dev.ts +3 -0
- package/src/setup-globals.ts +21 -0
- package/src/vite-plugin/config-plugin.ts +6 -2
- package/src/vite-plugin/dev-server-plugin.ts +217 -196
|
@@ -5746,14 +5746,11 @@ function ErrorList() {
|
|
|
5746
5746
|
/* @__PURE__ */ u3("td", {
|
|
5747
5747
|
class: "px-4 py-2.5 text-right",
|
|
5748
5748
|
children: err.traceId ? /* @__PURE__ */ u3("a", {
|
|
5749
|
-
href: `#/traces
|
|
5749
|
+
href: `#/traces/${err.traceId}`,
|
|
5750
5750
|
onClick: (e3) => e3.stopPropagation(),
|
|
5751
5751
|
class: "text-link hover:text-accent-lime text-xs font-mono",
|
|
5752
|
-
children:
|
|
5753
|
-
|
|
5754
|
-
"..."
|
|
5755
|
-
]
|
|
5756
|
-
}, undefined, true, undefined, this) : /* @__PURE__ */ u3("span", {
|
|
5752
|
+
children: err.traceId
|
|
5753
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ u3("span", {
|
|
5757
5754
|
class: "text-text-dim text-xs",
|
|
5758
5755
|
children: "-"
|
|
5759
5756
|
}, undefined, false, undefined, this)
|
|
@@ -5911,14 +5908,30 @@ function ErrorDetailPage({ errorId }) {
|
|
|
5911
5908
|
}, undefined, false, undefined, this)
|
|
5912
5909
|
}, undefined, false, undefined, this)
|
|
5913
5910
|
}, undefined, false, undefined, this),
|
|
5914
|
-
|
|
5911
|
+
detail.traceId && /* @__PURE__ */ u3(Section, {
|
|
5915
5912
|
title: "Trace",
|
|
5916
5913
|
open: true,
|
|
5917
|
-
children:
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5914
|
+
children: [
|
|
5915
|
+
/* @__PURE__ */ u3("div", {
|
|
5916
|
+
class: "px-4 py-2.5 border-b border-border-subtle flex items-center gap-2",
|
|
5917
|
+
children: [
|
|
5918
|
+
/* @__PURE__ */ u3("span", {
|
|
5919
|
+
class: "text-xs text-text-muted",
|
|
5920
|
+
children: "Trace ID:"
|
|
5921
|
+
}, undefined, false, undefined, this),
|
|
5922
|
+
/* @__PURE__ */ u3("a", {
|
|
5923
|
+
href: `#/traces/${detail.traceId}`,
|
|
5924
|
+
class: "text-link hover:text-accent-lime text-xs font-mono",
|
|
5925
|
+
children: detail.traceId
|
|
5926
|
+
}, undefined, false, undefined, this)
|
|
5927
|
+
]
|
|
5928
|
+
}, undefined, true, undefined, this),
|
|
5929
|
+
traceSpans && traceSpans.length > 0 && /* @__PURE__ */ u3(SimpleTraceWaterfall, {
|
|
5930
|
+
spans: traceSpans,
|
|
5931
|
+
highlightSpanId: detail.spanId
|
|
5932
|
+
}, undefined, false, undefined, this)
|
|
5933
|
+
]
|
|
5934
|
+
}, undefined, true, undefined, this),
|
|
5922
5935
|
data.request.method && data.request.url && /* @__PURE__ */ u3(Section, {
|
|
5923
5936
|
title: "Request",
|
|
5924
5937
|
open: true,
|
|
@@ -7582,6 +7595,14 @@ function ScheduledList() {
|
|
|
7582
7595
|
}
|
|
7583
7596
|
|
|
7584
7597
|
// src/dashboard/views/traces.tsx
|
|
7598
|
+
var SOURCE_BADGE_STYLES = {
|
|
7599
|
+
fetch: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
|
|
7600
|
+
scheduled: { bg: "var(--color-badge-purple-bg)", color: "var(--color-badge-purple-text)" },
|
|
7601
|
+
queue: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
|
|
7602
|
+
alarm: { bg: "var(--color-badge-yellow-bg)", color: "var(--color-badge-yellow-text)" },
|
|
7603
|
+
workflow: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" }
|
|
7604
|
+
};
|
|
7605
|
+
var DEFAULT_BADGE_STYLE = { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" };
|
|
7585
7606
|
var eventListeners = new Set;
|
|
7586
7607
|
function onTraceEvents(fn) {
|
|
7587
7608
|
eventListeners.add(fn);
|
|
@@ -7596,6 +7617,39 @@ function emitTraceEvents(events) {
|
|
|
7596
7617
|
} catch {}
|
|
7597
7618
|
}
|
|
7598
7619
|
}
|
|
7620
|
+
function useTraceData(traceId) {
|
|
7621
|
+
const [spans, setSpans] = d2([]);
|
|
7622
|
+
const [events, setEvents] = d2([]);
|
|
7623
|
+
const [traceErrors, setTraceErrors] = d2([]);
|
|
7624
|
+
const [isLoading, setIsLoading] = d2(true);
|
|
7625
|
+
y2(() => {
|
|
7626
|
+
setIsLoading(true);
|
|
7627
|
+
rpc("traces.getTrace", { traceId }).then((data) => {
|
|
7628
|
+
setSpans(data.spans);
|
|
7629
|
+
setEvents(data.events);
|
|
7630
|
+
setIsLoading(false);
|
|
7631
|
+
});
|
|
7632
|
+
rpc("traces.errors", { traceId }).then(setTraceErrors).catch(() => {});
|
|
7633
|
+
}, [traceId]);
|
|
7634
|
+
y2(() => {
|
|
7635
|
+
return onTraceEvents((traceEvents) => {
|
|
7636
|
+
for (const ev of traceEvents) {
|
|
7637
|
+
if (ev.type === "span.start" && ev.span.traceId === traceId) {
|
|
7638
|
+
setSpans((prev) => {
|
|
7639
|
+
if (prev.some((s3) => s3.spanId === ev.span.spanId))
|
|
7640
|
+
return prev;
|
|
7641
|
+
return [...prev, ev.span];
|
|
7642
|
+
});
|
|
7643
|
+
} else if (ev.type === "span.end" && ev.span.traceId === traceId) {
|
|
7644
|
+
setSpans((prev) => prev.map((s3) => s3.spanId === ev.span.spanId ? ev.span : s3));
|
|
7645
|
+
} else if (ev.type === "span.event" && ev.event.traceId === traceId) {
|
|
7646
|
+
setEvents((prev) => [...prev, ev.event]);
|
|
7647
|
+
}
|
|
7648
|
+
}
|
|
7649
|
+
});
|
|
7650
|
+
}, [traceId]);
|
|
7651
|
+
return { spans, events, traceErrors, isLoading };
|
|
7652
|
+
}
|
|
7599
7653
|
function useTraceStream() {
|
|
7600
7654
|
const [traces, setTraces] = d2(new Map);
|
|
7601
7655
|
const [wsStatus, setWsStatus] = d2("connecting");
|
|
@@ -7707,7 +7761,16 @@ var TIME_RANGE_OPTIONS = [
|
|
|
7707
7761
|
{ label: "24h", ms: 24 * 60 * 60 * 1000 },
|
|
7708
7762
|
{ label: "All", ms: 0 }
|
|
7709
7763
|
];
|
|
7710
|
-
function TracesView() {
|
|
7764
|
+
function TracesView({ route }) {
|
|
7765
|
+
const traceIdFromRoute = route.startsWith("/traces/") ? route.slice("/traces/".length) : null;
|
|
7766
|
+
if (traceIdFromRoute) {
|
|
7767
|
+
return /* @__PURE__ */ u3(TraceDetailPage, {
|
|
7768
|
+
traceId: traceIdFromRoute
|
|
7769
|
+
}, undefined, false, undefined, this);
|
|
7770
|
+
}
|
|
7771
|
+
return /* @__PURE__ */ u3(TracesListView, {}, undefined, false, undefined, this);
|
|
7772
|
+
}
|
|
7773
|
+
function TracesListView() {
|
|
7711
7774
|
const { traces, setFilter, wsStatus } = useTraceStream();
|
|
7712
7775
|
const [selectedTraceId, setSelectedTraceId] = d2(null);
|
|
7713
7776
|
const [pathFilter, setPathFilter] = d2("");
|
|
@@ -7794,10 +7857,7 @@ function TracesView() {
|
|
|
7794
7857
|
]
|
|
7795
7858
|
}, undefined, true, undefined, this),
|
|
7796
7859
|
/* @__PURE__ */ u3("button", {
|
|
7797
|
-
onClick: () =>
|
|
7798
|
-
clearTraces.mutate();
|
|
7799
|
-
setSelectedTraceId(null);
|
|
7800
|
-
},
|
|
7860
|
+
onClick: () => clearTraces.mutate(),
|
|
7801
7861
|
class: "rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all",
|
|
7802
7862
|
children: "Clear all"
|
|
7803
7863
|
}, undefined, false, undefined, this)
|
|
@@ -8001,6 +8061,132 @@ function TracesView() {
|
|
|
8001
8061
|
]
|
|
8002
8062
|
}, undefined, true, undefined, this);
|
|
8003
8063
|
}
|
|
8064
|
+
function TraceDetailPage({ traceId }) {
|
|
8065
|
+
const { spans, events, traceErrors, isLoading } = useTraceData(traceId);
|
|
8066
|
+
const rootSpan = spans.find((s3) => !s3.parentSpanId);
|
|
8067
|
+
return /* @__PURE__ */ u3("div", {
|
|
8068
|
+
class: "p-4 sm:p-8 h-full flex flex-col",
|
|
8069
|
+
children: [
|
|
8070
|
+
/* @__PURE__ */ u3("div", {
|
|
8071
|
+
class: "flex items-center gap-3 mb-6",
|
|
8072
|
+
children: [
|
|
8073
|
+
/* @__PURE__ */ u3("button", {
|
|
8074
|
+
onClick: () => navigate("/traces"),
|
|
8075
|
+
class: "flex items-center gap-1.5 text-sm text-text-secondary hover:text-ink transition-colors",
|
|
8076
|
+
children: [
|
|
8077
|
+
/* @__PURE__ */ u3("svg", {
|
|
8078
|
+
class: "w-4 h-4",
|
|
8079
|
+
viewBox: "0 0 20 20",
|
|
8080
|
+
fill: "currentColor",
|
|
8081
|
+
children: /* @__PURE__ */ u3("path", {
|
|
8082
|
+
"fill-rule": "evenodd",
|
|
8083
|
+
d: "M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z",
|
|
8084
|
+
"clip-rule": "evenodd"
|
|
8085
|
+
}, undefined, false, undefined, this)
|
|
8086
|
+
}, undefined, false, undefined, this),
|
|
8087
|
+
"Back to traces"
|
|
8088
|
+
]
|
|
8089
|
+
}, undefined, true, undefined, this),
|
|
8090
|
+
/* @__PURE__ */ u3("div", {
|
|
8091
|
+
class: "h-5 w-px bg-border"
|
|
8092
|
+
}, undefined, false, undefined, this),
|
|
8093
|
+
/* @__PURE__ */ u3("div", {
|
|
8094
|
+
children: [
|
|
8095
|
+
/* @__PURE__ */ u3("div", {
|
|
8096
|
+
class: "flex items-center gap-2",
|
|
8097
|
+
children: [
|
|
8098
|
+
rootSpan && /* @__PURE__ */ u3(TraceStatusBadge, {
|
|
8099
|
+
status: rootSpan.status
|
|
8100
|
+
}, undefined, false, undefined, this),
|
|
8101
|
+
/* @__PURE__ */ u3("h1", {
|
|
8102
|
+
class: "text-lg font-bold text-ink",
|
|
8103
|
+
children: rootSpan?.name ?? "Loading..."
|
|
8104
|
+
}, undefined, false, undefined, this)
|
|
8105
|
+
]
|
|
8106
|
+
}, undefined, true, undefined, this),
|
|
8107
|
+
/* @__PURE__ */ u3("div", {
|
|
8108
|
+
class: "text-xs text-text-muted font-mono mt-0.5",
|
|
8109
|
+
children: [
|
|
8110
|
+
"Trace ",
|
|
8111
|
+
traceId,
|
|
8112
|
+
rootSpan?.workerName && /* @__PURE__ */ u3("span", {
|
|
8113
|
+
class: "ml-2 inline-flex px-2 py-0.5 rounded-md text-xs font-medium bg-panel-hover text-text-secondary",
|
|
8114
|
+
children: rootSpan.workerName
|
|
8115
|
+
}, undefined, false, undefined, this),
|
|
8116
|
+
rootSpan?.durationMs != null && /* @__PURE__ */ u3("span", {
|
|
8117
|
+
class: "ml-2 text-text-secondary",
|
|
8118
|
+
children: formatDuration(rootSpan.durationMs)
|
|
8119
|
+
}, undefined, false, undefined, this)
|
|
8120
|
+
]
|
|
8121
|
+
}, undefined, true, undefined, this)
|
|
8122
|
+
]
|
|
8123
|
+
}, undefined, true, undefined, this)
|
|
8124
|
+
]
|
|
8125
|
+
}, undefined, true, undefined, this),
|
|
8126
|
+
/* @__PURE__ */ u3("div", {
|
|
8127
|
+
class: "flex-1 overflow-y-auto scrollbar-thin min-h-0",
|
|
8128
|
+
children: isLoading ? /* @__PURE__ */ u3("div", {
|
|
8129
|
+
class: "text-text-muted text-sm py-12 text-center",
|
|
8130
|
+
children: "Loading trace..."
|
|
8131
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ u3("div", {
|
|
8132
|
+
children: [
|
|
8133
|
+
traceErrors.length > 0 && /* @__PURE__ */ u3("div", {
|
|
8134
|
+
class: "mb-4",
|
|
8135
|
+
children: [
|
|
8136
|
+
/* @__PURE__ */ u3("div", {
|
|
8137
|
+
class: "text-xs font-medium text-text-muted uppercase tracking-wider mb-2",
|
|
8138
|
+
children: [
|
|
8139
|
+
"Errors (",
|
|
8140
|
+
traceErrors.length,
|
|
8141
|
+
")"
|
|
8142
|
+
]
|
|
8143
|
+
}, undefined, true, undefined, this),
|
|
8144
|
+
/* @__PURE__ */ u3("div", {
|
|
8145
|
+
class: "space-y-1",
|
|
8146
|
+
children: traceErrors.map((err) => /* @__PURE__ */ u3("a", {
|
|
8147
|
+
href: `#/errors/${err.id}`,
|
|
8148
|
+
class: "flex items-center gap-2 px-3 py-2 rounded-md text-xs no-underline transition-colors",
|
|
8149
|
+
style: { background: "var(--color-error-highlight)", borderColor: "var(--color-error-ring)" },
|
|
8150
|
+
children: [
|
|
8151
|
+
err.source && (() => {
|
|
8152
|
+
const s3 = SOURCE_BADGE_STYLES[err.source] ?? DEFAULT_BADGE_STYLE;
|
|
8153
|
+
return /* @__PURE__ */ u3("span", {
|
|
8154
|
+
class: "inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium",
|
|
8155
|
+
style: { background: s3.bg, color: s3.color },
|
|
8156
|
+
children: err.source
|
|
8157
|
+
}, undefined, false, undefined, this);
|
|
8158
|
+
})(),
|
|
8159
|
+
/* @__PURE__ */ u3("span", {
|
|
8160
|
+
class: "font-medium",
|
|
8161
|
+
style: { color: "var(--color-badge-red-text)" },
|
|
8162
|
+
children: err.errorName
|
|
8163
|
+
}, undefined, false, undefined, this),
|
|
8164
|
+
/* @__PURE__ */ u3("span", {
|
|
8165
|
+
style: { color: "var(--color-badge-red-text)" },
|
|
8166
|
+
class: "truncate",
|
|
8167
|
+
children: err.errorMessage
|
|
8168
|
+
}, undefined, false, undefined, this),
|
|
8169
|
+
/* @__PURE__ */ u3("span", {
|
|
8170
|
+
style: { color: "var(--color-badge-red-text)", opacity: 0.7 },
|
|
8171
|
+
class: "font-mono ml-auto flex-shrink-0",
|
|
8172
|
+
children: formatTimestamp(err.timestamp)
|
|
8173
|
+
}, undefined, false, undefined, this)
|
|
8174
|
+
]
|
|
8175
|
+
}, err.id, true, undefined, this))
|
|
8176
|
+
}, undefined, false, undefined, this)
|
|
8177
|
+
]
|
|
8178
|
+
}, undefined, true, undefined, this),
|
|
8179
|
+
/* @__PURE__ */ u3(TraceWaterfall, {
|
|
8180
|
+
spans,
|
|
8181
|
+
events,
|
|
8182
|
+
onAddAttributeFilter: () => {}
|
|
8183
|
+
}, undefined, false, undefined, this)
|
|
8184
|
+
]
|
|
8185
|
+
}, undefined, true, undefined, this)
|
|
8186
|
+
}, undefined, false, undefined, this)
|
|
8187
|
+
]
|
|
8188
|
+
}, undefined, true, undefined, this);
|
|
8189
|
+
}
|
|
8004
8190
|
function SpansListTab() {
|
|
8005
8191
|
const [spans, setSpans] = d2([]);
|
|
8006
8192
|
const [cursor, setCursor] = d2(null);
|
|
@@ -8098,11 +8284,8 @@ function SpansListTab() {
|
|
|
8098
8284
|
children: /* @__PURE__ */ u3("button", {
|
|
8099
8285
|
onClick: () => setSelectedTraceId(span.traceId),
|
|
8100
8286
|
class: "text-link hover:text-accent-lime text-xs font-mono",
|
|
8101
|
-
children:
|
|
8102
|
-
|
|
8103
|
-
"..."
|
|
8104
|
-
]
|
|
8105
|
-
}, undefined, true, undefined, this)
|
|
8287
|
+
children: span.traceId
|
|
8288
|
+
}, undefined, false, undefined, this)
|
|
8106
8289
|
}, undefined, false, undefined, this)
|
|
8107
8290
|
]
|
|
8108
8291
|
}, span.spanId, true, undefined, this))
|
|
@@ -8126,8 +8309,7 @@ function SpansListTab() {
|
|
|
8126
8309
|
}, undefined, false, undefined, this),
|
|
8127
8310
|
selectedTraceId && /* @__PURE__ */ u3(TraceDrawer, {
|
|
8128
8311
|
traceId: selectedTraceId,
|
|
8129
|
-
onClose: () => setSelectedTraceId(null)
|
|
8130
|
-
onAddAttributeFilter: () => {}
|
|
8312
|
+
onClose: () => setSelectedTraceId(null)
|
|
8131
8313
|
}, undefined, false, undefined, this)
|
|
8132
8314
|
]
|
|
8133
8315
|
}, undefined, true, undefined, this);
|
|
@@ -8136,6 +8318,7 @@ function LogsListTab() {
|
|
|
8136
8318
|
const [logs, setLogs] = d2([]);
|
|
8137
8319
|
const [cursor, setCursor] = d2(null);
|
|
8138
8320
|
const [isLoading, setIsLoading] = d2(true);
|
|
8321
|
+
const [selectedTraceId, setSelectedTraceId] = d2(null);
|
|
8139
8322
|
const loadLogs = q2((cur) => {
|
|
8140
8323
|
setIsLoading(true);
|
|
8141
8324
|
rpc("traces.listLogs", { limit: 50, cursor: cur }).then((data) => {
|
|
@@ -8216,12 +8399,13 @@ function LogsListTab() {
|
|
|
8216
8399
|
children: formatTimestamp(log.timestamp)
|
|
8217
8400
|
}, undefined, false, undefined, this),
|
|
8218
8401
|
/* @__PURE__ */ u3("td", {
|
|
8219
|
-
class: "px-4 py-2.5 text-right
|
|
8220
|
-
children:
|
|
8221
|
-
log.traceId
|
|
8222
|
-
"
|
|
8223
|
-
|
|
8224
|
-
|
|
8402
|
+
class: "px-4 py-2.5 text-right",
|
|
8403
|
+
children: /* @__PURE__ */ u3("button", {
|
|
8404
|
+
onClick: () => setSelectedTraceId(log.traceId),
|
|
8405
|
+
class: "text-link hover:text-accent-lime text-xs font-mono",
|
|
8406
|
+
children: log.traceId
|
|
8407
|
+
}, undefined, false, undefined, this)
|
|
8408
|
+
}, undefined, false, undefined, this)
|
|
8225
8409
|
]
|
|
8226
8410
|
}, log.id, true, undefined, this))
|
|
8227
8411
|
}, undefined, false, undefined, this)
|
|
@@ -8241,49 +8425,16 @@ function LogsListTab() {
|
|
|
8241
8425
|
isLoading && logs.length === 0 && /* @__PURE__ */ u3("div", {
|
|
8242
8426
|
class: "text-text-muted text-sm text-center py-12",
|
|
8243
8427
|
children: "Loading logs..."
|
|
8428
|
+
}, undefined, false, undefined, this),
|
|
8429
|
+
selectedTraceId && /* @__PURE__ */ u3(TraceDrawer, {
|
|
8430
|
+
traceId: selectedTraceId,
|
|
8431
|
+
onClose: () => setSelectedTraceId(null)
|
|
8244
8432
|
}, undefined, false, undefined, this)
|
|
8245
8433
|
]
|
|
8246
8434
|
}, undefined, true, undefined, this);
|
|
8247
8435
|
}
|
|
8248
|
-
var SOURCE_BADGE_STYLES = {
|
|
8249
|
-
fetch: { bg: "var(--color-badge-blue-bg)", color: "var(--color-badge-blue-text)" },
|
|
8250
|
-
scheduled: { bg: "var(--color-badge-purple-bg)", color: "var(--color-badge-purple-text)" },
|
|
8251
|
-
queue: { bg: "var(--color-badge-orange-bg)", color: "var(--color-badge-orange-text)" },
|
|
8252
|
-
alarm: { bg: "var(--color-badge-yellow-bg)", color: "var(--color-badge-yellow-text)" },
|
|
8253
|
-
workflow: { bg: "var(--color-badge-emerald-bg)", color: "var(--color-badge-emerald-text)" }
|
|
8254
|
-
};
|
|
8255
|
-
var DEFAULT_BADGE_STYLE = { bg: "var(--color-badge-red-bg)", color: "var(--color-badge-red-text)" };
|
|
8256
8436
|
function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
|
|
8257
|
-
const
|
|
8258
|
-
const [events, setEvents] = d2([]);
|
|
8259
|
-
const [traceErrors, setTraceErrors] = d2([]);
|
|
8260
|
-
const [isLoading, setIsLoading] = d2(true);
|
|
8261
|
-
y2(() => {
|
|
8262
|
-
setIsLoading(true);
|
|
8263
|
-
rpc("traces.getTrace", { traceId }).then((data) => {
|
|
8264
|
-
setSpans(data.spans);
|
|
8265
|
-
setEvents(data.events);
|
|
8266
|
-
setIsLoading(false);
|
|
8267
|
-
});
|
|
8268
|
-
rpc("traces.errors", { traceId }).then(setTraceErrors).catch(() => {});
|
|
8269
|
-
}, [traceId]);
|
|
8270
|
-
y2(() => {
|
|
8271
|
-
return onTraceEvents((traceEvents) => {
|
|
8272
|
-
for (const ev of traceEvents) {
|
|
8273
|
-
if (ev.type === "span.start" && ev.span.traceId === traceId) {
|
|
8274
|
-
setSpans((prev) => {
|
|
8275
|
-
if (prev.some((s3) => s3.spanId === ev.span.spanId))
|
|
8276
|
-
return prev;
|
|
8277
|
-
return [...prev, ev.span];
|
|
8278
|
-
});
|
|
8279
|
-
} else if (ev.type === "span.end" && ev.span.traceId === traceId) {
|
|
8280
|
-
setSpans((prev) => prev.map((s3) => s3.spanId === ev.span.spanId ? ev.span : s3));
|
|
8281
|
-
} else if (ev.type === "span.event" && ev.event.traceId === traceId) {
|
|
8282
|
-
setEvents((prev) => [...prev, ev.event]);
|
|
8283
|
-
}
|
|
8284
|
-
}
|
|
8285
|
-
});
|
|
8286
|
-
}, [traceId]);
|
|
8437
|
+
const { spans, events, traceErrors, isLoading } = useTraceData(traceId);
|
|
8287
8438
|
return /* @__PURE__ */ u3(k, {
|
|
8288
8439
|
children: [
|
|
8289
8440
|
/* @__PURE__ */ u3("div", {
|
|
@@ -8302,8 +8453,7 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
|
|
|
8302
8453
|
class: "text-xs text-text-muted font-mono",
|
|
8303
8454
|
children: [
|
|
8304
8455
|
"Trace ",
|
|
8305
|
-
traceId
|
|
8306
|
-
"..."
|
|
8456
|
+
traceId
|
|
8307
8457
|
]
|
|
8308
8458
|
}, undefined, true, undefined, this),
|
|
8309
8459
|
/* @__PURE__ */ u3("div", {
|
|
@@ -8312,11 +8462,34 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
|
|
|
8312
8462
|
}, undefined, false, undefined, this)
|
|
8313
8463
|
]
|
|
8314
8464
|
}, undefined, true, undefined, this),
|
|
8315
|
-
/* @__PURE__ */ u3("
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8465
|
+
/* @__PURE__ */ u3("div", {
|
|
8466
|
+
class: "flex items-center gap-1",
|
|
8467
|
+
children: [
|
|
8468
|
+
/* @__PURE__ */ u3("a", {
|
|
8469
|
+
href: `#/traces/${traceId}`,
|
|
8470
|
+
class: "w-7 h-7 flex items-center justify-center rounded-md hover:bg-panel-hover transition-colors text-text-muted hover:text-ink",
|
|
8471
|
+
title: "Open full page",
|
|
8472
|
+
children: /* @__PURE__ */ u3("svg", {
|
|
8473
|
+
class: "w-4 h-4",
|
|
8474
|
+
viewBox: "0 0 20 20",
|
|
8475
|
+
fill: "currentColor",
|
|
8476
|
+
children: [
|
|
8477
|
+
/* @__PURE__ */ u3("path", {
|
|
8478
|
+
d: "M4.75 5.75a.75.75 0 00.75.75h4.69l-4.22 4.22a.75.75 0 001.06 1.06L11.25 7.56v4.69a.75.75 0 001.5 0v-6.5a.75.75 0 00-.75-.75h-6.5a.75.75 0 00-.75.75z"
|
|
8479
|
+
}, undefined, false, undefined, this),
|
|
8480
|
+
/* @__PURE__ */ u3("path", {
|
|
8481
|
+
d: "M3 13.5a1.5 1.5 0 011.5-1.5h1.25a.75.75 0 010 1.5H4.5v4h4v-1.25a.75.75 0 011.5 0v1.25a1.5 1.5 0 01-1.5 1.5h-4A1.5 1.5 0 013 17.5v-4z"
|
|
8482
|
+
}, undefined, false, undefined, this)
|
|
8483
|
+
]
|
|
8484
|
+
}, undefined, true, undefined, this)
|
|
8485
|
+
}, undefined, false, undefined, this),
|
|
8486
|
+
/* @__PURE__ */ u3("button", {
|
|
8487
|
+
onClick: onClose,
|
|
8488
|
+
class: "w-7 h-7 flex items-center justify-center rounded-md hover:bg-panel-hover transition-colors text-text-muted hover:text-ink",
|
|
8489
|
+
children: "×"
|
|
8490
|
+
}, undefined, false, undefined, this)
|
|
8491
|
+
]
|
|
8492
|
+
}, undefined, true, undefined, this)
|
|
8320
8493
|
]
|
|
8321
8494
|
}, undefined, true, undefined, this),
|
|
8322
8495
|
/* @__PURE__ */ u3("div", {
|
|
@@ -8375,7 +8548,7 @@ function TraceDrawer({ traceId, onClose, onAddAttributeFilter }) {
|
|
|
8375
8548
|
/* @__PURE__ */ u3(TraceWaterfall, {
|
|
8376
8549
|
spans,
|
|
8377
8550
|
events,
|
|
8378
|
-
onAddAttributeFilter
|
|
8551
|
+
onAddAttributeFilter: onAddAttributeFilter ?? (() => {})
|
|
8379
8552
|
}, undefined, false, undefined, this)
|
|
8380
8553
|
]
|
|
8381
8554
|
}, undefined, true, undefined, this)
|
|
@@ -9361,7 +9534,9 @@ function App() {
|
|
|
9361
9534
|
route
|
|
9362
9535
|
}, undefined, false, undefined, this);
|
|
9363
9536
|
if (route.startsWith("/traces"))
|
|
9364
|
-
return /* @__PURE__ */ u3(TracesView, {
|
|
9537
|
+
return /* @__PURE__ */ u3(TracesView, {
|
|
9538
|
+
route
|
|
9539
|
+
}, undefined, false, undefined, this);
|
|
9365
9540
|
if (route.startsWith("/workers"))
|
|
9366
9541
|
return /* @__PURE__ */ u3(WorkersView, {}, undefined, false, undefined, this);
|
|
9367
9542
|
if (route.startsWith("/kv"))
|
|
@@ -828,6 +828,10 @@
|
|
|
828
828
|
width: 100%;
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
+
.w-px {
|
|
832
|
+
width: 1px;
|
|
833
|
+
}
|
|
834
|
+
|
|
831
835
|
.max-w-4xl {
|
|
832
836
|
max-width: var(--container-4xl);
|
|
833
837
|
}
|
|
@@ -1378,6 +1382,10 @@
|
|
|
1378
1382
|
background-color: var(--color-blue-600);
|
|
1379
1383
|
}
|
|
1380
1384
|
|
|
1385
|
+
.bg-border {
|
|
1386
|
+
background-color: var(--color-border);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1381
1389
|
.bg-cyan-500\/15 {
|
|
1382
1390
|
background-color: #00b7d726;
|
|
1383
1391
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
10
10
|
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-pqnphvm2.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-5nxa3jfc.js"></script></head>
|
|
12
12
|
<body class="h-full bg-surface text-ink" style="font-family: system-ui, -apple-system, sans-serif;">
|
|
13
13
|
<script>
|
|
14
14
|
// Apply saved theme before first paint to prevent flash
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ export class BridgeWebSocket extends EventTarget {
|
|
|
28
28
|
readonly wsId: string
|
|
29
29
|
readyState = 1 // OPEN
|
|
30
30
|
private _postMessage: (msg: WsBridgeOutbound) => void
|
|
31
|
+
private _attachment: any = null
|
|
31
32
|
|
|
32
33
|
constructor(wsId: string, postMessage: (msg: WsBridgeOutbound) => void) {
|
|
33
34
|
super()
|
|
@@ -35,6 +36,14 @@ export class BridgeWebSocket extends EventTarget {
|
|
|
35
36
|
this._postMessage = postMessage
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
serializeAttachment(attachment: any): void {
|
|
40
|
+
this._attachment = JSON.parse(JSON.stringify(attachment))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deserializeAttachment(): any | null {
|
|
44
|
+
return this._attachment
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
send(data: string | ArrayBuffer): void {
|
|
39
48
|
if (this.readyState !== 1) return
|
|
40
49
|
this._postMessage({ type: 'ws-send', wsId: this.wsId, data })
|
|
@@ -602,6 +602,12 @@ export class DurableObjectStateImpl {
|
|
|
602
602
|
if (this._acceptedWebSockets.size >= this._limits.maxConcurrentWebSockets) {
|
|
603
603
|
throw new Error(`Exceeded max concurrent WebSocket connections (${this._limits.maxConcurrentWebSockets})`)
|
|
604
604
|
}
|
|
605
|
+
|
|
606
|
+
// Implicitly accept the WebSocket (in CF production, ctx.acceptWebSocket handles this)
|
|
607
|
+
if ('accept' in ws && typeof ws.accept === 'function') {
|
|
608
|
+
;(ws as any).accept()
|
|
609
|
+
}
|
|
610
|
+
|
|
605
611
|
const entry: AcceptedWebSocket = { ws, tags: tagList, autoResponseTimestamp: null }
|
|
606
612
|
this._acceptedWebSockets.add(entry)
|
|
607
613
|
|
|
@@ -41,6 +41,7 @@ export class CFWebSocket extends EventTarget {
|
|
|
41
41
|
/** @internal */ _peer: CFWebSocket | null = null
|
|
42
42
|
/** @internal */ _accepted = false
|
|
43
43
|
/** @internal */ _eventQueue: WSEvent[] = []
|
|
44
|
+
/** @internal */ _attachment: any = null
|
|
44
45
|
|
|
45
46
|
// Callback-style handlers (standard WebSocket compat)
|
|
46
47
|
onopen: ((ev: Event) => void) | null = null
|
|
@@ -48,6 +49,18 @@ export class CFWebSocket extends EventTarget {
|
|
|
48
49
|
onclose: ((ev: CloseEvent) => void) | null = null
|
|
49
50
|
onerror: ((ev: Event) => void) | null = null
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* CF-specific: attach serializable data to this WebSocket.
|
|
54
|
+
* Survives hibernation in production; here it's just in-memory.
|
|
55
|
+
*/
|
|
56
|
+
serializeAttachment(attachment: any): void {
|
|
57
|
+
this._attachment = JSON.parse(JSON.stringify(attachment))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deserializeAttachment(): any | null {
|
|
61
|
+
return this._attachment
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
/**
|
|
52
65
|
* CF-specific: begin dispatching events.
|
|
53
66
|
* Must be called before messages can be sent or received.
|
package/src/cli/dev.ts
CHANGED
|
@@ -333,6 +333,9 @@ export async function run(ctx: CliContext) {
|
|
|
333
333
|
const ce = ev as CloseEvent
|
|
334
334
|
ws.close(ce.code, ce.reason)
|
|
335
335
|
})
|
|
336
|
+
// Accept the client side so events from server.send() are dispatched
|
|
337
|
+
// (in CF production the runtime handles this; here we bridge manually)
|
|
338
|
+
cfSocket.accept()
|
|
336
339
|
},
|
|
337
340
|
message(ws, message) {
|
|
338
341
|
const data = ws.data as unknown as Record<string, unknown>
|
package/src/setup-globals.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SqliteCacheStorage } from './bindings/cache'
|
|
2
2
|
import { FixedLengthStream, IdentityTransformStream } from './bindings/cf-streams'
|
|
3
3
|
import { patchGlobalCrypto } from './bindings/crypto-extras'
|
|
4
|
+
import { WebSocketRequestResponsePair } from './bindings/durable-object'
|
|
4
5
|
import { HTMLRewriter } from './bindings/html-rewriter'
|
|
5
6
|
import { WebSocketPair } from './bindings/websocket-pair'
|
|
6
7
|
import { getDatabase } from './db'
|
|
@@ -77,6 +78,26 @@ export function setupCloudflareGlobals() {
|
|
|
77
78
|
configurable: true,
|
|
78
79
|
})
|
|
79
80
|
|
|
81
|
+
// Register global `WebSocketRequestResponsePair` class (used by DO hibernation API)
|
|
82
|
+
Object.defineProperty(globalThis, 'WebSocketRequestResponsePair', {
|
|
83
|
+
value: WebSocketRequestResponsePair,
|
|
84
|
+
writable: false,
|
|
85
|
+
configurable: true,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Patch Response to preserve CF-specific `webSocket` property from init
|
|
89
|
+
const OriginalResponse = globalThis.Response
|
|
90
|
+
globalThis.Response = class extends OriginalResponse {
|
|
91
|
+
webSocket?: InstanceType<typeof WebSocketPair>[0]
|
|
92
|
+
|
|
93
|
+
constructor(body?: any, init?: ResponseInit & { webSocket?: InstanceType<typeof WebSocketPair>[0] }) {
|
|
94
|
+
super(body, init)
|
|
95
|
+
if (init && 'webSocket' in init) {
|
|
96
|
+
this.webSocket = init.webSocket
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
// Register global CF stream classes
|
|
81
102
|
Object.defineProperty(globalThis, 'IdentityTransformStream', {
|
|
82
103
|
value: IdentityTransformStream,
|
|
@@ -19,7 +19,7 @@ class LopataDevEnvironment extends DevEnvironment {
|
|
|
19
19
|
|
|
20
20
|
get runner(): ModuleRunner {
|
|
21
21
|
if (!this._runner) {
|
|
22
|
-
this._runner = createServerModuleRunner(this, { hmr:
|
|
22
|
+
this._runner = createServerModuleRunner(this, { hmr: {} })
|
|
23
23
|
}
|
|
24
24
|
return this._runner
|
|
25
25
|
}
|
|
@@ -42,15 +42,19 @@ export function configPlugin(envName: string): Plugin {
|
|
|
42
42
|
name: 'lopata:config',
|
|
43
43
|
config() {
|
|
44
44
|
return {
|
|
45
|
+
// Disable Vite's SPA fallback (history API fallback) — Lopata handles
|
|
46
|
+
// all requests via worker.fetch(), so no index.html rewriting is needed.
|
|
47
|
+
appType: 'custom',
|
|
45
48
|
server: {
|
|
46
49
|
watch: {
|
|
47
|
-
ignored: ['**/.lopata/**'],
|
|
50
|
+
ignored: ['**/.lopata/**', '**/.wrangler/**', '**/.react-router/**'],
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
53
|
environments: {
|
|
51
54
|
[envName]: {
|
|
52
55
|
resolve: {
|
|
53
56
|
externalConditions: ['workerd', 'worker'],
|
|
57
|
+
dedupe: ['react', 'react-dom', 'react-router', 'react-router-dom'],
|
|
54
58
|
},
|
|
55
59
|
dev: {
|
|
56
60
|
createEnvironment(name, config) {
|
|
@@ -52,8 +52,53 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
52
52
|
let currentModule: Record<string, unknown> | null = null
|
|
53
53
|
// Serializes module reload — prevents concurrent wireClassRefs calls
|
|
54
54
|
let reloadLock: Promise<void> | null = null
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Import the worker module through Vite's SSR runner and re-wire
|
|
58
|
+
* class refs when the module identity changes (HMR invalidation).
|
|
59
|
+
* Serialized via reloadLock to prevent concurrent wireClassRefs calls.
|
|
60
|
+
*/
|
|
61
|
+
async function ensureWorkerModule(): Promise<Record<string, unknown>> {
|
|
62
|
+
const ssrEnv = server.environments[options.envName]
|
|
63
|
+
if (!ssrEnv || !('runner' in ssrEnv)) {
|
|
64
|
+
throw new Error(`SSR environment "${options.envName}" not found or has no runner`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entrypoint = resolve(server.config.root, config.main)
|
|
68
|
+
|
|
69
|
+
// Wait for any in-progress reload before importing
|
|
70
|
+
if (reloadLock) await reloadLock
|
|
71
|
+
|
|
72
|
+
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>
|
|
73
|
+
|
|
74
|
+
// Re-wire class refs when module changes (HMR invalidation)
|
|
75
|
+
if (workerModule !== currentModule) {
|
|
76
|
+
if (reloadLock) {
|
|
77
|
+
// Another request started reloading while we were importing — wait for it
|
|
78
|
+
await reloadLock
|
|
79
|
+
} else {
|
|
80
|
+
let resolveReload!: () => void
|
|
81
|
+
reloadLock = new Promise(r => {
|
|
82
|
+
resolveReload = r
|
|
83
|
+
})
|
|
84
|
+
try {
|
|
85
|
+
currentModule = workerModule
|
|
86
|
+
wireClassRefs(registry, workerModule, env, workerRegistry)
|
|
87
|
+
setGlobalEnv(env)
|
|
88
|
+
console.log('[lopata:vite] Worker module (re)loaded, classes wired')
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Reset so next request retries
|
|
91
|
+
currentModule = null
|
|
92
|
+
throw err
|
|
93
|
+
} finally {
|
|
94
|
+
reloadLock = null
|
|
95
|
+
resolveReload()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return currentModule ?? workerModule
|
|
101
|
+
}
|
|
57
102
|
|
|
58
103
|
return {
|
|
59
104
|
name: 'lopata:dev-server',
|
|
@@ -167,23 +212,7 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
167
212
|
apiMod.setWorkerRegistry(workerRegistry)
|
|
168
213
|
}
|
|
169
214
|
|
|
170
|
-
// 5.
|
|
171
|
-
// Collect changed file paths; the next request will invalidate only
|
|
172
|
-
// the affected modules (and their transitive importers) instead of
|
|
173
|
-
// clearing the entire runner cache. HMR on the runner is disabled
|
|
174
|
-
// (hmr: false in config-plugin) so there's no async race.
|
|
175
|
-
server.watcher.on('change', (file) => {
|
|
176
|
-
const ssrEnv = server.environments[options.envName]
|
|
177
|
-
if (!ssrEnv) return
|
|
178
|
-
const normalizedFile = file.replace(/\\/g, '/')
|
|
179
|
-
const mods = ssrEnv.moduleGraph.getModulesByFile(normalizedFile)
|
|
180
|
-
if (mods && mods.size > 0) {
|
|
181
|
-
changedFiles.add(normalizedFile)
|
|
182
|
-
currentModule = null
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// 6. Set up WebSocket trace streaming on httpServer
|
|
215
|
+
// 5. Set up WebSocket trace streaming on httpServer
|
|
187
216
|
setupTraceWebSocket(server)
|
|
188
217
|
|
|
189
218
|
// 6. Return middleware callback (post-middleware — runs after framework plugins)
|
|
@@ -232,63 +261,11 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
try {
|
|
235
|
-
const
|
|
236
|
-
if (!ssrEnv || !('runner' in ssrEnv)) {
|
|
237
|
-
console.error(`[lopata:vite] SSR environment "${options.envName}" not found or has no runner`)
|
|
238
|
-
return next()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const entrypoint = resolve(server.config.root, config.main)
|
|
242
|
-
|
|
243
|
-
// Wait for any in-progress reload before importing
|
|
244
|
-
if (reloadLock) await reloadLock
|
|
245
|
-
|
|
246
|
-
// Granular invalidation: only invalidate modules for changed
|
|
247
|
-
// files and their transitive importers, instead of wiping the
|
|
248
|
-
// entire runner cache. This preserves cached evaluations of
|
|
249
|
-
// unchanged modules for faster re-evaluation.
|
|
250
|
-
if (changedFiles.size > 0) {
|
|
251
|
-
const files = changedFiles
|
|
252
|
-
changedFiles = new Set()
|
|
253
|
-
const runner = (ssrEnv as any).runner
|
|
254
|
-
const count = invalidateChangedModules(runner.evaluatedModules, files)
|
|
255
|
-
if (count > 0) {
|
|
256
|
-
console.log(`[lopata:vite] Invalidated ${count} module(s) (${files.size} file(s) changed)`)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const workerModule = await (ssrEnv as any).runner.import(entrypoint) as Record<string, unknown>
|
|
261
|
-
|
|
262
|
-
// Re-wire class refs when module changes (HMR invalidation)
|
|
263
|
-
if (workerModule !== currentModule) {
|
|
264
|
-
if (reloadLock) {
|
|
265
|
-
// Another request started reloading while we were importing — wait for it
|
|
266
|
-
await reloadLock
|
|
267
|
-
} else {
|
|
268
|
-
let resolveReload!: () => void
|
|
269
|
-
reloadLock = new Promise(r => {
|
|
270
|
-
resolveReload = r
|
|
271
|
-
})
|
|
272
|
-
try {
|
|
273
|
-
currentModule = workerModule
|
|
274
|
-
wireClassRefs(registry, workerModule, env, workerRegistry)
|
|
275
|
-
setGlobalEnv(env)
|
|
276
|
-
console.log('[lopata:vite] Worker module (re)loaded, classes wired')
|
|
277
|
-
} catch (err) {
|
|
278
|
-
// Reset so next request retries
|
|
279
|
-
currentModule = null
|
|
280
|
-
throw err
|
|
281
|
-
} finally {
|
|
282
|
-
reloadLock = null
|
|
283
|
-
resolveReload()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
264
|
+
const activeModule = await ensureWorkerModule()
|
|
287
265
|
|
|
288
266
|
const request = nodeReqToRequest(req)
|
|
289
267
|
const parsedUrl = new URL(request.url)
|
|
290
268
|
|
|
291
|
-
const activeModule = currentModule ?? workerModule
|
|
292
269
|
const handler = activeModule.default as Record<string, unknown>
|
|
293
270
|
if (!handler || typeof handler.fetch !== 'function') {
|
|
294
271
|
console.error('[lopata:vite] Worker module default export has no fetch() method')
|
|
@@ -350,98 +327,28 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
350
327
|
|
|
351
328
|
// Dynamically import ws (available as Vite dependency)
|
|
352
329
|
import('ws').then(({ WebSocketServer }) => {
|
|
353
|
-
const
|
|
330
|
+
const traceWss = new WebSocketServer({ noServer: true })
|
|
331
|
+
const workerWss = new WebSocketServer({ noServer: true })
|
|
354
332
|
|
|
355
333
|
httpServer.on('upgrade', (req: IncomingMessage, socket: any, head: Buffer) => {
|
|
356
334
|
const url = req.url ?? ''
|
|
357
|
-
if (!url.startsWith('/__api/traces/ws')) return
|
|
358
|
-
|
|
359
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
360
|
-
const store = getTraceStore()
|
|
361
|
-
let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: 'include' | 'exclude' }> } = {}
|
|
362
|
-
let buffer: any[] = []
|
|
363
|
-
const MAX_BUFFER = 1000
|
|
364
|
-
const allowedTraces = new Set<string>()
|
|
365
|
-
const excludedTraces = new Set<string>()
|
|
366
|
-
|
|
367
|
-
function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
|
|
368
|
-
if (filter.status && filter.status !== 'all') {
|
|
369
|
-
if (span.status !== 'unset' && span.status !== filter.status) return true
|
|
370
|
-
}
|
|
371
|
-
if (filter.path) {
|
|
372
|
-
if (!matchGlob(span.name, filter.path)) return true
|
|
373
|
-
}
|
|
374
|
-
if (filter.attributeFilters && filter.attributeFilters.length > 0) {
|
|
375
|
-
const attrs = span.attributes
|
|
376
|
-
for (const af of filter.attributeFilters) {
|
|
377
|
-
const val = attrs[af.key]
|
|
378
|
-
const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase())
|
|
379
|
-
if (af.type === 'include' && !matches) return true
|
|
380
|
-
if (af.type === 'exclude' && matches) return true
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return false
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const unsubscribe = store.subscribe((event: any) => {
|
|
387
|
-
const traceId = event.type === 'span.event' ? event.event.traceId : event.span.traceId
|
|
388
|
-
if ((event.type === 'span.start' || event.type === 'span.end') && event.span.parentSpanId === null) {
|
|
389
|
-
if (isRootSpanFiltered(event.span)) {
|
|
390
|
-
excludedTraces.add(traceId)
|
|
391
|
-
allowedTraces.delete(traceId)
|
|
392
|
-
return
|
|
393
|
-
}
|
|
394
|
-
excludedTraces.delete(traceId)
|
|
395
|
-
allowedTraces.add(traceId)
|
|
396
|
-
} else {
|
|
397
|
-
if (excludedTraces.has(traceId)) return
|
|
398
|
-
}
|
|
399
|
-
if (buffer.length < MAX_BUFFER) {
|
|
400
|
-
buffer.push(event)
|
|
401
|
-
}
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
const interval = setInterval(() => {
|
|
405
|
-
if (buffer.length > 0) {
|
|
406
|
-
ws.send(JSON.stringify({ type: 'batch', events: buffer }))
|
|
407
|
-
buffer = []
|
|
408
|
-
}
|
|
409
|
-
}, 500)
|
|
410
335
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const pathParam = reqUrl.searchParams.get('path')
|
|
416
|
-
if (statusParam) filter.status = statusParam
|
|
417
|
-
if (pathParam) filter.path = pathParam
|
|
418
|
-
} catch {}
|
|
419
|
-
|
|
420
|
-
let sinceMs = 15 * 60 * 1000
|
|
421
|
-
const since = Date.now() - sinceMs
|
|
422
|
-
const recent = store.getRecentTraces(since, 200, filter)
|
|
423
|
-
ws.send(JSON.stringify({ type: 'initial', traces: recent }))
|
|
336
|
+
// Skip Vite HMR WebSocket — Vite uses sec-websocket-protocol
|
|
337
|
+
// "vite-hmr" / "vite-ping" to identify its connections
|
|
338
|
+
const wsProtocol = req.headers['sec-websocket-protocol']
|
|
339
|
+
if (wsProtocol === 'vite-hmr' || wsProtocol === 'vite-ping') return
|
|
424
340
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (msg.type === 'filter') {
|
|
429
|
-
filter = { path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters }
|
|
430
|
-
if (msg.sinceMs !== undefined) sinceMs = msg.sinceMs
|
|
431
|
-
allowedTraces.clear()
|
|
432
|
-
excludedTraces.clear()
|
|
433
|
-
const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0
|
|
434
|
-
const freshTraces = store.getRecentTraces(freshSince, 200, filter)
|
|
435
|
-
ws.send(JSON.stringify({ type: 'initial', traces: freshTraces }))
|
|
436
|
-
}
|
|
437
|
-
} catch {}
|
|
341
|
+
if (url.startsWith('/__api/traces/ws')) {
|
|
342
|
+
traceWss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
343
|
+
handleTraceWebSocket(ws, req)
|
|
438
344
|
})
|
|
345
|
+
return
|
|
346
|
+
}
|
|
439
347
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
})
|
|
348
|
+
// Worker WebSocket upgrade — bridge to CF WebSocketPair
|
|
349
|
+
if (req.headers.upgrade?.toLowerCase() === 'websocket') {
|
|
350
|
+
handleWorkerWebSocketUpgrade(workerWss, req, socket, head)
|
|
351
|
+
}
|
|
445
352
|
})
|
|
446
353
|
|
|
447
354
|
console.log('[lopata:vite] Dashboard: http://localhost:5173/__dashboard')
|
|
@@ -450,51 +357,165 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
450
357
|
console.log('[lopata:vite] Dashboard available (trace streaming disabled — ws package not found)')
|
|
451
358
|
})
|
|
452
359
|
}
|
|
453
|
-
}
|
|
454
360
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
361
|
+
function handleTraceWebSocket(ws: any, req: IncomingMessage) {
|
|
362
|
+
const store = getTraceStore()
|
|
363
|
+
let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: 'include' | 'exclude' }> } = {}
|
|
364
|
+
let buffer: any[] = []
|
|
365
|
+
const MAX_BUFFER = 1000
|
|
366
|
+
const allowedTraces = new Set<string>()
|
|
367
|
+
const excludedTraces = new Set<string>()
|
|
368
|
+
|
|
369
|
+
function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
|
|
370
|
+
if (filter.status && filter.status !== 'all') {
|
|
371
|
+
if (span.status !== 'unset' && span.status !== filter.status) return true
|
|
372
|
+
}
|
|
373
|
+
if (filter.path) {
|
|
374
|
+
if (!matchGlob(span.name, filter.path)) return true
|
|
375
|
+
}
|
|
376
|
+
if (filter.attributeFilters && filter.attributeFilters.length > 0) {
|
|
377
|
+
const attrs = span.attributes
|
|
378
|
+
for (const af of filter.attributeFilters) {
|
|
379
|
+
const val = attrs[af.key]
|
|
380
|
+
const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase())
|
|
381
|
+
if (af.type === 'include' && !matches) return true
|
|
382
|
+
if (af.type === 'exclude' && matches) return true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return false
|
|
473
386
|
}
|
|
474
|
-
}
|
|
475
387
|
|
|
476
|
-
|
|
477
|
-
|
|
388
|
+
const unsubscribe = store.subscribe((event: any) => {
|
|
389
|
+
const traceId = event.type === 'span.event' ? event.event.traceId : event.span.traceId
|
|
390
|
+
if ((event.type === 'span.start' || event.type === 'span.end') && event.span.parentSpanId === null) {
|
|
391
|
+
if (isRootSpanFiltered(event.span)) {
|
|
392
|
+
excludedTraces.add(traceId)
|
|
393
|
+
allowedTraces.delete(traceId)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
excludedTraces.delete(traceId)
|
|
397
|
+
allowedTraces.add(traceId)
|
|
398
|
+
} else {
|
|
399
|
+
if (excludedTraces.has(traceId)) return
|
|
400
|
+
}
|
|
401
|
+
if (buffer.length < MAX_BUFFER) {
|
|
402
|
+
buffer.push(event)
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const interval = setInterval(() => {
|
|
407
|
+
if (buffer.length > 0) {
|
|
408
|
+
ws.send(JSON.stringify({ type: 'batch', events: buffer }))
|
|
409
|
+
buffer = []
|
|
410
|
+
}
|
|
411
|
+
}, 500)
|
|
412
|
+
|
|
413
|
+
// Parse filter from query params
|
|
414
|
+
try {
|
|
415
|
+
const reqUrl = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`)
|
|
416
|
+
const statusParam = reqUrl.searchParams.get('status')
|
|
417
|
+
const pathParam = reqUrl.searchParams.get('path')
|
|
418
|
+
if (statusParam) filter.status = statusParam
|
|
419
|
+
if (pathParam) filter.path = pathParam
|
|
420
|
+
} catch {}
|
|
421
|
+
|
|
422
|
+
let sinceMs = 15 * 60 * 1000
|
|
423
|
+
const since = Date.now() - sinceMs
|
|
424
|
+
const recent = store.getRecentTraces(since, 200, filter)
|
|
425
|
+
ws.send(JSON.stringify({ type: 'initial', traces: recent }))
|
|
426
|
+
|
|
427
|
+
ws.on('message', (data: any) => {
|
|
428
|
+
try {
|
|
429
|
+
const msg = JSON.parse(typeof data === 'string' ? data : data.toString())
|
|
430
|
+
if (msg.type === 'filter') {
|
|
431
|
+
filter = { path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters }
|
|
432
|
+
if (msg.sinceMs !== undefined) sinceMs = msg.sinceMs
|
|
433
|
+
allowedTraces.clear()
|
|
434
|
+
excludedTraces.clear()
|
|
435
|
+
const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0
|
|
436
|
+
const freshTraces = store.getRecentTraces(freshSince, 200, filter)
|
|
437
|
+
ws.send(JSON.stringify({ type: 'initial', traces: freshTraces }))
|
|
438
|
+
}
|
|
439
|
+
} catch {}
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
ws.on('close', () => {
|
|
443
|
+
unsubscribe()
|
|
444
|
+
clearInterval(interval)
|
|
445
|
+
})
|
|
478
446
|
}
|
|
479
447
|
|
|
480
|
-
|
|
481
|
-
|
|
448
|
+
async function handleWorkerWebSocketUpgrade(wss: any, req: IncomingMessage, socket: any, head: Buffer) {
|
|
449
|
+
try {
|
|
450
|
+
const { CFWebSocket } = await import('../bindings/websocket-pair.ts')
|
|
482
451
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
452
|
+
const activeModule = await ensureWorkerModule()
|
|
453
|
+
const handler = activeModule.default as Record<string, unknown>
|
|
454
|
+
if (!handler || typeof handler.fetch !== 'function') {
|
|
455
|
+
socket.destroy()
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const request = nodeReqToRequest(req)
|
|
460
|
+
const ctx = new ExecutionContext()
|
|
461
|
+
const response = await runWithExecutionContext(ctx, async () => {
|
|
462
|
+
return (handler.fetch as Function).call(handler, request, env, ctx) as Response
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const cfSocket = (response as Response & { webSocket?: InstanceType<typeof CFWebSocket> }).webSocket
|
|
466
|
+
if (response.status !== 101 || !cfSocket || !(cfSocket instanceof CFWebSocket)) {
|
|
467
|
+
socket.destroy()
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Complete the upgrade and bridge
|
|
472
|
+
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
473
|
+
// CF → real WS
|
|
474
|
+
cfSocket.addEventListener('message', (ev: Event) => {
|
|
475
|
+
const msgData = (ev as MessageEvent).data
|
|
476
|
+
try {
|
|
477
|
+
ws.send(msgData)
|
|
478
|
+
} catch {}
|
|
479
|
+
})
|
|
480
|
+
cfSocket.addEventListener('close', (ev: Event) => {
|
|
481
|
+
const ce = ev as CloseEvent
|
|
482
|
+
try {
|
|
483
|
+
ws.close(ce.code, ce.reason)
|
|
484
|
+
} catch {}
|
|
485
|
+
})
|
|
486
|
+
// Accept the client side so events from server.send() are dispatched
|
|
487
|
+
cfSocket.accept()
|
|
488
|
+
|
|
489
|
+
// Real WS → CF
|
|
490
|
+
ws.on('message', (data: Buffer, isBinary: boolean) => {
|
|
491
|
+
const msgData = isBinary
|
|
492
|
+
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer
|
|
493
|
+
: data.toString('utf-8')
|
|
494
|
+
const evt = { type: 'message' as const, data: msgData }
|
|
495
|
+
if (cfSocket._peer?._accepted) {
|
|
496
|
+
cfSocket._peer._dispatchWSEvent(evt)
|
|
497
|
+
} else if (cfSocket._peer) {
|
|
498
|
+
cfSocket._peer._eventQueue.push(evt)
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
ws.on('close', (code: number, reason: Buffer) => {
|
|
503
|
+
if (cfSocket._peer && cfSocket._peer.readyState !== 3) {
|
|
504
|
+
const evt = { type: 'close' as const, code: code ?? 1000, reason: reason?.toString('utf-8') ?? '', wasClean: true }
|
|
505
|
+
if (cfSocket._peer._accepted) {
|
|
506
|
+
cfSocket._peer._dispatchWSEvent(evt)
|
|
507
|
+
} else {
|
|
508
|
+
cfSocket._peer._eventQueue.push(evt)
|
|
509
|
+
}
|
|
510
|
+
cfSocket._peer.readyState = 3
|
|
511
|
+
}
|
|
512
|
+
cfSocket.readyState = 3
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error('[lopata:vite] Worker WebSocket upgrade failed:', err)
|
|
517
|
+
socket.destroy()
|
|
518
|
+
}
|
|
498
519
|
}
|
|
499
520
|
}
|
|
500
521
|
|