* introduce pako and add general packer interface

* add tests for packer

* use function API instead of class API for better tree shaking support

* refcatoring the rollup bundle config
This commit is contained in:
yz-yu
2026-04-01 12:00:00 +08:00
committed by GitHub
parent f1adef4693
commit dcad6ff922
23 changed files with 316 additions and 218 deletions

12
src/declarations/pako/index.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare module 'pako/dist/pako_deflate' {
export const deflate: any;
}
declare module 'pako/dist/pako_inflate' {
export const inflate: any;
}
declare module 'pako' {
export const deflate: any;
export const inflate: any;
}

View File

@@ -8,6 +8,7 @@ export {
MouseInteractions,
ReplayerEvents,
} from './types';
export { pack, unpack } from './packer';
const { addCustomEvent } = record;

10
src/packer/base.ts Normal file
View File

@@ -0,0 +1,10 @@
import { eventWithTime } from '../types';
export type PackFn = (event: eventWithTime) => string;
export type UnpackFn = (raw: string) => eventWithTime;
export type eventWithTimeAndPacker = eventWithTime & {
v: string;
};
export const MARK = 'v1';

2
src/packer/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { pack } from './pack';
export { unpack } from './unpack';

10
src/packer/pack.ts Normal file
View File

@@ -0,0 +1,10 @@
import { deflate } from 'pako/dist/pako_deflate';
import { PackFn, MARK, eventWithTimeAndPacker } from './base';
export const pack: PackFn = (event) => {
const _e: eventWithTimeAndPacker = {
...event,
v: MARK,
};
return deflate(JSON.stringify(_e), { to: 'string' });
};

31
src/packer/unpack.ts Normal file
View File

@@ -0,0 +1,31 @@
import { inflate } from 'pako/dist/pako_inflate';
import { UnpackFn, eventWithTimeAndPacker, MARK } from './base';
import { eventWithTime } from '../types';
export const unpack: UnpackFn = (raw: string) => {
if (typeof raw !== 'string') {
return raw;
}
try {
const e: eventWithTime = JSON.parse(raw);
if (e.timestamp) {
return e;
}
} catch (error) {
// ignore and continue
}
try {
const e: eventWithTimeAndPacker = JSON.parse(
inflate(raw, { to: 'string' }),
);
if (e.v === MARK) {
return e;
}
throw new Error(
`These events were packed with packer ${e.v} which is incompatible with current packer ${MARK}.`,
);
} catch (error) {
console.error(error);
throw new Error('Unknown data format.');
}
};

View File

@@ -25,7 +25,9 @@ function wrapEvent(e: event): eventWithTime {
let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void;
function record(options: recordOptions = {}): listenerHandler | undefined {
function record<T = eventWithTime>(
options: recordOptions<T> = {},
): listenerHandler | undefined {
const {
emit,
checkoutEveryNms,
@@ -36,6 +38,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
maskAllInputs = false,
hooks,
mousemoveWait = 50,
packFn,
} = options;
// runtime checks for user options
if (!emit) {
@@ -47,7 +50,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
let lastFullSnapshotEvent: eventWithTime;
let incrementalSnapshotCount = 0;
wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
emit(e, isCheckout);
emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout);
if (e.type === EventType.FullSnapshot) {
lastFullSnapshotEvent = e;
incrementalSnapshotCount = 0;
@@ -120,7 +123,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
handlers.push(
initObservers(
{
mutationCb: m =>
mutationCb: (m) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -140,7 +143,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
mouseInteractionCb: d =>
mouseInteractionCb: (d) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -150,7 +153,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
scrollCb: p =>
scrollCb: (p) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -160,7 +163,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
viewportResizeCb: d =>
viewportResizeCb: (d) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -170,7 +173,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
inputCb: v =>
inputCb: (v) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -180,7 +183,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
mediaInteractionCb: p =>
mediaInteractionCb: (p) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -190,7 +193,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
},
}),
),
styleSheetRuleCb: r =>
styleSheetRuleCb: (r) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
@@ -233,7 +236,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
);
}
return () => {
handlers.forEach(h => h());
handlers.forEach((h) => h());
};
} catch (error) {
// TODO: handle internal error

View File

@@ -59,11 +59,19 @@ export class Replayer {
private playing: boolean = false;
constructor(events: eventWithTime[], config?: Partial<playerConfig>) {
constructor(
events: Array<eventWithTime | string>,
config?: Partial<playerConfig>,
) {
if (events.length < 2) {
throw new Error('Replayer need at least 2 events.');
}
this.events = events;
this.events = events.map((e) => {
if (config && config.unpackFn) {
return config.unpackFn(e as string);
}
return e as eventWithTime;
});
this.handleResize = this.handleResize.bind(this);
const defaultConfig: playerConfig = {
@@ -179,7 +187,10 @@ export class Replayer {
this.emitter.emit(ReplayerEvents.Resume);
}
public addEvent(event: eventWithTime) {
public addEvent(rawEvent: eventWithTime | string) {
const event = this.config.unpackFn
? this.config.unpackFn(rawEvent as string)
: (rawEvent as eventWithTime);
const castFn = this.getCastFn(event, true);
castFn();
}
@@ -329,7 +340,7 @@ export class Replayer {
.forEach((css: HTMLLinkElement) => {
if (!css.sheet) {
if (unloadSheets.size === 0) {
this.timer.clear(); // artificial pause
this.timer.clear(); // artificial pause
this.emitter.emit(ReplayerEvents.LoadStylesheetStart);
timer = window.setTimeout(() => {
if (this.playing) {
@@ -364,7 +375,7 @@ export class Replayer {
const { data: d } = e;
switch (d.source) {
case IncrementalSource.Mutation: {
d.removes.forEach(mutation => {
d.removes.forEach((mutation) => {
const target = mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
@@ -432,13 +443,13 @@ export class Replayer {
}
};
d.adds.forEach(mutation => {
d.adds.forEach((mutation) => {
appendNode(mutation);
});
while (queue.length) {
if (queue.every(m => !Boolean(mirror.getNode(m.parentId)))) {
return queue.forEach(m => this.warnNodeNotFound(d, m.node.id));
if (queue.every((m) => !Boolean(mirror.getNode(m.parentId)))) {
return queue.forEach((m) => this.warnNodeNotFound(d, m.node.id));
}
const mutation = queue.shift()!;
appendNode(mutation);
@@ -448,14 +459,14 @@ export class Replayer {
Object.assign(this.missingNodeRetryMap, missingNodeMap);
}
d.texts.forEach(mutation => {
d.texts.forEach((mutation) => {
const target = mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
target.textContent = mutation.value;
});
d.attributes.forEach(mutation => {
d.attributes.forEach((mutation) => {
const target = mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
@@ -481,7 +492,7 @@ export class Replayer {
const lastPosition = d.positions[d.positions.length - 1];
this.moveAndHover(d, lastPosition.x, lastPosition.y, lastPosition.id);
} else {
d.positions.forEach(p => {
d.positions.forEach((p) => {
const action = {
doAction: () => {
this.moveAndHover(d, p.x, p.y, p.id);
@@ -702,7 +713,7 @@ export class Replayer {
private hoverElements(el: Element) {
this.iframe
.contentDocument!.querySelectorAll('.\\:hover')
.forEach(hoveredEl => {
.forEach((hoveredEl) => {
hoveredEl.classList.remove(':hover');
});
let currentEl: Element | null = el;

View File

@@ -1,4 +1,5 @@
import { serializedNodeWithId, idNodeMap, INode } from 'rrweb-snapshot';
import { PackFn, UnpackFn } from './packer/base';
export enum EventType {
DomContentLoaded,
@@ -125,8 +126,8 @@ export type eventWithTime = event & {
export type blockClass = string | RegExp;
export type recordOptions = {
emit?: (e: eventWithTime, isCheckout?: boolean) => void;
export type recordOptions<T> = {
emit?: (e: T, isCheckout?: boolean) => void;
checkoutEveryNth?: number;
checkoutEveryNms?: number;
blockClass?: blockClass;
@@ -135,6 +136,7 @@ export type recordOptions = {
inlineStylesheet?: boolean;
hooks?: hooksParam;
mousemoveWait?: number;
packFn?: PackFn;
};
export type observerParam = {
@@ -319,6 +321,7 @@ export type playerConfig = {
liveMode: boolean;
insertStyleRules: string[];
triggerFocus: boolean;
unpackFn?: UnpackFn;
};
export type playerMetaData = {