browserscale-ts 1.4.0 → 1.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.
@@ -0,0 +1,271 @@
1
+ import type { DomEvent } from "./gen/wrc_pb.ts";
2
+ /**
3
+ * A node in the mirrored page, in CDP's `DOM.Node` shape — the same shape
4
+ * {@link CloudBrowser.getDOM} returns, with two additions that the mirror
5
+ * needs and a caller usually wants anyway: {@link DomNode.frameId} and
6
+ * {@link DomNode.contentFrameId}.
7
+ *
8
+ * An `<iframe>` is an ordinary element here. The document it hosts is its one
9
+ * entry in {@link DomNode.children}, present once the element has been
10
+ * expanded, and nothing about walking the tree has to know a process boundary
11
+ * runs through it.
12
+ */
13
+ export interface DomNode {
14
+ nodeId: number;
15
+ backendNodeId: number;
16
+ nodeType: number;
17
+ nodeName: string;
18
+ localName?: string;
19
+ nodeValue?: string;
20
+ /** Flat `[name, value, name, value, ...]`, as CDP sends it. */
21
+ attributes?: string[];
22
+ /**
23
+ * Total children in the page, whether or not they are in `children`. An
24
+ * `<iframe>` reports 1: the document it hosts.
25
+ */
26
+ childNodeCount?: number;
27
+ /** Present once the node has been expanded. */
28
+ children?: DomNode[];
29
+ /** Author shadow roots, when the mirror was started with `pierce`. */
30
+ shadowRoots?: DomNode[];
31
+ /**
32
+ * The frame this node lives in. Always set.
33
+ *
34
+ * Together with `backendNodeId` this is the node's address: node ids are
35
+ * handed out per renderer and restart per frame, so two frames can and do
36
+ * use the same one, and the id on its own is ambiguous across a page.
37
+ */
38
+ frameId: string;
39
+ /**
40
+ * For an element that hosts a frame (`<iframe>`, `<frame>`, `<object>`):
41
+ * the frame it hosts, which is a different frame from `frameId` and is the
42
+ * one its child document's ids belong to.
43
+ */
44
+ contentFrameId?: string;
45
+ }
46
+ /**
47
+ * The opening snapshot: the main frame's document. Child frames are not in it
48
+ * — their documents are fetched by expanding the `<iframe>` elements that host
49
+ * them, which is also what starts mirroring them.
50
+ */
51
+ export interface DomSnapshot {
52
+ root: string;
53
+ frameId: string;
54
+ /** The page sequence this snapshot is the baseline for. */
55
+ seq: number;
56
+ }
57
+ /** Why a mirror had to be rebuilt. */
58
+ export type DomResyncReason = "documentReplaced" | "overflow" | "rendererGone" | "slowReader" | string;
59
+ export interface DomMirrorOptions {
60
+ /**
61
+ * Levels to fetch up front. Default 2, which is `#document` → `<html>` →
62
+ * `<head>`/`<body>` — enough to draw a collapsed tree. Fetching everything
63
+ * (-1) works but gives up what the mirror is for.
64
+ */
65
+ depth?: number;
66
+ /** Descend into author shadow roots. Fixed for the life of the mirror. */
67
+ pierce?: boolean;
68
+ }
69
+ /**
70
+ * Called after the tree changed. `root` is a fresh object whenever anything
71
+ * below it changed, so it can be compared by identity and rendered with
72
+ * memoized components.
73
+ */
74
+ export type DomChangeHandler = (mirror: DomMirror) => void;
75
+ /** Called when the mirror had to be rebuilt, after the new tree is in place. */
76
+ export type DomResyncHandler = (reason: DomResyncReason) => void;
77
+ /** @internal The transport calls the mirror needs. Supplied by CloudBrowser. */
78
+ export interface DomMirrorTransport {
79
+ start(opts: DomMirrorOptions): Promise<DomSnapshot>;
80
+ stop(): Promise<void>;
81
+ children(backendNodeId: number, frameId: string, depth?: number): Promise<{
82
+ children: string;
83
+ seq: number;
84
+ }>;
85
+ release(backendNodeId: number, frameId: string): Promise<void>;
86
+ reveal(backendNodeId: number, frameId: string): Promise<{
87
+ path: string;
88
+ seq: number;
89
+ }>;
90
+ }
91
+ /**
92
+ * A live copy of a page's DOM, across every frame in it.
93
+ *
94
+ * Returned by {@link CloudBrowser.mirrorDom}. The browser sends the top of the
95
+ * tree once and from then on only what changed in the part you expanded, so a
96
+ * page that churns inside a collapsed subtree costs one number per batch
97
+ * instead of a re-serialized document.
98
+ *
99
+ * It is one tree. An `<iframe>` is an element whose one child is the document
100
+ * it hosts; expanding it fetches that document and starts mirroring the frame,
101
+ * collapsing it stops again, and a frame navigating arrives as its owner's
102
+ * child being replaced. Underneath there is still one mirror per document,
103
+ * because a mutation observer is bound to a single Document and an
104
+ * out-of-process iframe is a different Document in a different process — but
105
+ * that is engine bookkeeping, not something a caller models.
106
+ *
107
+ * Node ids restart per frame, so a node's address is the pair
108
+ * ({@link DomNode.frameId}, `backendNodeId`) and never the id alone.
109
+ *
110
+ * The tree is treated as immutable: applying a change replaces the nodes from
111
+ * the root down to the one that moved and leaves every other object identical.
112
+ * A UI can therefore re-render from `root` and let `React.memo` (or any
113
+ * identity check) skip the parts that did not move.
114
+ *
115
+ * ```ts
116
+ * const mirror = await browser.mirrorDom({ pierce: true }, () => render(mirror.root));
117
+ * await mirror.expand(bodyNode); // start reporting changes inside <body>
118
+ * await mirror.collapse(bodyNode); // stop again
119
+ * await mirror.stop();
120
+ * ```
121
+ */
122
+ export declare class DomMirror {
123
+ private readonly transport;
124
+ private readonly options;
125
+ private readonly onChange;
126
+ private readonly onResync?;
127
+ private readonly abort;
128
+ private readonly finished;
129
+ private nodes;
130
+ private slots;
131
+ private expandedKeys;
132
+ /**
133
+ * Per node: the page sequence its current state was defined at, by a read
134
+ * payload or by an edit.
135
+ *
136
+ * Reads and events reach a client over two different channels — a unary call
137
+ * and a stream — so an event can turn up that the read reply already folded
138
+ * in. Applying it twice would duplicate an insertion, which is the one entry
139
+ * type that is not idempotent. Comparing against the node's own watermark
140
+ * rather than a single page-wide one keeps that from silently discarding a
141
+ * change to an unrelated part of the tree.
142
+ */
143
+ private asOf;
144
+ private seqValue;
145
+ private rootNode;
146
+ private mainFrame;
147
+ /**
148
+ * Events are held until a snapshot exists to apply them to, and dropped if
149
+ * they predate it. Without this a batch that lands between subscribing and
150
+ * the snapshot arriving would either be applied to nothing or applied twice.
151
+ */
152
+ private ready;
153
+ private pendingEvents;
154
+ private stopped;
155
+ private failure;
156
+ /** @internal Constructed by CloudBrowser; not part of the public API. */
157
+ constructor(init: {
158
+ stream: AsyncIterable<DomEvent>;
159
+ transport: DomMirrorTransport;
160
+ options: DomMirrorOptions;
161
+ onChange: DomChangeHandler;
162
+ onResync?: DomResyncHandler;
163
+ abort: AbortController;
164
+ });
165
+ /** The main frame's document, or null before the first snapshot arrived. */
166
+ get root(): DomNode | null;
167
+ /** The page's main frame. */
168
+ get mainFrameId(): string;
169
+ /**
170
+ * Every frame with a document in the tree, main frame first. A frame whose
171
+ * `<iframe>` has not been expanded is not mirrored and not listed.
172
+ */
173
+ get frameIds(): string[];
174
+ /**
175
+ * The page sequence of the last change applied. One clock for the whole
176
+ * page: a change in an out-of-process iframe and one in the main document
177
+ * are ordered against each other.
178
+ */
179
+ get seq(): number;
180
+ /** Looks up a node by its address. */
181
+ getNode(frameId: string, backendNodeId: number): DomNode | undefined;
182
+ /**
183
+ * Whether this node's children are known. Changes inside a node that is not
184
+ * expanded arrive only as an updated `childNodeCount`.
185
+ */
186
+ isExpanded(node: DomNode): boolean;
187
+ /** Why the mirror ended: null while running and after a clean stop. */
188
+ get error(): Error | null;
189
+ /**
190
+ * Fetches a node's children and starts reporting changes inside them. This
191
+ * is what a tree view calls when the user opens a node.
192
+ *
193
+ * On an `<iframe>` the one child is the document it hosts, and this call is
194
+ * what starts mirroring that frame. Nothing about the result says a process
195
+ * boundary was crossed; it is a child list like any other.
196
+ *
197
+ * @param depth levels below the node, default 1
198
+ */
199
+ expand(node: DomNode, depth?: number): Promise<void>;
200
+ /**
201
+ * Stops reporting changes inside a node, called when the user closes it. The
202
+ * node itself stays in the tree and keeps reporting its child count. A child
203
+ * frame below it stops being mirrored too.
204
+ *
205
+ * Skipping this is not an error, it is a slow leak: the browser's revealed
206
+ * set only grows, and eventually it is no longer filtering anything.
207
+ */
208
+ collapse(node: DomNode): Promise<void>;
209
+ /**
210
+ * Brings a node into the tree together with its ancestors and their
211
+ * siblings, and starts reporting changes along that path.
212
+ *
213
+ * Use it to focus a node you do not hold — an `inspectAtPosition` hit, say.
214
+ * You cannot walk up to it yourself: it is not in your tree, so there is
215
+ * nothing to walk from.
216
+ *
217
+ * The node may be in a frame nobody opened, and that works: the chain comes
218
+ * back crossing the frame boundaries it has to, and those frames start being
219
+ * mirrored, exactly as if you had expanded your way there by hand.
220
+ *
221
+ * @returns the ancestor chain, the main document first, or an empty array if
222
+ * the node is not on the page
223
+ */
224
+ reveal(backendNodeId: number, frameId?: string): Promise<DomNode[]>;
225
+ /**
226
+ * Throws away the local copy of the whole page and fetches a fresh one.
227
+ * Happens automatically whenever the browser says the copy is void, so you
228
+ * rarely need to call it.
229
+ */
230
+ resync(reason?: DomResyncReason): Promise<void>;
231
+ /** Resolves once the mirror ends — stop(), a dead session, a transport failure. */
232
+ wait(): Promise<void>;
233
+ /** Stops mirroring and detaches the reader. Idempotent, safe in a `finally`. */
234
+ stop(): Promise<void>;
235
+ /** @internal Called by CloudBrowser with the opening snapshot. */
236
+ install(snapshot: DomSnapshot): void;
237
+ private reset;
238
+ private pump;
239
+ private handle;
240
+ private apply;
241
+ /** Records that a node's state is current as of `seq`. Never moves back. */
242
+ private mark;
243
+ /**
244
+ * Replaces `key` and every ancestor with copies, so the path from the root
245
+ * to the changed node has new identities and nothing else does. Returns the
246
+ * fresh copy of `key`, which the caller then edits in place.
247
+ *
248
+ * The walk crosses frame boundaries without noticing them: a document is a
249
+ * child of the `<iframe>` hosting it like any other, so a change deep inside
250
+ * an out-of-process frame still produces a new `root`.
251
+ */
252
+ private touch;
253
+ /**
254
+ * Registers a payload subtree and returns the copy that lives in the tree.
255
+ *
256
+ * `frame` is the id space the payload's ids belong to, and it changes here
257
+ * and nowhere else: a document node names its own frame, and everything
258
+ * below it counts in that frame. That is the whole of what crossing into an
259
+ * iframe means to a client.
260
+ */
261
+ private adopt;
262
+ /** Replaces a node's child list from a fresh payload for the same node. */
263
+ private mergeChildren;
264
+ private dropChildren;
265
+ /**
266
+ * Removes a subtree from the index. It descends through hosted documents
267
+ * like through anything else — they are children, and a frame stops being
268
+ * mirrored exactly when the element hosting it stops being expanded.
269
+ */
270
+ private forget;
271
+ }