split out worker stuff

This commit is contained in:
Robin Appelman 2022-09-04 15:49:13 +02:00
commit a1838c5164
5 changed files with 294 additions and 218 deletions

184
www/src/App.tsx Normal file
View file

@ -0,0 +1,184 @@
import {Packet} from "./parser";
import React, {useCallback, Component} from 'react'
import {useDropzone} from 'react-dropzone'
import {Header} from "./header";
import {PacketDetails, PacketTable} from "./table";
import {isSearchEmpty, SearchBar, SearchFilter} from "./search";
import {DemoWorker, ResponseMessageData} from "./rpc";
let _style = require('../styles/style.css');
export interface PacketMeta {
index: number,
tick: number,
ty: PacketType,
}
export enum PacketType {
Signon = 1,
Message = 2,
SyncTick = 3,
ConsoleCmd = 4,
UserCmd = 5,
DataTables = 6,
Stop = 7,
StringTables = 8,
}
interface AppState {
loading: boolean,
header: Header | null,
progress: number,
packets: PacketMeta[],
prop_names: Map<string, { table: string, prop: string }>,
class_names: Map<number, string>,
active: Packet | null,
activeIndex: number | null,
search: SearchFilter,
matches: number[],
worker: DemoWorker | null,
}
export class App extends Component<{}, AppState> {
state: AppState = {
loading: false,
header: null,
progress: 0,
packets: [],
prop_names: new Map(),
class_names: new Map(),
active: null,
activeIndex: null,
search: {
entity: 0,
search: "",
class_ids: [],
prop_ids: [],
},
matches: [],
worker: null
}
onSearch = debounce((search: SearchFilter) => {
if (!isSearchEmpty(search) && this.state.worker) {
console.log(search);
this.state.worker.search(search).then(matches => this.setState({matches}));
}
this.setState({search});
}, 500)
selectPacket = (packet: number) => {
this.state.worker.get(packet).then(packet => this.setState({active: packet}));
}
async load(data: ArrayBuffer) {
this.setState({loading: true});
const worker = new DemoWorker();
this.setState({worker});
const parsed = await worker.load(data, (progress) => {
this.setState({progress});
});
const prop_names = new Map();
for (let prop of parsed.prop_names) {
prop_names.set(prop.identifier, prop);
}
const class_names = new Map();
for (let c of parsed.class_names) {
class_names.set(c.identifier, c.name);
}
this.setState({
loading: false,
packets: parsed.packets,
header: parsed.header,
prop_names,
class_names
});
}
filteredPackets(): PacketMeta[] {
if (isSearchEmpty(this.state.search)) {
return this.state.packets;
} else {
return this.state.matches.map(index => this.state.packets[index]);
}
}
render() {
if (this.state.loading && this.state.progress > 0) {
return (
<>
<h1>Loading</h1>
<progress value={this.state.progress} max={100}/>
</>
)
} else if (this.state.loading) {
return (
<>
<h1>Loading</h1>
</>
)
} else if (this.state.packets.length) {
let active = <></>;
const packets = this.filteredPackets();
if (this.state.active) {
active = <div className="details"><PacketDetails packet={this.state.active}
search={this.state.search}
prop_names={this.state.prop_names}
class_names={this.state.class_names}/></div>
}
return (
<>
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
prop_names={this.state.prop_names}/>
<div className="packets">
<PacketTable packets={packets} class_names={this.state.class_names}
activeIndex={this.state.activeIndex}
prop_names={this.state.prop_names}
onClick={this.selectPacket}/>
{active}
</div>
</>
)
} else {
return (
<>
<DemoDropzone onDrop={(data) => this.load(data)}/>
</>
)
}
}
}
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
const onDropCb = useCallback((acceptedFiles: File[]) => {
let reader = new FileReader();
reader.readAsArrayBuffer(acceptedFiles[0]);
reader.addEventListener('load', () => {
let result = reader.result as ArrayBuffer;
onDrop(result)
});
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop: onDropCb})
return (
<div className="dropzone" {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the demo file here ...</p> :
<p>Drag 'n' drop a demo here, or click to select a file</p>
}
</div>
)
}
function debounce(func: Function, timeout = 300) {
let timer: any;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

View file

@ -1,208 +1,7 @@
import {Packet} from "./parser";
import React, {useCallback, Component} from 'react'
import {useDropzone} from 'react-dropzone'
import ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import {Header} from "./header";
import {PacketDetails, PacketTable} from "./table";
import {filterPacket, isSearchEmpty, SearchBar, SearchFilter} from "./search";
import {ResponseMessageData} from "./rpc";
let _style = require('../styles/style.css');
export interface PacketMeta {
index: number,
tick: number,
ty: PacketType,
}
export enum PacketType {
Signon = 1,
Message = 2,
SyncTick = 3,
ConsoleCmd = 4,
UserCmd = 5,
DataTables = 6,
Stop = 7,
StringTables = 8,
}
interface AppState {
loading: boolean,
header: Header | null,
progress: number,
packets: PacketMeta[],
prop_names: Map<string, { table: string, prop: string }>,
class_names: Map<number, string>,
active: Packet | null,
activeIndex: number | null,
search: SearchFilter,
matches: number[],
worker: Worker
}
class App extends Component<{}, AppState> {
state: AppState = {
loading: false,
header: null,
progress: 0,
packets: [],
prop_names: new Map(),
class_names: new Map(),
active: null,
activeIndex: null,
search: {
entity: 0,
search: "",
class_ids: [],
prop_ids: [],
},
matches: [],
worker: null
}
onSearch = debounce((search: SearchFilter) => {
if (!isSearchEmpty(search)) {
console.log(search);
this.state.worker.postMessage({type: "search", filter: search});
}
this.setState({search});
}, 500)
load(data: ArrayBuffer) {
this.setState({loading: true});
const worker = new Worker('./worker.js');
this.setState({worker});
worker.addEventListener("message", (event: MessageEvent<ResponseMessageData>) => {
const data = event.data;
if (data.type !== "progress") {
console.log(data);
}
switch (data.type) {
case "progress":
let progress = data.progress;
this.setState({progress});
break;
case "done":
const prop_names = new Map();
for (let prop of data.prop_names) {
prop_names.set(prop.identifier, prop);
}
const class_names = new Map();
for (let c of data.class_names) {
class_names.set(c.identifier, c.name);
}
this.setState({
loading: false,
packets: data.packets,
header: data.header,
prop_names,
class_names
});
break;
case "packet":
this.setState({active: data.packet});
break;
case "search_result":
this.setState({matches: data.matches});
break;
}
});
worker.postMessage({
type: "data",
data
}, [data]);
}
filteredPackets(): PacketMeta[] {
if (isSearchEmpty(this.state.search)) {
return this.state.packets;
} else {
return this.state.matches.map(index => this.state.packets[index]);
}
}
render() {
if (this.state.loading && this.state.progress > 0) {
return (
<>
<h1>Loading</h1>
<progress value={this.state.progress} max={100}/>
</>
)
} else if (this.state.loading) {
return (
<>
<h1>Loading</h1>
</>
)
} else if (this.state.packets.length) {
let active = <></>;
const packets = this.filteredPackets();
if (this.state.active) {
active = <div className="details"><PacketDetails packet={this.state.active}
search={this.state.search}
prop_names={this.state.prop_names}
class_names={this.state.class_names}/></div>
}
return (
<>
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
prop_names={this.state.prop_names}/>
<div className="packets">
<PacketTable packets={packets} class_names={this.state.class_names}
activeIndex={this.state.activeIndex}
prop_names={this.state.prop_names}
onClick={(index) => {
this.state.worker.postMessage({type: "get", packet: index})
}}/>
{active}
</div>
</>
)
} else {
return (
<>
<DemoDropzone onDrop={(data) => this.load(data)}/>
</>
)
}
}
}
import React from 'react'
import {createRoot} from "react-dom/client";
import {App} from './App'
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App/>);
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
const onDropCb = useCallback((acceptedFiles: File[]) => {
let reader = new FileReader();
reader.readAsArrayBuffer(acceptedFiles[0]);
reader.addEventListener('load', () => {
let result = reader.result as ArrayBuffer;
onDrop(result)
});
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop: onDropCb})
return (
<div className="dropzone" {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the demo file here ...</p> :
<p>Drag 'n' drop a demo here, or click to a file</p>
}
</div>
)
}
function debounce(func: Function, timeout = 300) {
let timer: any;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}

View file

@ -1,14 +1,105 @@
import {SearchFilter} from "./search";
import {Packet} from "./parser";
import {Header} from "./header";
import {PacketMeta} from "./index";
import {PacketMeta} from "./App";
import {Simulate} from "react-dom/test-utils";
import progress = Simulate.progress;
export type RequestMessageData = {type: "data", data: ArrayBuffer}
| {type: "get", packet: number}
| {type: "search", filter: SearchFilter}
export type RequestMessageData = { type: "data", sequence?: number, data: ArrayBuffer }
| { type: "get", sequence?: number, packet: number }
| { type: "search", sequence?: number, filter: SearchFilter }
export type ResponseMessageData = { type: "progress", progress: number }
| { type: "packet", packet: Packet }
| { type: "done", packets: PacketMeta[], header: Header, prop_names: { identifier: string, table: string, prop: string }[], class_names: { identifier: number, name: string }[] }
| { type: "packet_names", packet: {} }
| { type: "search_result", matches: number[] };
export type ResponseMessageData = { type: "error", sequence: number, e: Error }
|{ type: "progress", sequence: number, progress: number }
| { type: "packet", sequence: number, packet: Packet }
| { type: "done", sequence: number, packets: PacketMeta[], header: Header, prop_names: { identifier: string, table: string, prop: string }[], class_names: { identifier: number, name: string }[] }
| { type: "search_result", sequence: number, matches: number[] };
const ResponseTypeMap = {
"search": "search_result",
"get": "packet",
"data": "done",
}
export interface ParsedDemo {
packets: PacketMeta[],
header: Header,
prop_names: { identifier: string, table: string, prop: string }[],
class_names: { identifier: number, name: string }[]
}
export class DemoWorker {
worker: Worker
packets: PacketMeta[] = [];
lastSequence = 0;
callbacks: Map<number, [(_: ResponseMessageData) => void, (_: Error) => void]>;
onProgress: null | ((progress: number) => void) = null;
constructor() {
this.callbacks = new Map();
this.worker = new Worker('./worker.js');
this.worker.addEventListener("message", (event: MessageEvent<ResponseMessageData>) => {
const data = event.data;
const sequence = data.sequence;
const [resolve, reject] = this.callbacks.get(sequence);
if (data.type == "error") {
this.callbacks.delete(sequence);
reject(data.e);
} else if (data.type === "progress") {
if (this.onProgress) {
this.onProgress(data.progress);
}
} else {
this.callbacks.delete(sequence);
resolve(event.data);
}
});
}
postMessage(message: RequestMessageData, transfer: Transferable[] = []): Promise<ResponseMessageData> {
const sequence = this.lastSequence++;
message.sequence = sequence;
this.worker.postMessage(message, transfer);
return new Promise((resolve, reject) => {
this.callbacks.set(sequence, [resolve, reject]);
});
}
public async load(data: ArrayBuffer, onProgress: (progress: number) => void): Promise<ParsedDemo> {
this.onProgress = onProgress;
const response = await this.postMessage({
type: "data",
data,
}, [data]);
this.onProgress = null;
if (response.type === "done") {
return response;
} else {
throw new Error("Invalid response type");
}
}
public async get(packet: number): Promise<Packet> {
const response = await this.postMessage({
type: "get",
packet,
});
if (response.type === "packet") {
return response.packet;
} else {
throw new Error("Invalid response type");
}
}
public async search(filter: SearchFilter): Promise<number[]> {
const response = await this.postMessage({
type: "search",
filter,
});
if (response.type === "search_result") {
return response.matches;
} else {
throw new Error("Invalid response type");
}
}
}

View file

@ -4,7 +4,7 @@ import {FixedSizeList as List} from 'react-window';
import {MessageInfo} from "./packets/message";
import {UserCmdDetails} from "./packets/usercmd";
import {filterMessage, filterPacket, SearchFilter} from "./search";
import {PacketMeta, PacketType} from "./index"
import {PacketMeta, PacketType} from "./App"
interface TableProps {
packets: PacketMeta[],

View file

@ -7,32 +7,34 @@ let parser: Parser | null = null;
onmessage = function (event: MessageEvent<RequestMessageData>) {
const data = event.data;
const sequence = data.sequence;
switch (data.type) {
case "data":
import("demo-inspector")
.then(({Parser}) => {
parser = new Parser(new Uint8Array(data.data), (progress: number) => {
postMessage({type: "progress", progress})
postMessage({type: "progress", progress, sequence})
});
postMessage({
type: "done",
packets: parser.packets(),
header: parser.header(),
prop_names: parser.prop_names(),
class_names: parser.class_names()
class_names: parser.class_names(),
sequence
})
})
break;
case "search":
if (parser) {
const matches = parser.search(data.filter);
postMessage({type: "search_result", matches: Array.prototype.slice.call(matches)})
postMessage({type: "search_result", matches: Array.prototype.slice.call(matches), sequence})
}
break;
case "get":
if (parser) {
const packet = parser.packet(data.packet);
postMessage({type: "packet", packet})
postMessage({type: "packet", packet, sequence})
}
break;
}