Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/replay/backend/api/ReplayOutput.ts
1030 views
1
import { IOutputChange } from '~shared/interfaces/ISaSession';
2
3
interface IOutputAtCommand {
4
output: any;
5
bytes: number;
6
changes: { type: string; path: string }[];
7
}
8
export default class ReplayOutput {
9
private changesByCommandId = new Map<number, IOutputAtCommand>();
10
11
public getLatestOutput(commandId: number): IOutputAtCommand {
12
for (let id = commandId; id >= 0; id -= 1) {
13
if (this.changesByCommandId.has(id)) {
14
return this.changesByCommandId.get(id);
15
}
16
}
17
}
18
19
public onOutput(changes: IOutputChange[]): void {
20
for (const output of changes) {
21
const path = JSON.parse(output.path) as PropertyKey[];
22
23
let prevCommandId = output.lastCommandId;
24
while (prevCommandId >= 0) {
25
if (this.changesByCommandId.has(prevCommandId)) {
26
break;
27
}
28
prevCommandId -= 1;
29
}
30
31
let startOutput = this.changesByCommandId.get(prevCommandId)?.output;
32
33
if (!startOutput) {
34
if (typeof path[0] === 'number') startOutput = [];
35
else startOutput = {};
36
} else if (Array.isArray(startOutput)) {
37
startOutput = [...startOutput];
38
} else {
39
startOutput = { ...startOutput };
40
}
41
42
if (!this.changesByCommandId.has(output.lastCommandId)) {
43
this.changesByCommandId.set(output.lastCommandId, { output: null, changes: [], bytes: 0 });
44
}
45
const changeEntry = this.changesByCommandId.get(output.lastCommandId);
46
changeEntry.output = startOutput;
47
48
let propertyOwner = startOutput;
49
const property = path.pop();
50
// re-build objects up to the last entry so we don't modify previous entries
51
for (const entry of path) {
52
const existing = propertyOwner[entry];
53
if (existing && typeof existing === 'object') {
54
if (Array.isArray(existing)) propertyOwner[entry] = [...existing];
55
else propertyOwner[entry] = { ...existing };
56
}
57
propertyOwner = propertyOwner[entry];
58
}
59
60
if (output.type === 'delete') {
61
if (Array.isArray(propertyOwner)) {
62
propertyOwner.splice(property as number, 1);
63
} else {
64
delete propertyOwner[property];
65
}
66
} else if (output.type === 'reorder') {
67
const order = JSON.parse(output.value) as number[];
68
if (property) {
69
const startArray = propertyOwner[property];
70
propertyOwner[property] = order.map(x => startArray[x]);
71
} else {
72
changeEntry.output = order.map(x => startOutput[x]);
73
}
74
} else {
75
propertyOwner[property] = JSON.parse(output.value);
76
}
77
78
let flatPath = '';
79
for (const part of path.concat([property])) {
80
if (typeof part === 'number') {
81
flatPath += `[${part}]`;
82
} else if (typeof part === 'string' && part.includes('.')) {
83
flatPath += `["${part}"]`;
84
} else {
85
flatPath += `.${part as string}`;
86
}
87
}
88
changeEntry.changes.push({ path: flatPath, type: output.type });
89
changeEntry.bytes = Buffer.byteLength(JSON.stringify(changeEntry.output));
90
}
91
}
92
}
93
94