@hakam-aldeen-kh/blix 0.4.2 → 0.6.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.
@@ -1,5 +1,3 @@
1
- import { AxiosInstance } from 'axios';
2
-
3
1
  /**
4
2
  * Blix — HTTP capture entry point.
5
3
  *
@@ -8,19 +6,122 @@ import { AxiosInstance } from 'axios';
8
6
  * encryption or auth interceptors, so this interceptor runs last in axios's
9
7
  * LIFO request stack and therefore sees the plaintext request body.
10
8
  *
9
+ * Typed structurally rather than against axios: axios is an optional peer, and
10
+ * an app that only uses `fetch` must be able to type-check Blix without it.
11
+ *
11
12
  * No-op when `process.env.NODE_ENV !== "development"`.
12
13
  */
13
-
14
+ /**
15
+ * The part of an axios interceptor manager Blix uses.
16
+ *
17
+ * The callbacks are typed `any`, and have to be: axios types them against its
18
+ * own config and response types, and a callback's parameter is checked
19
+ * contravariantly — `unknown` there would reject a real axios instance. `use`
20
+ * and `eject` themselves use method syntax, whose parameters are checked
21
+ * bivariantly, which is what lets `AxiosInterceptorManager<V>` satisfy this
22
+ * for any `V`.
23
+ */
24
+ interface InterceptorManagerLike {
25
+ use(onFulfilled?: ((value: any) => any) | null, onRejected?: ((error: any) => any) | null): number;
26
+ eject(id: number): void;
27
+ }
28
+ /**
29
+ * What `attachHttpMonitor` needs from an axios instance: `interceptors`, and
30
+ * nothing else. Structural, like `HttpClientLike` — Blix does not import
31
+ * axios's types, so a fetch-only app never needs axios installed to type-check
32
+ * against Blix, and a real `AxiosInstance` still passes with no cast.
33
+ */
34
+ interface HttpMonitorTarget {
35
+ interceptors: {
36
+ request: InterceptorManagerLike;
37
+ response: InterceptorManagerLike;
38
+ };
39
+ }
14
40
  /**
15
41
  * Attaches HTTP-capture interceptors to an axios instance.
16
42
  *
17
43
  * Call this at module init time (e.g. at the bottom of your `axios.ts`),
18
44
  * after registering any encryption interceptors so that the monitor sees the
19
45
  * plaintext body first.
46
+ *
47
+ * Idempotent per instance, and returns a disposer that ejects both
48
+ * interceptors. A second install used to register a second pair: every request
49
+ * then started two entries, both response interceptors settled the second, and
50
+ * the first stayed pending for the rest of the session.
20
51
  */
21
- declare function attachHttpMonitor(instance: AxiosInstance, options?: {
52
+ declare function attachHttpMonitor(instance: HttpMonitorTarget, options?: {
22
53
  dbName?: string;
23
- }): void;
54
+ }): () => void;
55
+
56
+ /**
57
+ * Blix — HTTP capture for native `fetch`.
58
+ *
59
+ * `attachHttpMonitor` only sees requests made through an axios instance, so an
60
+ * app that calls `fetch` directly — the default in a Next.js App Router project
61
+ * — got an empty Network tab and a reasonable conclusion that Blix was broken.
62
+ * This wraps `globalThis.fetch` and feeds the same store with the same
63
+ * `MonitorEntry` shape, marked `client: "fetch"`.
64
+ *
65
+ * ## The rules this file is built around
66
+ *
67
+ * - **The caller's bodies are never consumed.** `Request` and `Response` bodies
68
+ * are one-shot streams. Everything Blix reads it reads from a `clone()`, taken
69
+ * before the body can be touched: the request's before `fetch` is called, the
70
+ * response's before the caller is handed it.
71
+ * - **Nothing is buffered without bound.** Bodies are read through a byte
72
+ * counter on every read and abandoned past `maxBodyBytes`, whatever
73
+ * `Content-Length` claimed. That header is the *encoded* length; the clone
74
+ * yields decoded bytes, so a small gzip can inflate far past it.
75
+ * - **Nothing the caller passed is mutated, and no error is swallowed or
76
+ * replaced.** A failure anywhere in capture degrades to "not captured", never
77
+ * to a broken request.
78
+ * - **Browser only.** Next.js patches `fetch` on the server for its data cache;
79
+ * Blix never touches that one.
80
+ *
81
+ * `captureEncrypted` does not apply here. It correlates by an object the host
82
+ * holds inside its own interceptor, and `fetch` has no interceptor stage.
83
+ */
84
+ /**
85
+ * A rule that keeps a request out of the log. A string matches as a substring,
86
+ * and a regular expression is tested, against the URL's path and query
87
+ * (`/api/items?page=2`); a function receives the resolved `URL`.
88
+ */
89
+ type FetchIgnoreRule = string | RegExp | ((url: URL) => boolean);
90
+ interface FetchMonitorOptions {
91
+ /** IndexedDB database name — see `attachHttpMonitor`. */
92
+ dbName?: string;
93
+ /**
94
+ * Largest body Blix will read, request or response, in bytes. Default
95
+ * 5 MB. A larger body is still logged — status, headers, timing — with the
96
+ * body marked as not captured, and why. `0` captures no bodies at all.
97
+ */
98
+ maxBodyBytes?: number;
99
+ /**
100
+ * Requests to leave out of the log entirely. An array **replaces** the
101
+ * defaults; a function receives the defaults and returns the list to use,
102
+ * which is how to extend them. `[]` captures everything.
103
+ *
104
+ * The defaults drop framework dev traffic that would otherwise bury the app's
105
+ * own requests: Next.js assets, HMR and data requests (`/_next/`), the
106
+ * Next.js dev overlay (`/__nextjs`), App Router navigation and prefetch
107
+ * payloads (`?_rsc=`), and webpack HMR (`.hot-update.`, `/__webpack_hmr`).
108
+ */
109
+ ignore?: readonly FetchIgnoreRule[] | ((defaults: readonly FetchIgnoreRule[]) => readonly FetchIgnoreRule[]);
110
+ }
111
+ /**
112
+ * Wraps `globalThis.fetch` so every request the page makes is captured, and
113
+ * returns a disposer that unwraps it.
114
+ *
115
+ * Call once, as early as possible — before the first `fetch` you want to see.
116
+ * A no-op in production, on the server, and when Blix's wrapper is already
117
+ * installed (the returned disposer is then a no-op too).
118
+ *
119
+ * The disposer restores the exact original `fetch`, but only while Blix's
120
+ * wrapper is still what `globalThis.fetch` points at. If something else has
121
+ * patched over it since, restoring would silently remove that patch as well,
122
+ * so it is left alone and a development warning explains why.
123
+ */
124
+ declare function attachFetchMonitor(options?: FetchMonitorOptions): () => void;
24
125
 
25
126
  /**
26
127
  * Blix — encrypted-payload capture (opt-in).
@@ -304,7 +405,7 @@ interface ReduxActionMeta {
304
405
  }
305
406
  /** Query/mutation only: the cache row's current lifecycle state. Deliberately
306
407
  * not folded into `MonitorState` — `MonitorState` drives the shared state
307
- * filter and waterfall colour, and pending/success/error covers this fine;
408
+ * filter and row colour, and pending/success/error covers this fine;
308
409
  * the richer status (fresh/stale/fetching/removed) lives here instead. */
309
410
  interface QueryMeta {
310
411
  sub: "query" | "mutation";
@@ -353,14 +454,13 @@ interface MonitorEntry {
353
454
  /**
354
455
  * Start/end as absolute epoch ms (`timeOrigin + startTime`).
355
456
  *
356
- * The waterfall must use these, never `startTime`/`endTime`: those are
357
- * relative to a per-page-load origin, so a persisted entry's `startTime` is
358
- * meaningless against the current load's.
457
+ * Anything comparing two entries in time must use these, never
458
+ * `startTime`/`endTime`: those are relative to a per-page-load origin, so a
459
+ * persisted entry's `startTime` is meaningless against the current load's.
359
460
  */
360
461
  startAbs: number;
361
462
  endAbs?: number;
362
- /** Which page load captured this. Drives the divider rows and the per-load
363
- * waterfall timeline. */
463
+ /** Which page load captured this. Drives the divider rows in the list. */
364
464
  loadId: string;
365
465
  status?: number;
366
466
  state: MonitorState;
@@ -409,6 +509,15 @@ interface MonitorEntry {
409
509
  redux?: ReduxActionMeta;
410
510
  /** Query only: the cache row's key, status and observer count. */
411
511
  query?: QueryMeta;
512
+ /**
513
+ * HTTP only: which capture path recorded this request — `attachHttpMonitor`
514
+ * (`"axios"`) or `attachFetchMonitor` (`"fetch"`). Deliberately not
515
+ * `transport`, which is realtime-only and is rendered in place of the method.
516
+ *
517
+ * Absent on entries captured before this field existed. Absent means
518
+ * unknown — never read it as "axios".
519
+ */
520
+ client?: "axios" | "fetch";
412
521
  /** HTTP only: id of the query/mutation entry that caused this request,
413
522
  * when it could be determined — see `monitorContext.ts`. Absence means
414
523
  * "unknown", never "not caused by a query". */
@@ -437,10 +546,15 @@ interface Pos {
437
546
  * Everything the panel remembers between sessions.
438
547
  *
439
548
  * Stored in IndexedDB (`meta.prefs`) as the source of truth, mirrored into
440
- * `localStorage["nm:prefs"]` purely as a synchronous read cache so the panel
441
- * can restore its geometry without a frame of flash. The mirror is expendable —
549
+ * `localStorage["<dbName>:prefs"]` — `blix:checkout:prefs` for
550
+ * `dbName="checkout"` — purely as a synchronous read cache so the panel can
551
+ * restore its geometry without a frame of flash. The mirror is expendable —
442
552
  * any host that clears web storage wipes it, and the IndexedDB copy silently
443
553
  * restores it on next boot.
554
+ *
555
+ * Both halves are scoped to the database name: web storage, like IndexedDB, is
556
+ * origin-scoped, so an unscoped key meant every app on one origin shared one
557
+ * set of preferences.
444
558
  */
445
559
  interface MonitorPrefs {
446
560
  /** Bumped on every write; used to reconcile the two copies. */
@@ -481,10 +595,12 @@ interface MonitorPrefs {
481
595
  dataFormat: string;
482
596
  /** Active top-level section — `"network"` (HTTP) or `"realtime"` (sockets). */
483
597
  section: string;
598
+ /** Sources rail shown as icons only. A deliberate choice, kept separate from
599
+ * the automatic collapse a narrow panel applies — re-widening the panel
600
+ * restores the rail unless the developer collapsed it themselves. */
601
+ railCollapsed: boolean;
484
602
  /** Row height preset: compact | normal | comfy. */
485
603
  density: string;
486
- /** User-resized column widths, in px, keyed by column id. */
487
- columnWidths: Record<string, number>;
488
604
  }
489
605
 
490
606
  /**
@@ -542,12 +658,42 @@ declare function tapRealtimeAdapter<T extends RealtimeAdapterLike>(adapter: T, t
542
658
  *
543
659
  * So `withInitiatorCapture` wraps the axios instance, takes the stack
544
660
  * synchronously in the caller's own frame, and stashes it on the config for
545
- * `beginMonitor` to read.
661
+ * `beginMonitor` to read. The fetch wrapper needs no such proxy: it *is* the
662
+ * call site, so it calls `captureFrames` directly.
663
+ *
664
+ * ## Why Blix's own frames are dropped by depth
665
+ *
666
+ * Both callers know exactly how many of their own frames sit above the caller's
667
+ * code, and pass that as `skip`. The alternative — recognizing Blix's frames by
668
+ * module name — does not survive bundling: the published package is a handful
669
+ * of hashed `dist/chunk-*.js` files, none of them named after the source
670
+ * module, so in a real install every Blix frame slipped through and the
671
+ * Initiator column pointed at Blix itself. Do not add module-name rules for
672
+ * Blix's own code to `NOISE`.
546
673
  *
547
674
  * The whole module is behind a statically-foldable `NODE_ENV` check at its call
548
675
  * site, so it is dead-code-eliminated from production builds.
549
676
  */
550
677
 
678
+ /**
679
+ * What `withInitiatorCapture` needs from an HTTP client — declared
680
+ * structurally, so axios's types are never imported and an app that only uses
681
+ * `fetch` never needs axios installed to type-check against Blix.
682
+ *
683
+ * `request` is the one entry point every axios-style client has; the verb
684
+ * helpers are wrapped when present. Method syntax on purpose: method parameters
685
+ * are compared bivariantly, which is what lets axios's generic, config-typed
686
+ * signatures satisfy these `unknown`-typed ones without a cast.
687
+ */
688
+ interface InitiatorCaptureTarget {
689
+ request(config: unknown): unknown;
690
+ get?(url: string, config?: unknown): unknown;
691
+ delete?(url: string, config?: unknown): unknown;
692
+ head?(url: string, config?: unknown): unknown;
693
+ post?(url: string, data?: unknown, config?: unknown): unknown;
694
+ put?(url: string, data?: unknown, config?: unknown): unknown;
695
+ patch?(url: string, data?: unknown, config?: unknown): unknown;
696
+ }
551
697
  /**
552
698
  * Wraps the axios instance so every call site records its own stack.
553
699
  *
@@ -555,7 +701,11 @@ declare function tapRealtimeAdapter<T extends RealtimeAdapterLike>(adapter: T, t
555
701
  * `put`, `patch`, `delete` and `head`. Other entry points (`options`, the
556
702
  * `*Form` helpers) pass through unwrapped — requests made through them are
557
703
  * still captured, they just carry no initiator stack.
704
+ *
705
+ * Generic over the instance, so what the caller gets back is its own type —
706
+ * an `AxiosInstance` stays an `AxiosInstance`, with `get<T>`, `defaults` and
707
+ * `interceptors` intact — rather than the minimal shape it was checked against.
558
708
  */
559
- declare function withInitiatorCapture(instance: AxiosInstance): AxiosInstance;
709
+ declare function withInitiatorCapture<T extends InitiatorCaptureTarget>(instance: T): T;
560
710
 
561
- export { type CacheLike, type Corner, type DiffOp, type DockMode, type EncryptedPayload, type FrameDirection, type InitiatorFrame, type MiddlewareApiLike, type MiddlewareLike, type MonitorEntry, type MonitorKind, type MonitorPrefs, type MonitorState, type PersistedEntry, type Pos, type QueryClientLike, type QueryMeta, type RealtimeAdapterLike, type ReduxActionMeta, type ReduxCaptureOptions, type Size, type StateDiff, type StateDiffEntry, type TimingMarks, type WsFrame, attachHttpMonitor, captureEncrypted, createReduxMonitorMiddleware, tapQueryClient, tapRealtimeAdapter, withInitiatorCapture };
711
+ export { type CacheLike, type Corner, type DiffOp, type DockMode, type EncryptedPayload, type FetchIgnoreRule, type FetchMonitorOptions, type FrameDirection, type InitiatorFrame, type MiddlewareApiLike, type MiddlewareLike, type MonitorEntry, type MonitorKind, type MonitorPrefs, type MonitorState, type PersistedEntry, type Pos, type QueryClientLike, type QueryMeta, type RealtimeAdapterLike, type ReduxActionMeta, type ReduxCaptureOptions, type Size, type StateDiff, type StateDiffEntry, type TimingMarks, type WsFrame, attachFetchMonitor, attachHttpMonitor, captureEncrypted, createReduxMonitorMiddleware, tapQueryClient, tapRealtimeAdapter, withInitiatorCapture };
@@ -1,14 +1,16 @@
1
1
  import {
2
+ attachFetchMonitor,
2
3
  attachHttpMonitor,
3
4
  captureEncrypted,
4
5
  tapRealtimeAdapter
5
- } from "../chunk-OQHKPRNI.js";
6
+ } from "../chunk-4JZEVUOR.js";
6
7
  import {
7
8
  createReduxMonitorMiddleware,
8
9
  tapQueryClient,
9
10
  withInitiatorCapture
10
- } from "../chunk-MRTSIXR7.js";
11
+ } from "../chunk-FIYP47QZ.js";
11
12
  export {
13
+ attachFetchMonitor,
12
14
  attachHttpMonitor,
13
15
  captureEncrypted,
14
16
  createReduxMonitorMiddleware,