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; _errors: Map; _activeIC: number; _activeLines: Map; _activeLine: number; _save_timeout?: ReturnType; 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); }