@lvce-editor/renderer-process 30.57.1 → 30.57.2
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/rendererProcessMain.js +573 -551
- package/dist/sessionReplayWorkerMain.js +142 -132
- package/package.json +1 -1
|
@@ -1,219 +1,229 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// ../session-replay-worker/src/protocol.ts
|
|
2
|
+
var version = 1;
|
|
3
|
+
var maxEventBytes = 75e4;
|
|
4
|
+
var maxSessionBytes = 64 * 1024 * 1024;
|
|
5
|
+
var bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
6
|
+
var validateSession = (value) => {
|
|
7
|
+
const session = value;
|
|
8
|
+
if (session?.version !== version || !Array.isArray(session.events) || session.events.length > 2e5 || bytes(session) > maxSessionBytes) {
|
|
9
|
+
throw new Error("Unsupported or oversized session replay");
|
|
9
10
|
}
|
|
10
11
|
let time = 0;
|
|
11
12
|
for (const [sequence, event] of session.events.entries()) {
|
|
12
|
-
if (event.sequence !== sequence || !Number.isFinite(event.timestamp) || event.timestamp < time || ![
|
|
13
|
-
throw new Error(
|
|
13
|
+
if (!event || event.sequence !== sequence || !Number.isFinite(event.timestamp) || event.timestamp < time || !["frame", "message"].includes(event.type)) {
|
|
14
|
+
throw new Error("Invalid session replay event sequence");
|
|
14
15
|
}
|
|
15
|
-
if (bytes(event) > maxEventBytes) throw new Error(
|
|
16
|
+
if (bytes(event) > maxEventBytes) throw new Error("Session replay event is too large");
|
|
16
17
|
time = event.timestamp;
|
|
17
18
|
}
|
|
18
|
-
return session
|
|
19
|
+
return session;
|
|
19
20
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const frames = session.events.filter((event) => event.type === 'frame');
|
|
25
|
-
if (!frames.length) throw new Error('This session has no visual frames')
|
|
21
|
+
var loadContent = (value) => {
|
|
22
|
+
const session = validateSession(value);
|
|
23
|
+
const frames = session.events.filter((event) => event.type === "frame");
|
|
24
|
+
if (frames.length === 0) throw new Error("This session has no visual frames");
|
|
26
25
|
const duration = session.events.at(-1)?.timestamp || 0;
|
|
27
26
|
const seek = (timestamp) => {
|
|
28
|
-
const position = Math.max(0, Math.min(duration,
|
|
27
|
+
const position = Math.max(0, Math.min(duration, timestamp || 0));
|
|
29
28
|
let low = 0;
|
|
30
29
|
let high = frames.length;
|
|
31
30
|
while (low < high) {
|
|
32
|
-
const middle =
|
|
31
|
+
const middle = low + high >>> 1;
|
|
33
32
|
if (frames[middle].timestamp <= position) low = middle + 1;
|
|
34
33
|
else high = middle;
|
|
35
34
|
}
|
|
36
|
-
return {
|
|
35
|
+
return { duration, frame: frames[Math.max(0, low - 1)].data, position };
|
|
37
36
|
};
|
|
38
|
-
return { duration, seek }
|
|
37
|
+
return { duration, seek };
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
// ../session-replay-worker/src/recorder.ts
|
|
41
|
+
var createRecorder = ({
|
|
42
|
+
fetch: request = globalThis.fetch,
|
|
43
|
+
now = () => performance.now(),
|
|
44
|
+
storage: storage2
|
|
45
|
+
}) => {
|
|
42
46
|
let metadata;
|
|
43
47
|
let options;
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const events = [];
|
|
49
|
+
const pending = [];
|
|
46
50
|
let size = 0;
|
|
47
51
|
let origin = 0;
|
|
48
52
|
let remote;
|
|
49
53
|
let uploadPromise;
|
|
50
|
-
let lastError =
|
|
54
|
+
let lastError = "";
|
|
51
55
|
let saved = 0;
|
|
52
56
|
let uploaded = 0;
|
|
53
57
|
const start = async (config) => {
|
|
54
|
-
if (metadata) throw new Error(
|
|
58
|
+
if (metadata) throw new Error("A session is already recording");
|
|
55
59
|
options = config;
|
|
56
60
|
origin = now();
|
|
57
|
-
metadata = {
|
|
58
|
-
if (options.local) await
|
|
59
|
-
return metadata.id
|
|
61
|
+
metadata = { createdAt: (/* @__PURE__ */ new Date()).toISOString(), id: crypto.randomUUID(), version };
|
|
62
|
+
if (options.local) await storage2.save(metadata, []);
|
|
63
|
+
return metadata.id;
|
|
60
64
|
};
|
|
61
|
-
const record = async (type, data) => {
|
|
62
|
-
if (!metadata) return
|
|
63
|
-
const event = { sequence: events.length, timestamp: Math.max(events.at(-1)?.timestamp || 0, now() - origin), type
|
|
65
|
+
const record = async (...[type, data]) => {
|
|
66
|
+
if (!metadata) return;
|
|
67
|
+
const event = { data, sequence: events.length, timestamp: Math.max(events.at(-1)?.timestamp || 0, now() - origin), type };
|
|
64
68
|
const eventSize = bytes(event);
|
|
65
69
|
if (eventSize > maxEventBytes || size + eventSize > maxSessionBytes)
|
|
66
|
-
throw new Error(
|
|
70
|
+
throw new Error("Session replay storage limit reached; download this session and start a new one");
|
|
67
71
|
size += eventSize;
|
|
68
72
|
events.push(event);
|
|
69
73
|
if (options.upload) pending.push(event);
|
|
70
74
|
if (options.local) {
|
|
71
|
-
await
|
|
75
|
+
await storage2.save(metadata, [event]);
|
|
72
76
|
saved++;
|
|
73
77
|
}
|
|
74
78
|
};
|
|
75
79
|
const doUpload = async () => {
|
|
76
|
-
if (!options?.upload ||
|
|
80
|
+
if (!options?.upload || pending.length === 0) return;
|
|
77
81
|
const endpoint = new URL(options.endpoint);
|
|
78
|
-
if (![
|
|
79
|
-
const headers = {
|
|
82
|
+
if (!["https:", "http:"].includes(endpoint.protocol)) throw new Error("Invalid session replay backend URL");
|
|
83
|
+
const headers = { "Content-Type": "application/json", ...options.token && { Authorization: `Bearer ${options.token}` } };
|
|
80
84
|
const send = async (url, body) => {
|
|
81
85
|
const response = await request(url, {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
credentials: 'include',
|
|
84
|
-
headers,
|
|
85
86
|
body: JSON.stringify(body),
|
|
86
|
-
|
|
87
|
+
credentials: "include",
|
|
88
|
+
headers,
|
|
89
|
+
method: "POST",
|
|
90
|
+
signal: AbortSignal.timeout(1e4)
|
|
87
91
|
});
|
|
88
|
-
if (!response.ok) throw new Error(`Session replay upload failed (${response.status})`)
|
|
89
|
-
return response.json()
|
|
92
|
+
if (!response.ok) throw new Error(`Session replay upload failed (${response.status})`);
|
|
93
|
+
return response.json();
|
|
90
94
|
};
|
|
91
|
-
if (!remote) remote = await send(endpoint, {
|
|
92
|
-
while (pending.length) {
|
|
95
|
+
if (!remote) remote = await send(endpoint, { createdAt: metadata.createdAt, version });
|
|
96
|
+
while (pending.length > 0) {
|
|
93
97
|
const batch = [];
|
|
94
98
|
let batchBytes = 0;
|
|
95
99
|
for (const event of pending) {
|
|
96
100
|
const length = bytes(event);
|
|
97
|
-
if (batch.length && (batchBytes + length >
|
|
101
|
+
if (batch.length > 0 && (batchBytes + length > 8e5 || batch.length >= 250)) break;
|
|
98
102
|
batch.push(event);
|
|
99
103
|
batchBytes += length;
|
|
100
104
|
}
|
|
101
105
|
const url = new URL(endpoint);
|
|
102
|
-
url.pathname = `${url.pathname.replace(/\/$/,
|
|
103
|
-
await send(url, { uploadToken: remote.uploadToken
|
|
106
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${encodeURIComponent(remote.id)}/events`;
|
|
107
|
+
await send(url, { events: batch, uploadToken: remote.uploadToken });
|
|
104
108
|
pending.splice(0, batch.length);
|
|
105
109
|
uploaded += batch.length;
|
|
106
110
|
}
|
|
107
111
|
};
|
|
108
112
|
const flush = () => {
|
|
109
|
-
uploadPromise ||= doUpload()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
122
|
-
return uploadPromise
|
|
113
|
+
uploadPromise ||= doUpload().then(
|
|
114
|
+
() => {
|
|
115
|
+
lastError = "";
|
|
116
|
+
},
|
|
117
|
+
(error) => {
|
|
118
|
+
lastError = error.message;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
).finally(() => {
|
|
122
|
+
uploadPromise = void 0;
|
|
123
|
+
});
|
|
124
|
+
return uploadPromise;
|
|
123
125
|
};
|
|
124
126
|
return {
|
|
125
|
-
start,
|
|
126
|
-
record,
|
|
127
|
-
flush,
|
|
128
127
|
export: () => ({ ...metadata, events }),
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
flush,
|
|
129
|
+
record,
|
|
130
|
+
start,
|
|
131
|
+
status: () => ({
|
|
132
|
+
bytes: size,
|
|
133
|
+
error: lastError,
|
|
134
|
+
events: events.length,
|
|
135
|
+
id: metadata?.id,
|
|
136
|
+
pending: pending.length,
|
|
137
|
+
saved,
|
|
138
|
+
uploaded
|
|
139
|
+
})
|
|
140
|
+
};
|
|
131
141
|
};
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// ../session-replay-worker/src/storage.ts
|
|
144
|
+
var withoutSessionId = ({ sessionId, ...event }) => event;
|
|
145
|
+
var open = () => new Promise((resolve, reject) => {
|
|
146
|
+
const request = indexedDB.open("lvce-session-replays", 1);
|
|
147
|
+
request.onupgradeneeded = () => {
|
|
148
|
+
request.result.createObjectStore("sessions", { keyPath: "id" });
|
|
149
|
+
request.result.createObjectStore("events", { keyPath: ["sessionId", "sequence"] });
|
|
150
|
+
};
|
|
151
|
+
request.onsuccess = () => resolve(request.result);
|
|
152
|
+
request.onerror = () => reject(request.error || new Error("Replay storage open failed"));
|
|
153
|
+
});
|
|
154
|
+
var createStorage = async () => {
|
|
145
155
|
const db = await open();
|
|
146
|
-
const transaction = (stores, action) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
156
|
+
const transaction = (stores, action) => new Promise((resolve, reject) => {
|
|
157
|
+
const tx = db.transaction(stores, "readwrite");
|
|
158
|
+
action(tx);
|
|
159
|
+
tx.oncomplete = () => resolve();
|
|
160
|
+
tx.onerror = tx.onabort = () => reject(tx.error || new Error("Replay storage transaction aborted"));
|
|
161
|
+
});
|
|
153
162
|
return {
|
|
154
|
-
save: (session, events) =>
|
|
155
|
-
transaction(['sessions', 'events'], (tx) => {
|
|
156
|
-
tx.objectStore('sessions').put(session);
|
|
157
|
-
for (const event of events) tx.objectStore('events').put({ ...event, sessionId: session.id });
|
|
158
|
-
}),
|
|
159
|
-
read: (id) =>
|
|
160
|
-
new Promise((resolve, reject) => {
|
|
161
|
-
const tx = db.transaction(['sessions', 'events']);
|
|
162
|
-
const metadata = tx.objectStore('sessions').get(id);
|
|
163
|
-
const events = tx.objectStore('events').getAll(IDBKeyRange.bound([id, 0], [id, Number.MAX_SAFE_INTEGER]));
|
|
164
|
-
tx.oncomplete = () =>
|
|
165
|
-
metadata.result
|
|
166
|
-
? resolve({ ...metadata.result, events: events.result.map(({ sessionId, ...event }) => event) })
|
|
167
|
-
: reject(new Error('Session replay not found'));
|
|
168
|
-
tx.onerror = () => reject(tx.error);
|
|
169
|
-
}),
|
|
170
163
|
close: () => db.close(),
|
|
171
|
-
|
|
164
|
+
read: (id) => new Promise((resolve, reject) => {
|
|
165
|
+
const tx = db.transaction(["sessions", "events"]);
|
|
166
|
+
const metadata = tx.objectStore("sessions").get(id);
|
|
167
|
+
const events = tx.objectStore("events").getAll(IDBKeyRange.bound([id, 0], [id, Number.MAX_SAFE_INTEGER]));
|
|
168
|
+
tx.oncomplete = () => metadata.result ? resolve({ ...metadata.result, events: events.result.map(withoutSessionId) }) : reject(new Error("Session replay not found"));
|
|
169
|
+
tx.onerror = () => reject(tx.error || new Error("Replay storage read failed"));
|
|
170
|
+
}),
|
|
171
|
+
save: (session, events) => transaction(["sessions", "events"], (tx) => {
|
|
172
|
+
tx.objectStore("sessions").put(session);
|
|
173
|
+
for (const event of events) tx.objectStore("events").put({ ...event, sessionId: session.id });
|
|
174
|
+
})
|
|
175
|
+
};
|
|
172
176
|
};
|
|
173
177
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
recorder = createRecorder({ storage: options.local ? await getStorage() : undefined });
|
|
183
|
-
const id = await recorder.start(options);
|
|
184
|
-
clearInterval(timer);
|
|
185
|
-
timer = setInterval(() => recorder.flush().catch(() => {}), 2000);
|
|
186
|
-
return id
|
|
187
|
-
},
|
|
188
|
-
record: (type, data) => recorder.record(type, data),
|
|
189
|
-
flush: () => recorder.flush(),
|
|
178
|
+
// ../session-replay-worker/src/worker.ts
|
|
179
|
+
var storage;
|
|
180
|
+
var recorder;
|
|
181
|
+
var content;
|
|
182
|
+
var queue = Promise.resolve();
|
|
183
|
+
var timer;
|
|
184
|
+
var getStorage = async () => storage ||= await createStorage();
|
|
185
|
+
var commands = {
|
|
190
186
|
export: () => recorder.export(),
|
|
191
|
-
|
|
187
|
+
flush: () => recorder.flush(),
|
|
192
188
|
async load(source) {
|
|
193
189
|
let session;
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
if ("localId" in source) {
|
|
191
|
+
const localStorage = await getStorage();
|
|
192
|
+
session = await localStorage.read(source.localId);
|
|
193
|
+
} else if ("url" in source) {
|
|
194
|
+
const response = await fetch(source.url, { credentials: "include", headers: { Accept: "application/json" } });
|
|
195
|
+
if (!response.ok) throw new Error(`Cannot load session replay (${response.status})`);
|
|
198
196
|
session = await response.json();
|
|
199
|
-
} else session = source
|
|
197
|
+
} else ({ session } = source);
|
|
200
198
|
content = loadContent(session);
|
|
201
|
-
return content.seek(0)
|
|
199
|
+
return content.seek(0);
|
|
202
200
|
},
|
|
201
|
+
record: (...params) => recorder.record(...params),
|
|
203
202
|
seek: (timestamp) => content.seek(timestamp),
|
|
203
|
+
async start(options) {
|
|
204
|
+
recorder = createRecorder({ storage: options.local ? await getStorage() : void 0 });
|
|
205
|
+
const id = await recorder.start(options);
|
|
206
|
+
clearInterval(timer);
|
|
207
|
+
timer = setInterval(() => {
|
|
208
|
+
void recorder.flush().catch(() => {
|
|
209
|
+
});
|
|
210
|
+
}, 2e3);
|
|
211
|
+
return id;
|
|
212
|
+
},
|
|
213
|
+
status: () => recorder.status(),
|
|
204
214
|
async stop() {
|
|
205
215
|
clearInterval(timer);
|
|
206
216
|
await recorder?.flush();
|
|
207
217
|
storage?.close();
|
|
208
|
-
}
|
|
218
|
+
}
|
|
209
219
|
};
|
|
210
|
-
|
|
220
|
+
globalThis.onmessage = ({ data: { id, method, params = [] } }) => {
|
|
211
221
|
queue = queue.then(async () => {
|
|
212
222
|
try {
|
|
213
|
-
if (!Object.hasOwn(commands, method)) throw new Error(
|
|
214
|
-
|
|
223
|
+
if (!Object.hasOwn(commands, method)) throw new Error("Unknown session replay command");
|
|
224
|
+
globalThis.postMessage({ id, result: await commands[method](...params) });
|
|
215
225
|
} catch (error) {
|
|
216
|
-
|
|
226
|
+
globalThis.postMessage({ error: error instanceof Error ? error.message : String(error), id });
|
|
217
227
|
}
|
|
218
228
|
});
|
|
219
229
|
};
|