Files
ic10emu/www/src/ts/session.ts

312 lines
7.0 KiB
TypeScript

const demoCode = `# Highlighting Demo
# This is a comment
# Hover a define id anywhere to see it's definition
define a_def 10
# Hover HASH("String")'s to see computed crc32
# hover here vvvvvvvvvvvvvvvv
define a_hash HASH("This is a String")
# hover over an alias anywhere in the code
# to see it's definition
alias a_var r0
alias a_device d0
# instructions have Auto Completion,
# numeric logic types are identified on hover
s db 12 0
# ^^
# hover here
# Enums and their values are Known, Hover them!
# vvvvvvvvvvvvvvvvvv
move r2 LogicType.Temperature
push r2
# same with constants
# vvvv
move r3 pinf
# Labels are known
main:
l r1 dr15 RatioWater
move r2 100000.001
push r2
# Hover Hash Strings of Known prefab names
# to get their documentation
# vvvvvvvvvvvvvvv
move r0 HASH("AccessCardBlack")
push r0
beqzal r1 test
# -2045627372 is the crc32 hash of a SolarPanel,
# hover it to see the documentation!
# vvvvvvvvvv
move r1 -2045627372
jal test
move r1 $FF
push r1
beqzal 0 test
move r1 %1000
push r1
yield
j main
test:
add r15 r15 1
j ra
`;
import type { ICError } from "ic10emu_wasm";
export class Session extends EventTarget {
_programs: Map<number, string>;
_errors: Map<number, ICError[]>;
_activeIC: number;
_activeLines: Map<number, number>;
_activeLine: number;
_save_timeout?: ReturnType<typeof setTimeout>;
constructor() {
super();
this._programs = new Map();
this._errors = new Map();
this._save_timeout = undefined;
this._activeIC = 0;
this._activeLines = new Map();
this.loadFromFragment();
const that = this;
window.addEventListener("hashchange", (_event) => {
that.loadFromFragment();
});
}
get programs() {
return this._programs;
}
set programs(programs) {
this._programs = new Map([...programs]);
this._fireOnLoad();
}
get activeIC() {
return this._activeIC;
}
set activeIC(val: number) {
this._activeIC = val;
this.dispatchEvent(
new CustomEvent("session-active-ic", { detail: this.activeIC }),
);
}
onActiveIc(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-active-ic", callback);
}
get errors() {
return this._errors;
}
getActiveLine(id: number) {
return this._activeLines.get(id);
}
setActiveLine(id: number, line: number) {
const last = this._activeLines.get(id);
if (last !== line) {
this._activeLines.set(id, line);
this._fireOnActiveLine(id);
}
}
set activeLine(line: number) {
this._activeLine = line;
}
setProgramCode(id: number, code: string) {
this._programs.set(id, code);
this.save();
}
setProgramErrors(id: number, errors: ICError[]) {
this._errors.set(id, errors);
this._fireOnErrors([id]);
}
_fireOnErrors(ids: number[]) {
this.dispatchEvent(
new CustomEvent("session-errors", {
detail: ids,
}),
);
}
onErrors(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-errors", callback);
}
onLoad(callback: EventListenerOrEventListenerObject) {
this.addEventListener("session-load", callback);
}
_fireOnLoad() {
this.dispatchEvent(
new CustomEvent("session-load", {
detail: this,
}),
);
}
onActiveLine(callback: EventListenerOrEventListenerObject) {
this.addEventListener("active-line", callback);
}
_fireOnActiveLine(id: number) {
this.dispatchEvent(
new CustomEvent("active-line", {
detail: id,
}),
);
}
save() {
if (this._save_timeout) clearTimeout(this._save_timeout);
this._save_timeout = setTimeout(() => {
this.saveToFragment();
if (window.App!.vm) {
window.App!.vm.updateCode();
}
this._save_timeout = undefined;
}, 1000);
}
async saveToFragment() {
const toSave = { programs: Array.from(this._programs) };
const bytes = new TextEncoder().encode(JSON.stringify(toSave));
try {
const c_bytes = await compress(bytes);
const fragment = base64url_encode(c_bytes);
window.history.replaceState(null, "", `#${fragment}`);
} catch (e) {
console.log("Error compressing content fragment:", e);
return;
}
}
async loadFromFragment() {
const fragment = window.location.hash.slice(1);
if (fragment === "demo") {
this._programs = new Map([[0, demoCode]]);
this._fireOnLoad();
return;
}
if (fragment.length > 0) {
const c_bytes = base64url_decode(fragment);
const bytes = await decompressFragment(c_bytes);
if (bytes !== null) {
const txt = new TextDecoder().decode(bytes);
const data = getJson(txt);
if (data === null) {
// backwards compatible
this._programs = new Map([[0, txt]]);
this, this._fireOnLoad();
return;
}
try {
this._programs = new Map(data.programs);
this._fireOnLoad();
return;
} catch (e) {
console.log("Bad session data:", e);
}
}
}
}
}
async function decompressFragment(c_bytes: ArrayBuffer) {
try {
const bytes = await decompress(c_bytes);
return bytes;
} catch (e) {
console.log("Error decompressing content fragment:", e);
return null;
}
}
function getJson(value: any) {
try {
return JSON.parse(value);
} catch (_) {
return null;
}
}
async function* streamAsyncIterator(stream: ReadableStream) {
// Get a lock on the stream
const reader = stream.getReader();
try {
while (true) {
// Read from the stream
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}
function base64url_encode(buffer: ArrayBuffer) {
return btoa(
Array.from(new Uint8Array(buffer), (b) => String.fromCharCode(b)).join(""),
)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function base64url_decode(value: string): ArrayBuffer {
const m = value.length % 4;
return Uint8Array.from(
atob(
value
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(value.length + (m === 0 ? 0 : 4 - m), "="),
),
(c) => c.charCodeAt(0),
).buffer;
}
async function concatUintArrays(arrays: Uint8Array[]) {
const blob = new Blob(arrays);
const buffer = await blob.arrayBuffer();
return new Uint8Array(buffer);
}
async function compress(bytes: ArrayBuffer) {
const s = new Blob([bytes]).stream();
const cs = s.pipeThrough(new CompressionStream("deflate-raw"));
const chunks: Uint8Array[] = [];
for await (const chunk of streamAsyncIterator(cs)) {
chunks.push(chunk);
}
return await concatUintArrays(chunks);
}
async function decompress(bytes: ArrayBuffer) {
const s = new Blob([bytes]).stream();
const ds = s.pipeThrough(new DecompressionStream("deflate-raw"));
const chunks: Uint8Array[] = [];
for await (const chunk of streamAsyncIterator(ds)) {
chunks.push(chunk);
}
return await concatUintArrays(chunks);
}