@loxel.dev/pharos-browser 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.
@@ -0,0 +1,926 @@
1
+ import {
2
+ ACTIVATION_EVENT_TAG,
3
+ EVENT_TYPE_FULL_SNAPSHOT,
4
+ EVENT_TYPE_META,
5
+ EXCLUDE_CLASS,
6
+ MASK_CLASS,
7
+ MASK_TOKEN,
8
+ MAX_WINDOW_BYTES,
9
+ ReplayBuffer,
10
+ attachInteractions,
11
+ beginsWithSnapshot,
12
+ clearPersisted,
13
+ createDropLedger,
14
+ createUploadSink,
15
+ decide,
16
+ drainPersisted,
17
+ encodeForUpload,
18
+ envelopeMetaFor,
19
+ isQuotaExceeded,
20
+ maskText,
21
+ masksValue,
22
+ persistWindow,
23
+ postEnvelope,
24
+ randomSessionId,
25
+ resetQuotaDroppedWindows,
26
+ resolveConfig,
27
+ scrubAttribute,
28
+ scrubUrl
29
+ } from "./index-bc4bw3ba.js";
30
+ import"./index-c3taa3cg.js";
31
+
32
+ // src/replay/recorder.ts
33
+ import { record } from "rrweb";
34
+
35
+ // src/replay/budget.ts
36
+ var LONG_TASK_MS = 50;
37
+ var ESCALATION_COOLDOWN_MS = 5000;
38
+ var RECOVERY_AFTER_MS = 30000;
39
+ var MAX_DEGRADATION_LEVEL = 3;
40
+ var CEILING_RECORD_COOLDOWN_MS = 1e4;
41
+ var CHECKPOINT_MULTIPLIER = [1, 2, 4, 4];
42
+ var POINTER_MULTIPLIER = [1, 1, 1, 4];
43
+ function createBudget(opts) {
44
+ let level = 0;
45
+ let lastEscalationAt = Number.NEGATIVE_INFINITY;
46
+ let lastLongTaskAt = Number.NEGATIVE_INFINITY;
47
+ let lastCeilingRecordAt = Number.NEGATIVE_INFINITY;
48
+ let openCeilingRun = null;
49
+ const log = [];
50
+ const checkpointIntervalMs = () => opts.config.checkpointIntervalMs * CHECKPOINT_MULTIPLIER[level];
51
+ const mousemoveWaitMs = () => opts.config.mousemoveWaitMs * POINTER_MULTIPLIER[level];
52
+ const record = (reason) => {
53
+ const entry = {
54
+ at: opts.now(),
55
+ level,
56
+ reason,
57
+ checkpointIntervalMs: checkpointIntervalMs(),
58
+ mousemoveWaitMs: mousemoveWaitMs(),
59
+ occurrences: 1
60
+ };
61
+ log.push(entry);
62
+ openCeilingRun = reason === "memory-ceiling" ? entry : null;
63
+ return entry;
64
+ };
65
+ return {
66
+ level: () => level,
67
+ checkpointIntervalMs,
68
+ mousemoveWaitMs,
69
+ noteLongTask(durationMs) {
70
+ if (durationMs < LONG_TASK_MS)
71
+ return false;
72
+ const at = opts.now();
73
+ lastLongTaskAt = at;
74
+ if (level >= MAX_DEGRADATION_LEVEL)
75
+ return false;
76
+ if (at - lastEscalationAt < ESCALATION_COOLDOWN_MS)
77
+ return false;
78
+ lastEscalationAt = at;
79
+ level += 1;
80
+ record("long-task");
81
+ return true;
82
+ },
83
+ noteCeilingEviction() {
84
+ const at = opts.now();
85
+ if (openCeilingRun && at - lastCeilingRecordAt < CEILING_RECORD_COOLDOWN_MS) {
86
+ openCeilingRun.occurrences += 1;
87
+ return;
88
+ }
89
+ lastCeilingRecordAt = at;
90
+ record("memory-ceiling");
91
+ },
92
+ maybeRecover() {
93
+ if (level === 0)
94
+ return false;
95
+ if (opts.now() - lastLongTaskAt < RECOVERY_AFTER_MS)
96
+ return false;
97
+ level -= 1;
98
+ record("recovery");
99
+ return true;
100
+ },
101
+ records: () => log.map((entry) => ({ ...entry }))
102
+ };
103
+ }
104
+ function observeLongTasks(budget, onEscalate) {
105
+ const Ctor = globalThis.PerformanceObserver;
106
+ if (typeof Ctor !== "function")
107
+ return () => {};
108
+ let observer;
109
+ try {
110
+ observer = new Ctor((list) => {
111
+ let changed = false;
112
+ for (const entry of list.getEntries()) {
113
+ if (budget.noteLongTask(entry.duration))
114
+ changed = true;
115
+ }
116
+ if (changed)
117
+ onEscalate();
118
+ });
119
+ observer.observe({ type: "longtask", buffered: false });
120
+ } catch {
121
+ return () => {};
122
+ }
123
+ return () => {
124
+ try {
125
+ observer.disconnect();
126
+ } catch {}
127
+ };
128
+ }
129
+
130
+ // src/replay/privacy-hooks.ts
131
+ var RRWEB_INTERNAL_ATTRS = new Set([
132
+ "rr_width",
133
+ "rr_height",
134
+ "rr_scrollLeft",
135
+ "rr_scrollTop",
136
+ "rr_dataURL",
137
+ "_cssText"
138
+ ]);
139
+ var RENAMED_CONTENT_ATTRS = new Map([["rr_src", "src"]]);
140
+ function shouldMaskValueOf(el) {
141
+ if (decide(el) !== "record")
142
+ return true;
143
+ return masksValue(el);
144
+ }
145
+ function isStyleDiff(value) {
146
+ return typeof value === "object" && value !== null && !Array.isArray(value);
147
+ }
148
+ var SERIALIZED_ELEMENT = 2;
149
+ var SOURCE_MUTATION = 0;
150
+ var SOURCE_INPUT = 5;
151
+ function shouldMaskTextOf(el) {
152
+ if (!el)
153
+ return true;
154
+ return shouldMaskValueOf(el);
155
+ }
156
+ function maskTextHook(text, el) {
157
+ return shouldMaskTextOf(el) ? maskText(text) : text;
158
+ }
159
+ function maskInputHook(text, _el) {
160
+ return maskText(text);
161
+ }
162
+ function scrubAttributeValue(el, name, value) {
163
+ const rule = RENAMED_CONTENT_ATTRS.get(name) ?? name;
164
+ if (rule === "style" && isStyleDiff(value))
165
+ return scrubStyleDiff(el, value);
166
+ if (typeof value !== "string")
167
+ return value;
168
+ if (rule.toLowerCase() === "value" && shouldMaskValueOf(el))
169
+ return MASK_TOKEN;
170
+ return scrubAttribute(el, rule, value);
171
+ }
172
+ function scrubStyleDiff(el, diff) {
173
+ const out = {};
174
+ for (const [property, value] of Object.entries(diff)) {
175
+ if (typeof value === "string") {
176
+ out[property] = scrubStyleValue(el, property, value);
177
+ } else if (Array.isArray(value) && typeof value[0] === "string") {
178
+ out[property] = [scrubStyleValue(el, property, value[0]), value[1]];
179
+ } else {
180
+ out[property] = value;
181
+ }
182
+ }
183
+ return out;
184
+ }
185
+ function scrubStyleValue(el, property, value) {
186
+ const prefix = `${property}:`;
187
+ const scrubbed = scrubAttribute(el, "style", `${prefix}${value}`);
188
+ if (scrubbed === null)
189
+ return MASK_TOKEN;
190
+ return scrubbed.startsWith(prefix) ? scrubbed.slice(prefix.length) : MASK_TOKEN;
191
+ }
192
+ var inertDocuments = new WeakMap;
193
+ function inertDocumentFor(doc) {
194
+ const cached = inertDocuments.get(doc);
195
+ if (cached)
196
+ return cached;
197
+ let inert;
198
+ try {
199
+ inert = doc.implementation.createHTMLDocument("");
200
+ } catch {
201
+ return null;
202
+ }
203
+ inertDocuments.set(doc, inert);
204
+ return inert;
205
+ }
206
+ function surrogateElement(tagName, attributes, doc) {
207
+ const inert = inertDocumentFor(doc);
208
+ if (!inert)
209
+ return null;
210
+ let surrogate;
211
+ try {
212
+ surrogate = inert.createElement(tagName);
213
+ } catch {
214
+ try {
215
+ surrogate = inert.createElement("span");
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ for (const [name, value] of Object.entries(attributes)) {
221
+ if (typeof value !== "string")
222
+ continue;
223
+ if (RRWEB_INTERNAL_ATTRS.has(name))
224
+ continue;
225
+ try {
226
+ surrogate.setAttribute(name, value);
227
+ } catch {}
228
+ }
229
+ return surrogate;
230
+ }
231
+ var EXCLUDED_ELEMENT_KEPT_ATTRS = new Set([
232
+ "class",
233
+ "rr_width",
234
+ "rr_height",
235
+ "rr_scrollLeft",
236
+ "rr_scrollTop"
237
+ ]);
238
+ function excludeSerializedNode(node) {
239
+ if (node.attributes) {
240
+ for (const name of Object.keys(node.attributes)) {
241
+ if (EXCLUDED_ELEMENT_KEPT_ATTRS.has(name))
242
+ continue;
243
+ delete node.attributes[name];
244
+ }
245
+ }
246
+ node.childNodes = [];
247
+ }
248
+ function scrubSerializedNode(node, doc) {
249
+ if (node.type === SERIALIZED_ELEMENT && node.attributes && node.tagName) {
250
+ const surrogate = surrogateElement(node.tagName, node.attributes, doc);
251
+ if (surrogate && decide(surrogate) === "exclude") {
252
+ excludeSerializedNode(node);
253
+ return;
254
+ }
255
+ for (const [name, value] of Object.entries(node.attributes)) {
256
+ if (RRWEB_INTERNAL_ATTRS.has(name))
257
+ continue;
258
+ if (!surrogate) {
259
+ delete node.attributes[name];
260
+ continue;
261
+ }
262
+ const scrubbed = scrubAttributeValue(surrogate, name, value);
263
+ if (scrubbed === null)
264
+ delete node.attributes[name];
265
+ else
266
+ node.attributes[name] = scrubbed;
267
+ }
268
+ }
269
+ if (node.childNodes) {
270
+ for (const child of node.childNodes)
271
+ scrubSerializedNode(child, doc);
272
+ }
273
+ }
274
+ function scrubEventAttributes(event, ctx) {
275
+ const e = event;
276
+ const data = e.data;
277
+ if (!data)
278
+ return event;
279
+ const node = data.node;
280
+ if (node && typeof node === "object")
281
+ scrubSerializedNode(node, ctx.doc);
282
+ if (data.source === SOURCE_INPUT && typeof data.text === "string") {
283
+ const live = ctx.resolveNode(data.id);
284
+ const el = live && live.nodeType === 1 ? live : null;
285
+ data.text = el ? scrubAttributeValue(el, "value", data.text) : MASK_TOKEN;
286
+ }
287
+ if (data.source !== SOURCE_MUTATION)
288
+ return event;
289
+ const adds = data.adds;
290
+ if (Array.isArray(adds)) {
291
+ for (const add of adds) {
292
+ if (add?.node)
293
+ scrubSerializedNode(add.node, ctx.doc);
294
+ }
295
+ }
296
+ const attributes = data.attributes;
297
+ if (Array.isArray(attributes)) {
298
+ for (let i = attributes.length - 1;i >= 0; i--) {
299
+ const entry = attributes[i];
300
+ const live = ctx.resolveNode(entry.id);
301
+ if (!live || live.nodeType !== 1) {
302
+ attributes.splice(i, 1);
303
+ continue;
304
+ }
305
+ const el = live;
306
+ if (decide(el) === "exclude") {
307
+ attributes.splice(i, 1);
308
+ continue;
309
+ }
310
+ for (const [name, value] of Object.entries(entry.attributes)) {
311
+ if (RRWEB_INTERNAL_ATTRS.has(name))
312
+ continue;
313
+ entry.attributes[name] = scrubAttributeValue(el, name, value);
314
+ }
315
+ }
316
+ }
317
+ return event;
318
+ }
319
+
320
+ // src/replay/triggers.ts
321
+ var TRIGGER_MAX_WINDOW_EXTENSION_MS = 30000;
322
+ function contained(fn) {
323
+ try {
324
+ fn();
325
+ } catch {}
326
+ }
327
+ function beginsBeforeTrigger(events, triggeredAt) {
328
+ return events[0].timestamp <= triggeredAt;
329
+ }
330
+ function createTriggerCoordinator(opts) {
331
+ let pending = null;
332
+ let timer = null;
333
+ const noteDrop = (reason) => {
334
+ if (!opts.onDrop)
335
+ return;
336
+ contained(() => opts.onDrop(reason));
337
+ };
338
+ const clear = () => {
339
+ if (timer !== null) {
340
+ opts.clearTimer(timer);
341
+ timer = null;
342
+ }
343
+ };
344
+ const flush = (unloaded) => {
345
+ if (!pending)
346
+ return;
347
+ clear();
348
+ const current = pending;
349
+ pending = null;
350
+ const flushedAt = opts.now();
351
+ const events = opts.buffer.extract(current.at, flushedAt);
352
+ if (!events) {
353
+ noteDrop("no-snapshot");
354
+ return;
355
+ }
356
+ if (!beginsBeforeTrigger(events, current.at)) {
357
+ noteDrop("fabricated-window");
358
+ return;
359
+ }
360
+ let degraded = [];
361
+ contained(() => {
362
+ degraded = opts.degraded();
363
+ });
364
+ const replayWindow = {
365
+ events,
366
+ trigger: {
367
+ kind: current.kind,
368
+ ...current.name !== undefined ? { name: current.name } : {},
369
+ at: current.at,
370
+ occurrences: current.occurrences,
371
+ flushedAt,
372
+ unloaded
373
+ },
374
+ degraded,
375
+ config: opts.config
376
+ };
377
+ contained(() => opts.sink(replayWindow));
378
+ };
379
+ const schedule = () => {
380
+ if (!pending)
381
+ return;
382
+ const deadline = Math.min(opts.now() + opts.config.postRollMs, pending.at + TRIGGER_MAX_WINDOW_EXTENSION_MS);
383
+ const delay = deadline - opts.now();
384
+ if (delay <= 0) {
385
+ flush(false);
386
+ return;
387
+ }
388
+ timer = opts.setTimer(() => {
389
+ timer = null;
390
+ flush(false);
391
+ }, delay);
392
+ };
393
+ return {
394
+ trigger(source) {
395
+ if (pending) {
396
+ pending.occurrences += 1;
397
+ clear();
398
+ schedule();
399
+ return;
400
+ }
401
+ pending = {
402
+ kind: source.kind,
403
+ ...source.name !== undefined ? { name: source.name } : {},
404
+ at: opts.now(),
405
+ occurrences: 1
406
+ };
407
+ schedule();
408
+ },
409
+ flushNow(unloaded) {
410
+ flush(unloaded);
411
+ },
412
+ cancel() {
413
+ clear();
414
+ pending = null;
415
+ },
416
+ pending() {
417
+ return pending !== null;
418
+ },
419
+ checkpoint(events, startedAt, unloaded) {
420
+ if (!beginsWithSnapshot(events)) {
421
+ noteDrop("no-snapshot");
422
+ return;
423
+ }
424
+ const flushedAt = opts.now();
425
+ let degraded = [];
426
+ contained(() => {
427
+ degraded = opts.degraded();
428
+ });
429
+ const replayWindow = {
430
+ events,
431
+ trigger: { kind: "checkpoint", at: startedAt, occurrences: 1, flushedAt, unloaded },
432
+ degraded,
433
+ config: opts.config
434
+ };
435
+ contained(() => opts.sink(replayWindow));
436
+ }
437
+ };
438
+ }
439
+
440
+ // src/replay/recorder.ts
441
+ function noop() {}
442
+ function defaultScheduleIdle(cb) {
443
+ const ric = globalThis.requestIdleCallback;
444
+ if (typeof ric === "function")
445
+ ric(() => cb(), { timeout: 2000 });
446
+ else
447
+ setTimeout(cb, 0);
448
+ }
449
+ function buildRecordOptions(input) {
450
+ return {
451
+ emit: input.emit,
452
+ blockClass: EXCLUDE_CLASS,
453
+ maskTextClass: MASK_CLASS,
454
+ maskTextSelector: "*",
455
+ maskAllInputs: true,
456
+ maskTextFn: maskTextHook,
457
+ maskInputFn: maskInputHook,
458
+ checkoutEveryNms: input.checkoutEveryNms,
459
+ recordAfter: "DOMContentLoaded",
460
+ sampling: {
461
+ mousemove: input.mousemoveWaitMs,
462
+ mouseInteraction: true,
463
+ scroll: 150,
464
+ input: "last"
465
+ },
466
+ recordCanvas: false,
467
+ inlineImages: false,
468
+ collectFonts: false
469
+ };
470
+ }
471
+ function scrubMetaHref(event) {
472
+ if (event.type !== EVENT_TYPE_META)
473
+ return;
474
+ const data = event.data;
475
+ if (!data || typeof data.href !== "string")
476
+ return;
477
+ data.href = scrubUrl(data.href);
478
+ }
479
+ var active = null;
480
+ function inertHandle(config) {
481
+ return {
482
+ config,
483
+ stop() {},
484
+ trigger() {},
485
+ flushNow() {},
486
+ stats: () => ({
487
+ snapshots: 0,
488
+ bufferBytes: 0,
489
+ segments: 0,
490
+ interactionWork: 0,
491
+ degradationLevel: 0
492
+ })
493
+ };
494
+ }
495
+ function startRecording(opts) {
496
+ const config = resolveConfig(opts);
497
+ if (active !== null)
498
+ return inertHandle(config);
499
+ const ownership = {};
500
+ active = ownership;
501
+ const now = opts.now ?? (() => Date.now());
502
+ const setTimer = opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
503
+ const clearTimer = opts.clearTimer ?? ((handle) => clearTimeout(handle));
504
+ const scheduleIdle = opts.scheduleIdle ?? defaultScheduleIdle;
505
+ const doc = opts.doc ?? document;
506
+ const target = opts.target ?? doc;
507
+ const lifecycleTarget = opts.lifecycleTarget ?? opts.doc?.defaultView ?? globalThis;
508
+ const budget = createBudget({ config, now });
509
+ const buffer = new ReplayBuffer({
510
+ maxBytes: config.maxBytes,
511
+ preRollMs: config.preRollMs,
512
+ onCeilingEviction: () => budget.noteCeilingEviction(),
513
+ onSegmentClosed: (events, startedAt) => {
514
+ if (lastUnloadShip !== null && lastUnloadShip.startedAt === startedAt) {
515
+ const unchanged = lastUnloadShip.eventCount === events.length;
516
+ lastUnloadShip = null;
517
+ if (unchanged)
518
+ return;
519
+ }
520
+ coordinator.checkpoint(events, startedAt, false);
521
+ }
522
+ });
523
+ const coordinator = createTriggerCoordinator({
524
+ buffer,
525
+ sink: opts.sink,
526
+ onDrop: opts.onWindowDropped,
527
+ config,
528
+ degraded: () => budget.records(),
529
+ now,
530
+ setTimer,
531
+ clearTimer
532
+ });
533
+ let stopped = false;
534
+ let snapshots = 0;
535
+ let stopRrweb;
536
+ let checkpointTimer = null;
537
+ let appliedMousemoveWaitMs = config.mousemoveWaitMs;
538
+ let detachLongTasks = noop;
539
+ let detachLifecycle = noop;
540
+ let lastUnloadShip = null;
541
+ const onEmit = (event) => {
542
+ if (stopped)
543
+ return;
544
+ const e = event;
545
+ if (e.type === EVENT_TYPE_FULL_SNAPSHOT)
546
+ snapshots += 1;
547
+ scrubMetaHref(e);
548
+ buffer.append(scrubEventAttributes(e, { doc, resolveNode: (id) => record.mirror.getNode(id) }));
549
+ };
550
+ const startSession = () => {
551
+ stopRrweb = record(buildRecordOptions({
552
+ emit: onEmit,
553
+ checkoutEveryNms: budget.checkpointIntervalMs() * 2,
554
+ mousemoveWaitMs: appliedMousemoveWaitMs
555
+ }));
556
+ if (typeof stopRrweb === "function")
557
+ return true;
558
+ teardown();
559
+ return false;
560
+ };
561
+ const tracker = attachInteractions({
562
+ target,
563
+ now,
564
+ pointerIntervalMs: config.mousemoveWaitMs,
565
+ emit: (payload) => {
566
+ if (stopped)
567
+ return;
568
+ try {
569
+ record.addCustomEvent(ACTIVATION_EVENT_TAG, payload);
570
+ } catch {}
571
+ }
572
+ });
573
+ const teardown = () => {
574
+ if (stopped)
575
+ return;
576
+ stopped = true;
577
+ if (checkpointTimer !== null) {
578
+ clearTimer(checkpointTimer);
579
+ checkpointTimer = null;
580
+ }
581
+ detachLongTasks();
582
+ detachLifecycle();
583
+ tracker.detach();
584
+ stopRrweb?.();
585
+ stopRrweb = undefined;
586
+ coordinator.cancel();
587
+ buffer.clear();
588
+ if (active === ownership)
589
+ active = null;
590
+ };
591
+ const applyBudget = () => {
592
+ const wait = budget.mousemoveWaitMs();
593
+ if (wait === appliedMousemoveWaitMs || stopped)
594
+ return;
595
+ appliedMousemoveWaitMs = wait;
596
+ tracker.setPointerIntervalMs(wait);
597
+ stopRrweb?.();
598
+ startSession();
599
+ };
600
+ const scheduleCheckpoint = () => {
601
+ if (stopped)
602
+ return;
603
+ checkpointTimer = setTimer(() => {
604
+ checkpointTimer = null;
605
+ if (stopped)
606
+ return;
607
+ scheduleIdle(() => {
608
+ if (stopped)
609
+ return;
610
+ try {
611
+ record.takeFullSnapshot(true);
612
+ } catch {}
613
+ });
614
+ budget.maybeRecover();
615
+ applyBudget();
616
+ scheduleCheckpoint();
617
+ }, budget.checkpointIntervalMs());
618
+ };
619
+ const started = startSession();
620
+ const onPageHidden = () => {
621
+ if (stopped)
622
+ return;
623
+ coordinator.flushNow(true);
624
+ const current = buffer.currentSegment();
625
+ if (current) {
626
+ const unchanged = lastUnloadShip !== null && lastUnloadShip.startedAt === current.startedAt && lastUnloadShip.eventCount === current.events.length;
627
+ if (!unchanged) {
628
+ lastUnloadShip = { startedAt: current.startedAt, eventCount: current.events.length };
629
+ coordinator.checkpoint(current.events, current.startedAt, true);
630
+ }
631
+ }
632
+ };
633
+ const onVisibilityChange = () => {
634
+ if (doc.visibilityState !== "hidden")
635
+ return;
636
+ onPageHidden();
637
+ };
638
+ const attachLifecycle = () => {
639
+ if (typeof lifecycleTarget?.addEventListener !== "function")
640
+ return noop;
641
+ lifecycleTarget.addEventListener("pagehide", onPageHidden);
642
+ lifecycleTarget.addEventListener("visibilitychange", onVisibilityChange);
643
+ return () => {
644
+ lifecycleTarget.removeEventListener("pagehide", onPageHidden);
645
+ lifecycleTarget.removeEventListener("visibilitychange", onVisibilityChange);
646
+ };
647
+ };
648
+ scheduleCheckpoint();
649
+ if (started) {
650
+ detachLongTasks = observeLongTasks(budget, applyBudget);
651
+ detachLifecycle = attachLifecycle();
652
+ }
653
+ return {
654
+ config,
655
+ stop() {
656
+ teardown();
657
+ },
658
+ trigger(source) {
659
+ if (stopped)
660
+ return;
661
+ coordinator.trigger(source);
662
+ },
663
+ flushNow() {
664
+ if (stopped)
665
+ return;
666
+ coordinator.flushNow(true);
667
+ },
668
+ stats() {
669
+ const s = buffer.stats();
670
+ return {
671
+ snapshots,
672
+ bufferBytes: s.bytes,
673
+ segments: s.segments,
674
+ interactionWork: tracker.state.work,
675
+ degradationLevel: budget.level()
676
+ };
677
+ }
678
+ };
679
+ }
680
+
681
+ // src/replay/wire.ts
682
+ var SESSION_STORAGE_KEY = "pharos-replay-session";
683
+ function defaultSessionStorage() {
684
+ try {
685
+ return globalThis.sessionStorage ?? null;
686
+ } catch {
687
+ return null;
688
+ }
689
+ }
690
+ function resolveSessionId(explicit, storage) {
691
+ if (explicit !== undefined && explicit.length > 0)
692
+ return explicit;
693
+ try {
694
+ const stored = storage?.getItem(SESSION_STORAGE_KEY);
695
+ if (typeof stored === "string" && stored.length > 0)
696
+ return stored;
697
+ } catch {}
698
+ const fresh = randomSessionId();
699
+ try {
700
+ storage?.setItem(SESSION_STORAGE_KEY, fresh);
701
+ } catch {}
702
+ return fresh;
703
+ }
704
+ var SESSION_DROPS_KEY = "pharos-replay-drops";
705
+ function readCarriedDrops(storage, sessionId) {
706
+ try {
707
+ const raw = storage?.getItem(SESSION_DROPS_KEY);
708
+ if (typeof raw !== "string" || raw.length === 0)
709
+ return 0;
710
+ const parsed = JSON.parse(raw);
711
+ if (parsed === null || parsed.sessionId !== sessionId)
712
+ return 0;
713
+ const n = parsed.total;
714
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0)
715
+ return 0;
716
+ return Math.floor(n);
717
+ } catch {
718
+ return 0;
719
+ }
720
+ }
721
+ function createDurableDropLedger(storage, sessionId) {
722
+ const inner = createDropLedger();
723
+ let carried = readCarriedDrops(storage, sessionId);
724
+ const writeThrough = () => {
725
+ try {
726
+ storage?.setItem(SESSION_DROPS_KEY, JSON.stringify({ sessionId, total: carried + inner.total() }));
727
+ } catch {}
728
+ };
729
+ return {
730
+ note(reason) {
731
+ inner.note(reason);
732
+ writeThrough();
733
+ },
734
+ total: () => carried + inner.total(),
735
+ byReason: () => inner.byReason(),
736
+ reset() {
737
+ inner.reset();
738
+ carried = 0;
739
+ try {
740
+ storage?.removeItem(SESSION_DROPS_KEY);
741
+ } catch {}
742
+ }
743
+ };
744
+ }
745
+ function startReplayUpload(opts) {
746
+ const {
747
+ sessionId: explicitSessionId,
748
+ compress,
749
+ endpoint,
750
+ appKey,
751
+ fetchImpl,
752
+ indexedDB: injectedIdb,
753
+ sessionStorage: injectedStorage,
754
+ ...recorderOptions
755
+ } = opts;
756
+ const storage = injectedStorage === undefined ? defaultSessionStorage() : injectedStorage;
757
+ const sessionId = resolveSessionId(explicitSessionId, storage);
758
+ const drops = createDurableDropLedger(storage, sessionId);
759
+ const target = { endpoint, appKey, ...fetchImpl ? { fetchImpl } : {} };
760
+ const now = opts.now ?? (() => Date.now());
761
+ const doc = opts.doc ?? globalThis.document;
762
+ const lifecycleTarget = opts.lifecycleTarget ?? opts.doc?.defaultView ?? globalThis;
763
+ let stopped = false;
764
+ const inflight = new Set;
765
+ const track = (p) => {
766
+ const settledP = p.catch(() => {
767
+ return;
768
+ });
769
+ inflight.add(settledP);
770
+ settledP.then(() => {
771
+ inflight.delete(settledP);
772
+ });
773
+ };
774
+ const drainInflight = async () => {
775
+ while (inflight.size > 0)
776
+ await Promise.all([...inflight]);
777
+ };
778
+ const idb = () => {
779
+ if (injectedIdb)
780
+ return injectedIdb;
781
+ try {
782
+ return globalThis.indexedDB ?? null;
783
+ } catch {
784
+ return null;
785
+ }
786
+ };
787
+ const uploadSink = createUploadSink({
788
+ endpoint,
789
+ appKey,
790
+ ...fetchImpl ? { fetchImpl } : {},
791
+ ...compress !== undefined ? { compress } : {},
792
+ sessionId,
793
+ drops,
794
+ track,
795
+ onDeliveryFailure: (window) => persistForLater(window)
796
+ });
797
+ const persistForLater = async (window) => {
798
+ let handingToStorage = false;
799
+ try {
800
+ const factory = idb();
801
+ if (!factory) {
802
+ drops.note("storage");
803
+ return;
804
+ }
805
+ const { body, compressed } = await encodeForUpload(window, compress);
806
+ if (stopped)
807
+ return;
808
+ if (body.byteLength > MAX_WINDOW_BYTES) {
809
+ drops.note("oversized");
810
+ return;
811
+ }
812
+ handingToStorage = true;
813
+ await persistWindow(body, envelopeMetaFor(window, sessionId, compressed, drops.total()), now, factory, drops);
814
+ } catch (err) {
815
+ if (!handingToStorage)
816
+ drops.note("delivery");
817
+ else if (isQuotaExceeded(err))
818
+ drops.note("quota");
819
+ else
820
+ drops.note("storage");
821
+ }
822
+ };
823
+ const sendPersisted = async (body, meta) => {
824
+ return await postEnvelope(body, meta, target);
825
+ };
826
+ const sink = (window) => {
827
+ if (stopped)
828
+ return;
829
+ if (window.trigger.unloaded) {
830
+ track(persistForLater(window));
831
+ return;
832
+ }
833
+ uploadSink(window);
834
+ };
835
+ let draining = false;
836
+ const drainNow = () => {
837
+ if (stopped || draining)
838
+ return;
839
+ const factory = idb();
840
+ if (!factory)
841
+ return;
842
+ draining = true;
843
+ track(drainPersisted(sendPersisted, now, factory).finally(() => {
844
+ draining = false;
845
+ }));
846
+ };
847
+ const onVisible = () => {
848
+ if (stopped)
849
+ return;
850
+ if (doc?.visibilityState === "hidden")
851
+ return;
852
+ drainNow();
853
+ };
854
+ const attachDrainOnVisible = () => {
855
+ if (typeof lifecycleTarget?.addEventListener !== "function")
856
+ return () => {
857
+ return;
858
+ };
859
+ lifecycleTarget.addEventListener("visibilitychange", onVisible);
860
+ return () => {
861
+ lifecycleTarget.removeEventListener("visibilitychange", onVisible);
862
+ };
863
+ };
864
+ const onOnline = () => {
865
+ if (stopped)
866
+ return;
867
+ drainNow();
868
+ };
869
+ const attachDrainOnOnline = () => {
870
+ if (typeof lifecycleTarget?.addEventListener !== "function")
871
+ return () => {
872
+ return;
873
+ };
874
+ lifecycleTarget.addEventListener("online", onOnline);
875
+ return () => {
876
+ lifecycleTarget.removeEventListener("online", onOnline);
877
+ };
878
+ };
879
+ const handle = startRecording({
880
+ ...recorderOptions,
881
+ sink,
882
+ onWindowDropped: (reason) => {
883
+ drops.note(reason);
884
+ }
885
+ });
886
+ drainNow();
887
+ const detachDrainOnVisible = attachDrainOnVisible();
888
+ const detachDrainOnOnline = attachDrainOnOnline();
889
+ return {
890
+ get config() {
891
+ return handle.config;
892
+ },
893
+ sessionId,
894
+ drops: () => drops,
895
+ settled: drainInflight,
896
+ trigger: (source) => handle.trigger(source),
897
+ flushNow: () => handle.flushNow(),
898
+ stats: () => handle.stats(),
899
+ stop() {
900
+ if (stopped)
901
+ return;
902
+ stopped = true;
903
+ handle.stop();
904
+ detachDrainOnVisible();
905
+ detachDrainOnOnline();
906
+ const already = [...inflight];
907
+ track((async () => {
908
+ await Promise.all(already);
909
+ const f = idb();
910
+ if (f)
911
+ await clearPersisted(f);
912
+ drops.reset();
913
+ resetQuotaDroppedWindows();
914
+ try {
915
+ storage?.removeItem(SESSION_STORAGE_KEY);
916
+ } catch {}
917
+ })());
918
+ }
919
+ };
920
+ }
921
+ export {
922
+ SESSION_DROPS_KEY,
923
+ SESSION_STORAGE_KEY,
924
+ resolveSessionId,
925
+ startReplayUpload
926
+ };