@mandujs/core 0.54.9 → 0.54.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 (63) hide show
  1. package/package.json +1 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/context.ts +535 -535
  5. package/src/agent/index.ts +6 -6
  6. package/src/agent/plan.ts +282 -282
  7. package/src/agent/repair.ts +171 -171
  8. package/src/agent/sync.ts +200 -200
  9. package/src/brain/doctor/analyzer.ts +7 -7
  10. package/src/bundler/__tests__/build-runner.ts +81 -62
  11. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  12. package/src/bundler/__tests__/css.test.ts +20 -20
  13. package/src/bundler/analyzer.ts +15 -15
  14. package/src/bundler/build.test.ts +120 -88
  15. package/src/bundler/build.ts +511 -511
  16. package/src/bundler/css.ts +42 -42
  17. package/src/bundler/manifest-schema.ts +21 -21
  18. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  19. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  20. package/src/bundler/types.ts +31 -31
  21. package/src/client/island.ts +79 -79
  22. package/src/config/validate.ts +1 -1
  23. package/src/deploy/inference/context.ts +82 -82
  24. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  25. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  26. package/src/filling/context.ts +17 -17
  27. package/src/generator/generate.ts +30 -30
  28. package/src/generator/index.ts +3 -3
  29. package/src/generator/templates.test.ts +66 -65
  30. package/src/generator/templates.ts +210 -210
  31. package/src/guard/check.ts +9 -9
  32. package/src/guard/config-guard.ts +13 -13
  33. package/src/guard/fs-routes-policy.ts +51 -51
  34. package/src/guard/index.ts +11 -11
  35. package/src/index.ts +3 -3
  36. package/src/kitchen/api/file-api.ts +11 -11
  37. package/src/report/index.ts +1 -1
  38. package/src/resource/__tests__/generator.test.ts +6 -6
  39. package/src/resource/__tests__/schema.test.ts +14 -14
  40. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  41. package/src/resource/ddl/emit.ts +146 -146
  42. package/src/resource/generator-schema.ts +11 -11
  43. package/src/resource/generators/slot.ts +72 -72
  44. package/src/resource/schema.ts +21 -21
  45. package/src/router/client-entry.test.ts +192 -89
  46. package/src/router/client-entry.ts +516 -400
  47. package/src/router/fs-routes.ts +16 -16
  48. package/src/router/fs-scanner.ts +16 -27
  49. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  50. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  51. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  52. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  53. package/src/runtime/devtools-adapter.ts +68 -68
  54. package/src/runtime/escape.ts +34 -34
  55. package/src/runtime/observability-lifecycle.ts +290 -290
  56. package/src/runtime/page-render-response.ts +106 -106
  57. package/src/runtime/request-middleware.ts +31 -31
  58. package/src/runtime/server.ts +243 -243
  59. package/src/runtime/ssr.ts +59 -59
  60. package/src/runtime/static-files.ts +289 -289
  61. package/src/runtime/streaming-ssr.ts +22 -22
  62. package/src/watcher/__tests__/watcher.test.ts +59 -59
  63. package/src/watcher/watcher.ts +61 -61
@@ -1,290 +1,290 @@
1
- import {
2
- eventBus,
3
- type EventType,
4
- type ObservabilityEvent,
5
- type ObservabilitySeverity,
6
- } from "../observability/event-bus";
7
- import {
8
- HEAP_ENDPOINT,
9
- METRICS_ENDPOINT,
10
- buildMetricsResponse,
11
- collectHeapSnapshot,
12
- isObservabilityExposed,
13
- recordHttpRequest,
14
- } from "../observability/metrics";
15
- import {
16
- createTracerFromConfig,
17
- runWithSpan,
18
- setTracer,
19
- type Tracer,
20
- type TracerConfig,
21
- } from "../observability/tracing";
22
- import { collectPerfSnapshot } from "../perf/user-marks";
23
-
24
- export const INTERNAL_EVENTS_ENDPOINT = "/__mandu/events";
25
-
26
- export interface RuntimeRequestRecord {
27
- id: string;
28
- method: string;
29
- path: string;
30
- status: number;
31
- duration: number;
32
- timestamp: number;
33
- cacheStatus?: string;
34
- }
35
-
36
- export interface RuntimeRequestObservation {
37
- req: Request;
38
- path: string;
39
- status: number;
40
- duration: number;
41
- correlationId: string;
42
- cacheStatus?: string;
43
- error?: boolean;
44
- recordRequest?: (entry: RuntimeRequestRecord) => void;
45
- }
46
-
47
- export interface RuntimeObservabilityLifecycle {
48
- readonly tracer: Tracer | undefined;
49
- handleEndpoint(req: Request, pathname: string): Response | null;
50
- runRequest<T extends Response>(
51
- req: Request,
52
- requestStart: number,
53
- correlationId: string,
54
- handler: () => Promise<T>
55
- ): Promise<T>;
56
- recordHttpResponse(req: Request, response: Response | undefined): void;
57
- recordDevRequest(observation: RuntimeRequestObservation): void;
58
- }
59
-
60
- export interface CreateRuntimeObservabilityLifecycleOptions {
61
- isDev: boolean;
62
- heapEndpoint?: boolean;
63
- metricsEndpoint?: boolean;
64
- tracing?: TracerConfig;
65
- }
66
-
67
- export function createRuntimeObservabilityLifecycle(
68
- options: CreateRuntimeObservabilityLifecycleOptions
69
- ): RuntimeObservabilityLifecycle {
70
- const tracerInstance = createTracerFromConfig(options.tracing);
71
- setTracer(tracerInstance);
72
- const tracer = tracerInstance.enabled ? tracerInstance : undefined;
73
-
74
- return {
75
- tracer,
76
- handleEndpoint(req, pathname) {
77
- return handleRuntimeObservabilityEndpoint(req, pathname, options);
78
- },
79
- async runRequest(req, requestStart, correlationId, handler) {
80
- if (!tracer || !tracer.enabled) {
81
- return await handler();
82
- }
83
-
84
- const url = new URL(req.url);
85
- const rootSpan = tracer.startSpanFromRequest("http.request", req, {
86
- kind: "server",
87
- attributes: {
88
- "http.method": req.method,
89
- "http.url": req.url,
90
- "http.target": url.pathname,
91
- "http.scheme": url.protocol.replace(":", ""),
92
- "http.host": url.host,
93
- "mandu.correlation_id": correlationId,
94
- "mandu.request_start_ms": requestStart,
95
- },
96
- });
97
-
98
- try {
99
- const response = await runWithSpan(rootSpan, handler);
100
- rootSpan.setAttribute("http.status_code", response.status);
101
- if (response.status >= 500) {
102
- rootSpan.setStatus("error", `HTTP ${response.status}`);
103
- } else if (rootSpan.status === "unset") {
104
- rootSpan.setStatus("ok");
105
- }
106
- return response;
107
- } catch (err) {
108
- const message = err instanceof Error ? err.message : String(err);
109
- rootSpan.setStatus("error", message);
110
- throw err;
111
- } finally {
112
- rootSpan.end();
113
- }
114
- },
115
- recordHttpResponse(req, response) {
116
- if (!response) return;
117
- try {
118
- recordHttpRequest(req.method, response.status);
119
- } catch {
120
- // Observability must never break the request path.
121
- }
122
- },
123
- recordDevRequest(observation) {
124
- recordRuntimeObservation(observation);
125
- },
126
- };
127
- }
128
-
129
- function handleRuntimeObservabilityEndpoint(
130
- req: Request,
131
- pathname: string,
132
- options: CreateRuntimeObservabilityLifecycleOptions
133
- ): Response | null {
134
- if (pathname === INTERNAL_EVENTS_ENDPOINT) {
135
- return handleEventsStreamRequest(req);
136
- }
137
- if (pathname === `${INTERNAL_EVENTS_ENDPOINT}/recent`) {
138
- return handleEventsRecentRequest(req);
139
- }
140
- if (pathname === HEAP_ENDPOINT) {
141
- if (!isObservabilityExposed(options.isDev, options.heapEndpoint)) return null;
142
- const base = collectHeapSnapshot();
143
- const perf = collectPerfSnapshot();
144
- const body = { ...base, perf };
145
- return new Response(JSON.stringify(body, null, 2), {
146
- status: 200,
147
- headers: {
148
- "Content-Type": "application/json; charset=utf-8",
149
- "Cache-Control": "no-store",
150
- },
151
- });
152
- }
153
- if (pathname === METRICS_ENDPOINT) {
154
- if (!isObservabilityExposed(options.isDev, options.metricsEndpoint)) return null;
155
- return buildMetricsResponse();
156
- }
157
- return null;
158
- }
159
-
160
- function handleEventsStreamRequest(req: Request): Response {
161
- const url = new URL(req.url);
162
- const filterType = url.searchParams.get("type") || undefined;
163
- const filterSeverity = url.searchParams.get("severity") || undefined;
164
- const filterSource = url.searchParams.get("source") || undefined;
165
- const filterTrace = url.searchParams.get("trace") || undefined;
166
-
167
- const matches = (event: ObservabilityEvent): boolean => {
168
- if (filterType && event.type !== filterType) return false;
169
- if (filterSeverity && event.severity !== filterSeverity) return false;
170
- if (filterSource && event.source !== filterSource) return false;
171
- if (filterTrace && event.correlationId !== filterTrace) return false;
172
- return true;
173
- };
174
-
175
- let unsubscribe: (() => void) | null = null;
176
- let heartbeat: ReturnType<typeof setInterval> | null = null;
177
-
178
- const stream = new ReadableStream<Uint8Array>({
179
- start(controller) {
180
- const encoder = new TextEncoder();
181
- const send = (data: string, eventName?: string) => {
182
- try {
183
- const prefix = eventName ? `event: ${eventName}\n` : "";
184
- controller.enqueue(encoder.encode(`${prefix}data: ${data}\n\n`));
185
- } catch {
186
- // Stream closed.
187
- }
188
- };
189
-
190
- for (const event of eventBus.getRecent()) {
191
- if (matches(event)) send(JSON.stringify(event));
192
- }
193
-
194
- unsubscribe = eventBus.on("*", (event) => {
195
- if (matches(event)) send(JSON.stringify(event));
196
- });
197
-
198
- heartbeat = setInterval(() => {
199
- try {
200
- controller.enqueue(encoder.encode(": heartbeat\n\n"));
201
- } catch {
202
- // Ignore closed streams.
203
- }
204
- }, 15000);
205
-
206
- req.signal.addEventListener("abort", () => {
207
- if (unsubscribe) {
208
- unsubscribe();
209
- unsubscribe = null;
210
- }
211
- if (heartbeat) {
212
- clearInterval(heartbeat);
213
- heartbeat = null;
214
- }
215
- try {
216
- controller.close();
217
- } catch {
218
- // No-op when already closed.
219
- }
220
- });
221
- },
222
- cancel() {
223
- if (unsubscribe) {
224
- unsubscribe();
225
- unsubscribe = null;
226
- }
227
- if (heartbeat) {
228
- clearInterval(heartbeat);
229
- heartbeat = null;
230
- }
231
- },
232
- });
233
-
234
- return new Response(stream, {
235
- status: 200,
236
- headers: {
237
- "Content-Type": "text/event-stream",
238
- "Cache-Control": "no-cache, no-store, must-revalidate",
239
- "Connection": "keep-alive",
240
- "X-Accel-Buffering": "no",
241
- },
242
- });
243
- }
244
-
245
- function handleEventsRecentRequest(req: Request): Response {
246
- const url = new URL(req.url);
247
- const count = url.searchParams.get("count");
248
- const type = url.searchParams.get("type") || undefined;
249
- const severity = url.searchParams.get("severity") || undefined;
250
- const windowParam = url.searchParams.get("windowMs");
251
- const windowMs = windowParam ? Number(windowParam) : undefined;
252
-
253
- const events = eventBus.getRecent(count ? Number(count) : undefined, {
254
- type: type as EventType | undefined,
255
- severity: severity as ObservabilitySeverity | undefined,
256
- });
257
- const stats = eventBus.getStats(windowMs);
258
- return Response.json({ events, stats });
259
- }
260
-
261
- function recordRuntimeObservation(observation: RuntimeRequestObservation): void {
262
- const cacheTag = observation.cacheStatus ? ` ${observation.cacheStatus}` : "";
263
- console.log(
264
- `[${new Date().toLocaleTimeString()}] ${observation.req.method} ${observation.path} ${observation.status} ${observation.duration}ms${cacheTag}`
265
- );
266
- observation.recordRequest?.({
267
- id: observation.correlationId,
268
- method: observation.req.method,
269
- path: observation.path,
270
- status: observation.status,
271
- duration: observation.duration,
272
- timestamp: Date.now(),
273
- cacheStatus: observation.cacheStatus,
274
- });
275
- eventBus.emit({
276
- type: "http",
277
- severity: observation.status >= 500 ? "error" : observation.status >= 400 ? "warn" : "info",
278
- source: "server",
279
- correlationId: observation.correlationId,
280
- message: `${observation.req.method} ${observation.path} ${observation.status}${cacheTag}`,
281
- duration: observation.duration,
282
- data: {
283
- method: observation.req.method,
284
- path: observation.path,
285
- status: observation.status,
286
- cache: observation.cacheStatus,
287
- error: observation.error || undefined,
288
- },
289
- });
290
- }
1
+ import {
2
+ eventBus,
3
+ type EventType,
4
+ type ObservabilityEvent,
5
+ type ObservabilitySeverity,
6
+ } from "../observability/event-bus";
7
+ import {
8
+ HEAP_ENDPOINT,
9
+ METRICS_ENDPOINT,
10
+ buildMetricsResponse,
11
+ collectHeapSnapshot,
12
+ isObservabilityExposed,
13
+ recordHttpRequest,
14
+ } from "../observability/metrics";
15
+ import {
16
+ createTracerFromConfig,
17
+ runWithSpan,
18
+ setTracer,
19
+ type Tracer,
20
+ type TracerConfig,
21
+ } from "../observability/tracing";
22
+ import { collectPerfSnapshot } from "../perf/user-marks";
23
+
24
+ export const INTERNAL_EVENTS_ENDPOINT = "/__mandu/events";
25
+
26
+ export interface RuntimeRequestRecord {
27
+ id: string;
28
+ method: string;
29
+ path: string;
30
+ status: number;
31
+ duration: number;
32
+ timestamp: number;
33
+ cacheStatus?: string;
34
+ }
35
+
36
+ export interface RuntimeRequestObservation {
37
+ req: Request;
38
+ path: string;
39
+ status: number;
40
+ duration: number;
41
+ correlationId: string;
42
+ cacheStatus?: string;
43
+ error?: boolean;
44
+ recordRequest?: (entry: RuntimeRequestRecord) => void;
45
+ }
46
+
47
+ export interface RuntimeObservabilityLifecycle {
48
+ readonly tracer: Tracer | undefined;
49
+ handleEndpoint(req: Request, pathname: string): Response | null;
50
+ runRequest<T extends Response>(
51
+ req: Request,
52
+ requestStart: number,
53
+ correlationId: string,
54
+ handler: () => Promise<T>
55
+ ): Promise<T>;
56
+ recordHttpResponse(req: Request, response: Response | undefined): void;
57
+ recordDevRequest(observation: RuntimeRequestObservation): void;
58
+ }
59
+
60
+ export interface CreateRuntimeObservabilityLifecycleOptions {
61
+ isDev: boolean;
62
+ heapEndpoint?: boolean;
63
+ metricsEndpoint?: boolean;
64
+ tracing?: TracerConfig;
65
+ }
66
+
67
+ export function createRuntimeObservabilityLifecycle(
68
+ options: CreateRuntimeObservabilityLifecycleOptions
69
+ ): RuntimeObservabilityLifecycle {
70
+ const tracerInstance = createTracerFromConfig(options.tracing);
71
+ setTracer(tracerInstance);
72
+ const tracer = tracerInstance.enabled ? tracerInstance : undefined;
73
+
74
+ return {
75
+ tracer,
76
+ handleEndpoint(req, pathname) {
77
+ return handleRuntimeObservabilityEndpoint(req, pathname, options);
78
+ },
79
+ async runRequest(req, requestStart, correlationId, handler) {
80
+ if (!tracer || !tracer.enabled) {
81
+ return await handler();
82
+ }
83
+
84
+ const url = new URL(req.url);
85
+ const rootSpan = tracer.startSpanFromRequest("http.request", req, {
86
+ kind: "server",
87
+ attributes: {
88
+ "http.method": req.method,
89
+ "http.url": req.url,
90
+ "http.target": url.pathname,
91
+ "http.scheme": url.protocol.replace(":", ""),
92
+ "http.host": url.host,
93
+ "mandu.correlation_id": correlationId,
94
+ "mandu.request_start_ms": requestStart,
95
+ },
96
+ });
97
+
98
+ try {
99
+ const response = await runWithSpan(rootSpan, handler);
100
+ rootSpan.setAttribute("http.status_code", response.status);
101
+ if (response.status >= 500) {
102
+ rootSpan.setStatus("error", `HTTP ${response.status}`);
103
+ } else if (rootSpan.status === "unset") {
104
+ rootSpan.setStatus("ok");
105
+ }
106
+ return response;
107
+ } catch (err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ rootSpan.setStatus("error", message);
110
+ throw err;
111
+ } finally {
112
+ rootSpan.end();
113
+ }
114
+ },
115
+ recordHttpResponse(req, response) {
116
+ if (!response) return;
117
+ try {
118
+ recordHttpRequest(req.method, response.status);
119
+ } catch {
120
+ // Observability must never break the request path.
121
+ }
122
+ },
123
+ recordDevRequest(observation) {
124
+ recordRuntimeObservation(observation);
125
+ },
126
+ };
127
+ }
128
+
129
+ function handleRuntimeObservabilityEndpoint(
130
+ req: Request,
131
+ pathname: string,
132
+ options: CreateRuntimeObservabilityLifecycleOptions
133
+ ): Response | null {
134
+ if (pathname === INTERNAL_EVENTS_ENDPOINT) {
135
+ return handleEventsStreamRequest(req);
136
+ }
137
+ if (pathname === `${INTERNAL_EVENTS_ENDPOINT}/recent`) {
138
+ return handleEventsRecentRequest(req);
139
+ }
140
+ if (pathname === HEAP_ENDPOINT) {
141
+ if (!isObservabilityExposed(options.isDev, options.heapEndpoint)) return null;
142
+ const base = collectHeapSnapshot();
143
+ const perf = collectPerfSnapshot();
144
+ const body = { ...base, perf };
145
+ return new Response(JSON.stringify(body, null, 2), {
146
+ status: 200,
147
+ headers: {
148
+ "Content-Type": "application/json; charset=utf-8",
149
+ "Cache-Control": "no-store",
150
+ },
151
+ });
152
+ }
153
+ if (pathname === METRICS_ENDPOINT) {
154
+ if (!isObservabilityExposed(options.isDev, options.metricsEndpoint)) return null;
155
+ return buildMetricsResponse();
156
+ }
157
+ return null;
158
+ }
159
+
160
+ function handleEventsStreamRequest(req: Request): Response {
161
+ const url = new URL(req.url);
162
+ const filterType = url.searchParams.get("type") || undefined;
163
+ const filterSeverity = url.searchParams.get("severity") || undefined;
164
+ const filterSource = url.searchParams.get("source") || undefined;
165
+ const filterTrace = url.searchParams.get("trace") || undefined;
166
+
167
+ const matches = (event: ObservabilityEvent): boolean => {
168
+ if (filterType && event.type !== filterType) return false;
169
+ if (filterSeverity && event.severity !== filterSeverity) return false;
170
+ if (filterSource && event.source !== filterSource) return false;
171
+ if (filterTrace && event.correlationId !== filterTrace) return false;
172
+ return true;
173
+ };
174
+
175
+ let unsubscribe: (() => void) | null = null;
176
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
177
+
178
+ const stream = new ReadableStream<Uint8Array>({
179
+ start(controller) {
180
+ const encoder = new TextEncoder();
181
+ const send = (data: string, eventName?: string) => {
182
+ try {
183
+ const prefix = eventName ? `event: ${eventName}\n` : "";
184
+ controller.enqueue(encoder.encode(`${prefix}data: ${data}\n\n`));
185
+ } catch {
186
+ // Stream closed.
187
+ }
188
+ };
189
+
190
+ for (const event of eventBus.getRecent()) {
191
+ if (matches(event)) send(JSON.stringify(event));
192
+ }
193
+
194
+ unsubscribe = eventBus.on("*", (event) => {
195
+ if (matches(event)) send(JSON.stringify(event));
196
+ });
197
+
198
+ heartbeat = setInterval(() => {
199
+ try {
200
+ controller.enqueue(encoder.encode(": heartbeat\n\n"));
201
+ } catch {
202
+ // Ignore closed streams.
203
+ }
204
+ }, 15000);
205
+
206
+ req.signal.addEventListener("abort", () => {
207
+ if (unsubscribe) {
208
+ unsubscribe();
209
+ unsubscribe = null;
210
+ }
211
+ if (heartbeat) {
212
+ clearInterval(heartbeat);
213
+ heartbeat = null;
214
+ }
215
+ try {
216
+ controller.close();
217
+ } catch {
218
+ // No-op when already closed.
219
+ }
220
+ });
221
+ },
222
+ cancel() {
223
+ if (unsubscribe) {
224
+ unsubscribe();
225
+ unsubscribe = null;
226
+ }
227
+ if (heartbeat) {
228
+ clearInterval(heartbeat);
229
+ heartbeat = null;
230
+ }
231
+ },
232
+ });
233
+
234
+ return new Response(stream, {
235
+ status: 200,
236
+ headers: {
237
+ "Content-Type": "text/event-stream",
238
+ "Cache-Control": "no-cache, no-store, must-revalidate",
239
+ "Connection": "keep-alive",
240
+ "X-Accel-Buffering": "no",
241
+ },
242
+ });
243
+ }
244
+
245
+ function handleEventsRecentRequest(req: Request): Response {
246
+ const url = new URL(req.url);
247
+ const count = url.searchParams.get("count");
248
+ const type = url.searchParams.get("type") || undefined;
249
+ const severity = url.searchParams.get("severity") || undefined;
250
+ const windowParam = url.searchParams.get("windowMs");
251
+ const windowMs = windowParam ? Number(windowParam) : undefined;
252
+
253
+ const events = eventBus.getRecent(count ? Number(count) : undefined, {
254
+ type: type as EventType | undefined,
255
+ severity: severity as ObservabilitySeverity | undefined,
256
+ });
257
+ const stats = eventBus.getStats(windowMs);
258
+ return Response.json({ events, stats });
259
+ }
260
+
261
+ function recordRuntimeObservation(observation: RuntimeRequestObservation): void {
262
+ const cacheTag = observation.cacheStatus ? ` ${observation.cacheStatus}` : "";
263
+ console.log(
264
+ `[${new Date().toLocaleTimeString()}] ${observation.req.method} ${observation.path} ${observation.status} ${observation.duration}ms${cacheTag}`
265
+ );
266
+ observation.recordRequest?.({
267
+ id: observation.correlationId,
268
+ method: observation.req.method,
269
+ path: observation.path,
270
+ status: observation.status,
271
+ duration: observation.duration,
272
+ timestamp: Date.now(),
273
+ cacheStatus: observation.cacheStatus,
274
+ });
275
+ eventBus.emit({
276
+ type: "http",
277
+ severity: observation.status >= 500 ? "error" : observation.status >= 400 ? "warn" : "info",
278
+ source: "server",
279
+ correlationId: observation.correlationId,
280
+ message: `${observation.req.method} ${observation.path} ${observation.status}${cacheTag}`,
281
+ duration: observation.duration,
282
+ data: {
283
+ method: observation.req.method,
284
+ path: observation.path,
285
+ status: observation.status,
286
+ cache: observation.cacheStatus,
287
+ error: observation.error || undefined,
288
+ },
289
+ });
290
+ }