@sailfish-ai/recorder 1.10.5 → 1.10.6

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,210 @@
1
+ // Async chunked DOM serializer producing rrweb-compatible node trees.
2
+ // Used when chunkSnapshot is enabled to break the initial DOM walk into
3
+ // yielding chunks, reducing Total Blocking Time.
4
+ import { yieldToMain } from "./scheduler";
5
+ // rrweb serialized node type IDs (matches @sailfish-rrweb/types NodeType)
6
+ const DOCUMENT = 0;
7
+ const DOCUMENT_TYPE = 1;
8
+ const ELEMENT = 2;
9
+ const TEXT = 3;
10
+ const CDATA = 4;
11
+ const COMMENT = 5;
12
+ /**
13
+ * ID offset for chunked serialization. Avoids collision with rrweb's internal
14
+ * genId counter (starts at 1, increments per node) which is used by the
15
+ * MutationObserver for nodes added after recording starts.
16
+ */
17
+ const CHUNK_ID_BASE = 100_001;
18
+ /**
19
+ * Async chunked DOM serializer. Walks the document tree producing an
20
+ * rrweb-compatible serialized node tree, yielding to the main thread
21
+ * every `chunkSize` nodes or `maxChunkMs` milliseconds (whichever first).
22
+ *
23
+ * Nodes removed between yield points are silently skipped.
24
+ * Returns null if the document cannot be serialized.
25
+ */
26
+ export async function chunkedSnapshot(doc, mirror, options = {}) {
27
+ const chunkSize = options.chunkSize ?? 500;
28
+ const maxChunkMs = options.maxChunkMs ?? 16;
29
+ const { blockClass, blockSelector, maskTextClass, maskTextSelector } = options;
30
+ let nextId = CHUNK_ID_BASE;
31
+ let nodeCount = 0;
32
+ let chunkStart = performance.now();
33
+ function genId(node) {
34
+ if (mirror.hasNode(node))
35
+ return mirror.getId(node);
36
+ const id = nextId++;
37
+ mirror.add(node, { id });
38
+ return id;
39
+ }
40
+ async function maybeYield() {
41
+ nodeCount++;
42
+ if (nodeCount % chunkSize === 0 ||
43
+ performance.now() - chunkStart > maxChunkMs) {
44
+ await yieldToMain();
45
+ chunkStart = performance.now();
46
+ }
47
+ }
48
+ function isBlocked(el) {
49
+ if (blockClass && el.classList?.contains(blockClass))
50
+ return true;
51
+ if (blockSelector) {
52
+ try {
53
+ if (el.matches(blockSelector))
54
+ return true;
55
+ }
56
+ catch {
57
+ /* invalid selector */
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+ function shouldMaskText(node) {
63
+ const parent = node.parentElement;
64
+ if (!parent)
65
+ return false;
66
+ if (maskTextClass && parent.closest(`.${maskTextClass}`))
67
+ return true;
68
+ if (maskTextSelector) {
69
+ try {
70
+ if (parent.closest(maskTextSelector))
71
+ return true;
72
+ }
73
+ catch {
74
+ /* invalid selector */
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+ function getAttributes(el) {
80
+ const attrs = {};
81
+ for (let i = 0; i < el.attributes.length; i++) {
82
+ const attr = el.attributes[i];
83
+ attrs[attr.name] = attr.value;
84
+ }
85
+ const tag = el.tagName;
86
+ // Capture live input/textarea/select values
87
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
88
+ const input = el;
89
+ if (input.value !== undefined && input.value !== "") {
90
+ attrs.value = input.value;
91
+ }
92
+ }
93
+ if (tag === "INPUT") {
94
+ const input = el;
95
+ if (input.type === "checkbox" || input.type === "radio") {
96
+ attrs.checked = input.checked;
97
+ }
98
+ }
99
+ if (tag === "OPTION") {
100
+ attrs.selected = el.selected;
101
+ }
102
+ return attrs;
103
+ }
104
+ async function serialize(node) {
105
+ // Node removed from DOM during a yield — skip silently
106
+ if (node !== doc && !node.parentNode)
107
+ return null;
108
+ await maybeYield();
109
+ switch (node.nodeType) {
110
+ case Node.DOCUMENT_NODE: {
111
+ const id = genId(node);
112
+ const childNodes = [];
113
+ for (const child of Array.from(node.childNodes)) {
114
+ const s = await serialize(child);
115
+ if (s)
116
+ childNodes.push(s);
117
+ }
118
+ return { type: DOCUMENT, childNodes, id };
119
+ }
120
+ case Node.DOCUMENT_TYPE_NODE: {
121
+ const dt = node;
122
+ return {
123
+ type: DOCUMENT_TYPE,
124
+ childNodes: [],
125
+ id: genId(node),
126
+ name: dt.name,
127
+ publicId: dt.publicId,
128
+ systemId: dt.systemId,
129
+ };
130
+ }
131
+ case Node.ELEMENT_NODE: {
132
+ const el = node;
133
+ const id = genId(node);
134
+ const tagName = el.tagName.toLowerCase();
135
+ const isSVG = el.namespaceURI === "http://www.w3.org/2000/svg" || undefined;
136
+ // Blocked elements: placeholder without children
137
+ if (isBlocked(el)) {
138
+ return {
139
+ type: ELEMENT,
140
+ tagName,
141
+ attributes: getAttributes(el),
142
+ childNodes: [],
143
+ id,
144
+ isSVG,
145
+ needBlock: true,
146
+ };
147
+ }
148
+ const childNodes = [];
149
+ for (const child of Array.from(el.childNodes)) {
150
+ if (!child.parentNode)
151
+ continue; // removed during yield
152
+ const s = await serialize(child);
153
+ if (s)
154
+ childNodes.push(s);
155
+ }
156
+ const result = {
157
+ type: ELEMENT,
158
+ tagName,
159
+ attributes: getAttributes(el),
160
+ childNodes,
161
+ id,
162
+ };
163
+ if (isSVG)
164
+ result.isSVG = true;
165
+ return result;
166
+ }
167
+ case Node.TEXT_NODE: {
168
+ let textContent = node.textContent || "";
169
+ const parent = node.parentElement;
170
+ const isStyle = parent?.tagName === "STYLE" || undefined;
171
+ // Mask text content when under maskTextClass/maskTextSelector
172
+ if (!isStyle && shouldMaskText(node) && textContent) {
173
+ textContent = textContent.replace(/\S/g, "*");
174
+ }
175
+ const result = {
176
+ type: TEXT,
177
+ textContent,
178
+ childNodes: [],
179
+ id: genId(node),
180
+ };
181
+ if (isStyle)
182
+ result.isStyle = true;
183
+ return result;
184
+ }
185
+ case Node.COMMENT_NODE:
186
+ return {
187
+ type: COMMENT,
188
+ textContent: node.textContent || "",
189
+ childNodes: [],
190
+ id: genId(node),
191
+ };
192
+ case Node.CDATA_SECTION_NODE:
193
+ return {
194
+ type: CDATA,
195
+ textContent: "",
196
+ childNodes: [],
197
+ id: genId(node),
198
+ };
199
+ default:
200
+ return null;
201
+ }
202
+ }
203
+ try {
204
+ return await serialize(doc);
205
+ }
206
+ catch (e) {
207
+ console.warn("[Sailfish] chunkSnapshot serialization error:", e);
208
+ return null;
209
+ }
210
+ }
@@ -0,0 +1,95 @@
1
+ import { y as e } from "./index-CJhJKC_I.js";
2
+ async function chunkedSnapshot(t, n, o = {}) {
3
+ const s = o.chunkSize ?? 500, r = o.maxChunkMs ?? 16, { blockClass: c, blockSelector: a, maskTextClass: i, maskTextSelector: d } = o;
4
+ let u = 100001, l = 0, N = performance.now();
5
+ function genId(e2) {
6
+ if (n.hasNode(e2)) return n.getId(e2);
7
+ const t2 = u++;
8
+ return n.add(e2, { id: t2 }), t2;
9
+ }
10
+ function getAttributes(e2) {
11
+ const t2 = {};
12
+ for (let n3 = 0; n3 < e2.attributes.length; n3++) {
13
+ const o2 = e2.attributes[n3];
14
+ t2[o2.name] = o2.value;
15
+ }
16
+ const n2 = e2.tagName;
17
+ if ("INPUT" === n2 || "TEXTAREA" === n2 || "SELECT" === n2) {
18
+ const n3 = e2;
19
+ void 0 !== n3.value && "" !== n3.value && (t2.value = n3.value);
20
+ }
21
+ if ("INPUT" === n2) {
22
+ const n3 = e2;
23
+ "checkbox" !== n3.type && "radio" !== n3.type || (t2.checked = n3.checked);
24
+ }
25
+ return "OPTION" === n2 && (t2.selected = e2.selected), t2;
26
+ }
27
+ try {
28
+ return await (async function serialize(n2) {
29
+ if (n2 !== t && !n2.parentNode) return null;
30
+ switch (await (async function maybeYield() {
31
+ l++, (l % s === 0 || performance.now() - N > r) && (await e(), N = performance.now());
32
+ })(), n2.nodeType) {
33
+ case Node.DOCUMENT_NODE: {
34
+ const e2 = genId(n2), t2 = [];
35
+ for (const e3 of Array.from(n2.childNodes)) {
36
+ const n3 = await serialize(e3);
37
+ n3 && t2.push(n3);
38
+ }
39
+ return { type: 0, childNodes: t2, id: e2 };
40
+ }
41
+ case Node.DOCUMENT_TYPE_NODE: {
42
+ const e2 = n2;
43
+ return { type: 1, childNodes: [], id: genId(n2), name: e2.name, publicId: e2.publicId, systemId: e2.systemId };
44
+ }
45
+ case Node.ELEMENT_NODE: {
46
+ const e2 = n2, t2 = genId(n2), o2 = e2.tagName.toLowerCase(), s2 = "http://www.w3.org/2000/svg" === e2.namespaceURI || void 0;
47
+ if ((function isBlocked(e3) {
48
+ var _a;
49
+ if (c && ((_a = e3.classList) == null ? void 0 : _a.contains(c))) return true;
50
+ if (a) try {
51
+ if (e3.matches(a)) return true;
52
+ } catch {
53
+ }
54
+ return false;
55
+ })(e2)) return { type: 2, tagName: o2, attributes: getAttributes(e2), childNodes: [], id: t2, isSVG: s2, needBlock: true };
56
+ const r2 = [];
57
+ for (const t3 of Array.from(e2.childNodes)) {
58
+ if (!t3.parentNode) continue;
59
+ const e3 = await serialize(t3);
60
+ e3 && r2.push(e3);
61
+ }
62
+ const i2 = { type: 2, tagName: o2, attributes: getAttributes(e2), childNodes: r2, id: t2 };
63
+ return s2 && (i2.isSVG = true), i2;
64
+ }
65
+ case Node.TEXT_NODE: {
66
+ let e2 = n2.textContent || "";
67
+ const t2 = n2.parentElement, o2 = "STYLE" === (t2 == null ? void 0 : t2.tagName) || void 0;
68
+ !o2 && (function shouldMaskText(e3) {
69
+ const t3 = e3.parentElement;
70
+ if (!t3) return false;
71
+ if (i && t3.closest(`.${i}`)) return true;
72
+ if (d) try {
73
+ if (t3.closest(d)) return true;
74
+ } catch {
75
+ }
76
+ return false;
77
+ })(n2) && e2 && (e2 = e2.replace(/\S/g, "*"));
78
+ const s2 = { type: 3, textContent: e2, childNodes: [], id: genId(n2) };
79
+ return o2 && (s2.isStyle = true), s2;
80
+ }
81
+ case Node.COMMENT_NODE:
82
+ return { type: 5, textContent: n2.textContent || "", childNodes: [], id: genId(n2) };
83
+ case Node.CDATA_SECTION_NODE:
84
+ return { type: 4, textContent: "", childNodes: [], id: genId(n2) };
85
+ default:
86
+ return null;
87
+ }
88
+ })(t);
89
+ } catch (e2) {
90
+ return console.warn("[Sailfish] chunkSnapshot serialization error:", e2), null;
91
+ }
92
+ }
93
+ export {
94
+ chunkedSnapshot
95
+ };
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const e = require("./index-CywnXnF8.js");
4
+ exports.chunkedSnapshot = async function chunkedSnapshot(t, n, o = {}) {
5
+ const s = o.chunkSize ?? 500, r = o.maxChunkMs ?? 16, { blockClass: c, blockSelector: a, maskTextClass: i, maskTextSelector: d } = o;
6
+ let u = 100001, l = 0, N = performance.now();
7
+ function genId(e2) {
8
+ if (n.hasNode(e2)) return n.getId(e2);
9
+ const t2 = u++;
10
+ return n.add(e2, { id: t2 }), t2;
11
+ }
12
+ function getAttributes(e2) {
13
+ const t2 = {};
14
+ for (let n3 = 0; n3 < e2.attributes.length; n3++) {
15
+ const o2 = e2.attributes[n3];
16
+ t2[o2.name] = o2.value;
17
+ }
18
+ const n2 = e2.tagName;
19
+ if ("INPUT" === n2 || "TEXTAREA" === n2 || "SELECT" === n2) {
20
+ const n3 = e2;
21
+ void 0 !== n3.value && "" !== n3.value && (t2.value = n3.value);
22
+ }
23
+ if ("INPUT" === n2) {
24
+ const n3 = e2;
25
+ "checkbox" !== n3.type && "radio" !== n3.type || (t2.checked = n3.checked);
26
+ }
27
+ return "OPTION" === n2 && (t2.selected = e2.selected), t2;
28
+ }
29
+ try {
30
+ return await (async function serialize(n2) {
31
+ if (n2 !== t && !n2.parentNode) return null;
32
+ switch (await (async function maybeYield() {
33
+ l++, (l % s === 0 || performance.now() - N > r) && (await e.yieldToMain(), N = performance.now());
34
+ })(), n2.nodeType) {
35
+ case Node.DOCUMENT_NODE: {
36
+ const e2 = genId(n2), t2 = [];
37
+ for (const e3 of Array.from(n2.childNodes)) {
38
+ const n3 = await serialize(e3);
39
+ n3 && t2.push(n3);
40
+ }
41
+ return { type: 0, childNodes: t2, id: e2 };
42
+ }
43
+ case Node.DOCUMENT_TYPE_NODE: {
44
+ const e2 = n2;
45
+ return { type: 1, childNodes: [], id: genId(n2), name: e2.name, publicId: e2.publicId, systemId: e2.systemId };
46
+ }
47
+ case Node.ELEMENT_NODE: {
48
+ const e2 = n2, t2 = genId(n2), o2 = e2.tagName.toLowerCase(), s2 = "http://www.w3.org/2000/svg" === e2.namespaceURI || void 0;
49
+ if ((function isBlocked(e3) {
50
+ var _a;
51
+ if (c && ((_a = e3.classList) == null ? void 0 : _a.contains(c))) return true;
52
+ if (a) try {
53
+ if (e3.matches(a)) return true;
54
+ } catch {
55
+ }
56
+ return false;
57
+ })(e2)) return { type: 2, tagName: o2, attributes: getAttributes(e2), childNodes: [], id: t2, isSVG: s2, needBlock: true };
58
+ const r2 = [];
59
+ for (const t3 of Array.from(e2.childNodes)) {
60
+ if (!t3.parentNode) continue;
61
+ const e3 = await serialize(t3);
62
+ e3 && r2.push(e3);
63
+ }
64
+ const i2 = { type: 2, tagName: o2, attributes: getAttributes(e2), childNodes: r2, id: t2 };
65
+ return s2 && (i2.isSVG = true), i2;
66
+ }
67
+ case Node.TEXT_NODE: {
68
+ let e2 = n2.textContent || "";
69
+ const t2 = n2.parentElement, o2 = "STYLE" === (t2 == null ? void 0 : t2.tagName) || void 0;
70
+ !o2 && (function shouldMaskText(e3) {
71
+ const t3 = e3.parentElement;
72
+ if (!t3) return false;
73
+ if (i && t3.closest(`.${i}`)) return true;
74
+ if (d) try {
75
+ if (t3.closest(d)) return true;
76
+ } catch {
77
+ }
78
+ return false;
79
+ })(n2) && e2 && (e2 = e2.replace(/\S/g, "*"));
80
+ const s2 = { type: 3, textContent: e2, childNodes: [], id: genId(n2) };
81
+ return o2 && (s2.isStyle = true), s2;
82
+ }
83
+ case Node.COMMENT_NODE:
84
+ return { type: 5, textContent: n2.textContent || "", childNodes: [], id: genId(n2) };
85
+ case Node.CDATA_SECTION_NODE:
86
+ return { type: 4, textContent: "", childNodes: [], id: genId(n2) };
87
+ default:
88
+ return null;
89
+ }
90
+ })(t);
91
+ } catch (e2) {
92
+ return console.warn("[Sailfish] chunkSnapshot serialization error:", e2), null;
93
+ }
94
+ };