From e4d42d69a5ba72fad0d8a4c8c768de1e036d08d8 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 20 Apr 2024 01:39:10 -0700 Subject: [PATCH] persist vm session --- Cargo.lock | 8 +- Cargo.toml | 2 +- www/package.json | 3 +- www/pnpm-lock.yaml | 7 ++ www/src/ts/app/app.ts | 23 +++- www/src/ts/app/index.ts | 1 + www/src/ts/app/nav.ts | 4 + www/src/ts/app/save.ts | 114 ++++++++++++++++++++ www/src/ts/editor/index.ts | 17 +-- www/src/ts/main.ts | 4 +- www/src/ts/session.ts | 126 ++++++++++++++++------ www/src/ts/utils.ts | 8 +- www/src/ts/virtual_machine/base_device.ts | 32 +++--- www/src/ts/virtual_machine/device.ts | 21 ++-- www/src/ts/virtual_machine/index.ts | 18 ++-- www/webpack.config.js | 9 ++ www/webpack.config.prod.js | 9 ++ 17 files changed, 321 insertions(+), 85 deletions(-) create mode 100644 www/src/ts/app/save.ts diff --git a/Cargo.lock b/Cargo.lock index 79c415b..14fe0ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,7 +560,7 @@ dependencies = [ [[package]] name = "ic10emu" -version = "0.1.0" +version = "0.2.0" dependencies = [ "const-crc32", "convert_case", @@ -580,7 +580,7 @@ dependencies = [ [[package]] name = "ic10emu_wasm" -version = "0.1.0" +version = "0.2.0" dependencies = [ "console_error_panic_hook", "ic10emu", @@ -617,7 +617,7 @@ dependencies = [ [[package]] name = "ic10lsp_wasm" -version = "0.1.0" +version = "0.2.0" dependencies = [ "console_error_panic_hook", "futures", @@ -1844,7 +1844,7 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "xtask" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index c0d0a8c..42e22c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["ic10lsp_wasm", "ic10emu_wasm", "ic10emu", "xtask"] resolver = "2" [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2021" [profile.release] diff --git a/www/package.json b/www/package.json index 41a1b26..1f4403e 100644 --- a/www/package.json +++ b/www/package.json @@ -1,6 +1,6 @@ { "name": "ic10emu", - "version": "0.1.0", + "version": "0.2.0", "description": "an IC10 emulator for IC10 mips from Stationeers", "main": "index.js", "scripts": { @@ -72,6 +72,7 @@ "crypto-browserify": "^3.12.0", "ic10emu_wasm": "file:../ic10emu_wasm/pkg", "ic10lsp_wasm": "file:../ic10lsp_wasm/pkg", + "idb": "^8.0.0", "jquery": "^3.7.1", "lit": "^3.1.3", "lzma-web": "^3.0.1", diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index 70f45d4..47c5f19 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: ic10lsp_wasm: specifier: file:../ic10lsp_wasm/pkg version: file:../ic10lsp_wasm/pkg + idb: + specifier: ^8.0.0 + version: 8.0.0 jquery: specifier: ^3.7.1 version: 3.7.1 @@ -4012,6 +4015,10 @@ packages: postcss: 8.4.38 dev: true + /idb@8.0.0: + resolution: {integrity: sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} diff --git a/www/src/ts/app/app.ts b/www/src/ts/app/app.ts index 94c610d..0f35f58 100644 --- a/www/src/ts/app/app.ts +++ b/www/src/ts/app/app.ts @@ -19,6 +19,15 @@ import { VirtualMachine } from "../virtual_machine"; import { openFile, saveFile } from "../utils"; import "../virtual_machine/ui"; +import "./save"; +import { SaveDialog } from "./save"; + +declare global { + const __COMMIT_HASH__: string; + const __BUILD_DATE__: string; +} + +import packageJson from "../../../package.json" @customElement("ic10emu-app") export class App extends BaseElement { @@ -43,10 +52,15 @@ export class App extends BaseElement { `, ]; + version = packageJson.version; + gitVer = __COMMIT_HASH__; + buildDate = __BUILD_DATE__; + editorSettings: { fontSize: number; relativeLineNumbers: boolean }; - @query("ace-ic10") accessor editor: IC10Editor; - @query("session-share-dialog") accessor shareDialog: ShareSessionDialog; + @query("ace-ic10") editor: IC10Editor; + @query("session-share-dialog") shareDialog: ShareSessionDialog; + @query("save-dialog") saveDialog: SaveDialog; // get editor() { // return this.renderRoot.querySelector("ace-ic10") as IC10Editor; @@ -67,6 +81,7 @@ export class App extends BaseElement { root.addEventListener("app-share-session", this._handleShare.bind(this)); root.addEventListener("app-open-file", this._handleOpenFile.bind(this)); root.addEventListener("app-save-as", this._handleSaveAs.bind(this)); + root.addEventListener("app-save", this._handleSave.bind(this)); return root; } @@ -86,6 +101,7 @@ export class App extends BaseElement { + `; } @@ -102,6 +118,9 @@ export class App extends BaseElement { saveFile(window.Editor.editorValue); } + _handleSave(_e: Event) { + this.saveDialog.show("save"); + } _handleOpenFile(_e: Event) { openFile(window.Editor.editor); } diff --git a/www/src/ts/app/index.ts b/www/src/ts/app/index.ts index fa6f33c..ec08af3 100644 --- a/www/src/ts/app/index.ts +++ b/www/src/ts/app/index.ts @@ -1,5 +1,6 @@ import { App } from "./app"; import { Nav } from "./nav"; +import { SaveDialog } from "./save"; import { ShareSessionDialog } from "./share"; import "./icons"; export { App, Nav, ShareSessionDialog } diff --git a/www/src/ts/app/nav.ts b/www/src/ts/app/nav.ts index 1fbe901..9d83572 100644 --- a/www/src/ts/app/nav.ts +++ b/www/src/ts/app/nav.ts @@ -107,6 +107,7 @@ export class Nav extends BaseElement { Share Open File + Save Save As app.session.addEventListener("sessions-local-update", this._handleSessionsUpdate.bind(this))); + this.loadsaves(); + } + + _handleSessionsUpdate() { + this.loadsaves(); + } + + loadsaves() { + window.App.get().then(async (app) => { + const saves = await app.session.getLocalSaved(); + this.saves = saves; + }); + } + + @query("sl-dialog") dialog: SlDialog; + + show(mode: SaveDialogMode) { + this.mode = mode; + this.dialog.show(); + } + + hide() { + this.dialog.hide(); + } + + render() { + return html` + + ${when(this.mode === "save", + () => html` +
+ + Save +
+ ` + )} + + + + + + + + + + ${when( + typeof this.saves !== "undefined", + () => repeat(this.saves, (save) => save.name, (save) => { + const size = JSON.stringify(save.session).length + return html` + + + + + + `; + }, + ) + )} +
NameDateSize
${save.name} + + +
+
+ `; + } + + @query(".save-name-input") saveInput: SlInput; + + async _handleSaveClick(_e: CustomEvent) { + const name = this.saveInput.value; + const app = await window.App.get(); + console.log(app); + await app.session.saveLocal(name); + } + + _handleSearchInput() { + + } +} diff --git a/www/src/ts/editor/index.ts b/www/src/ts/editor/index.ts index 1354bb0..65dd5b9 100644 --- a/www/src/ts/editor/index.ts +++ b/www/src/ts/editor/index.ts @@ -274,11 +274,11 @@ export class IC10Editor extends BaseElement { const app = await window.App.get(); app.session.onLoad(((e: CustomEvent) => { - const session = e.detail; + const session = app.session; const updated_ids: number[] = []; - for (const [id, _] of session.programs) { + for (const [id, code] of session.programs) { updated_ids.push(id); - that.createOrSetSession(id, session.programs.get(id)); + that.createOrSetSession(id, code); } that.activateSession(that.active_session); for (const [id, _] of that.sessions) { @@ -484,18 +484,19 @@ export class IC10Editor extends BaseElement { } } - createOrSetSession(session_id: number, content: any) { + createOrSetSession(session_id: number, content: string) { if (!this.sessions.has(session_id)) { - this.newSession(session_id); + this.newSession(session_id, content); + } else { + this.sessions.get(session_id).setValue(content); } - this.sessions.get(session_id)?.setValue(content); } - newSession(session_id: number) { + newSession(session_id: number, content?: string) { if (this.sessions.has(session_id)) { return false; } - const session = ace.createEditSession("", this.mode as any); + const session = ace.createEditSession(content ?? "", this.mode as any); session.setOptions({ firstLineNumber: 0, }); diff --git a/www/src/ts/main.ts b/www/src/ts/main.ts index c68a0d3..a19527f 100644 --- a/www/src/ts/main.ts +++ b/www/src/ts/main.ts @@ -15,7 +15,7 @@ class DeferedApp { get(): Promise { const that = this; return new Promise(resolve => { - if (typeof that.app !== "undefined") { + if (typeof that.app === "undefined") { that.resolvers.push(resolve); } else { resolve(that.app); @@ -45,7 +45,7 @@ class DeferedVM { get(): Promise { const that = this; return new Promise(resolve => { - if (typeof that.vm !== "undefined") { + if (typeof that.vm === "undefined") { that.resolvers.push(resolve); } else { resolve(that.vm); diff --git a/www/src/ts/session.ts b/www/src/ts/session.ts index 778cd09..4241021 100644 --- a/www/src/ts/session.ts +++ b/www/src/ts/session.ts @@ -63,6 +63,11 @@ j ra import type { ICError, FrozenVM } from "ic10emu_wasm"; import { App } from "./app"; +import { openDB, DBSchema } from 'idb'; +import { fromJson, toJson } from "./utils"; + +const LOCAL_DB_VERSION = 1; + export class Session extends EventTarget { private _programs: Map; private _errors: Map; @@ -179,17 +184,17 @@ export class Session extends EventTarget { save() { if (this._save_timeout) clearTimeout(this._save_timeout); this._save_timeout = setTimeout(() => { - this.saveToFragment(); if (this.app.vm) { this.app.vm.updateCode(); } + this.saveToFragment(); this._save_timeout = undefined; }, 1000); } async saveToFragment() { - const toSave = { vmState: this.app.vm.saveVMState(), activeIC: this.activeIC }; - const bytes = new TextEncoder().encode(JSON.stringify(toSave)); + const toSave = { vm: this.app.vm.saveVMState(), activeIC: this.activeIC }; + const bytes = new TextEncoder().encode(toJson(toSave)); try { const c_bytes = await compress(bytes, defaultCompression); const fragment = base64url_encode(c_bytes); @@ -200,11 +205,29 @@ export class Session extends EventTarget { } } + async load(data: VMState | OldPrograms | string) { + if (typeof data === "string") { + this._programs = new Map([[1, data]]); + } else if ( "programs" in data) { + this._programs = new Map(data.programs); + } else if ( "vm" in data ) { + this._programs = new Map(); + const state = data.vm; + // assign first so it's present when the + // vm fires events + this._activeIC = data.activeIC; + this.app.vm.restoreVMState(state); + this.programs = this.app.vm.getPrograms(); + // assign again to fire event + this.activeIC = data.activeIC; + } + this._fireOnLoad(); + } + async loadFromFragment() { const fragment = window.location.hash.slice(1); if (fragment === "demo") { - this._programs = new Map([[1, demoCode]]); - this._fireOnLoad(); + this.load(demoCode); return; } if (fragment.length > 0) { @@ -215,39 +238,78 @@ export class Session extends EventTarget { const data = getJson(txt); if (data === null) { // backwards compatible - this._programs = new Map([[1, txt]]); - this, this._fireOnLoad(); + this.load(txt); return; } else if ("programs" in data) { - try { - this._programs = new Map(data.programs); - this._fireOnLoad(); - return; - } catch (e) { - console.log("Bad session data:", e); - } - } else if ("vmState" in data && "activeIC" in data) { - try { - this._programs = new Map(); - const state = data.vmState as FrozenVM; - // assign first so it's present when the - // vm setting the programs list fires events - this._activeIC = data.activeIC; - this.app.vm.restoreVMState(state); - this.programs = this.app.vm.getPrograms(); - // assign again to fire event - this.activeIC = data.activeIC; - this._fireOnLoad(); - return; - } catch (e) { - console.log("Bad session data:", e); - } + this.load(data as OldPrograms); + return; + } else if ("vm" in data && "activeIC" in data) { + this.load(data as VMState); } else { - console.log("Bad session data:", data); + console.log("Bad session data:", data); } } } } + + async openIndexDB() { + return await openDB("ic10-vm-sessions", LOCAL_DB_VERSION, { + upgrade(db, oldVersion, newVersion, transaction, event) { + // only db verison currently known is v1 + if (oldVersion < 1) { + const sessionStore = db.createObjectStore('sessions'); + sessionStore.createIndex('by-date', 'date'); + sessionStore.createIndex('by-name', 'name'); + } + }, + }); + } + + async saveLocal(name: string) { + const state: VMState = { + vm: (await window.VM.get()).ic10vm.saveVMState(), + activeIC: this.activeIC, + }; + const db = await this.openIndexDB(); + const transaction = db.transaction(['sessions'], "readwrite"); + const sessionStore = transaction.objectStore("sessions"); + sessionStore.put({ + name, + date: new Date(), + session: state, + }, name); + this.dispatchEvent(new CustomEvent("sessions-local-update")) + } + + async getLocalSaved() { + const db = await this.openIndexDB(); + const sessions = await db.getAll('sessions'); + return sessions; + } +} + +export interface VMState { + activeIC: number; + vm: FrozenVM; +} + +interface AppDBSchemaV1 extends DBSchema { + sessions: { + key: string; + value: { + name: string; + date: Date; + session: VMState; + } + indexes: { + 'by-date': Date; + 'by-name': string; + }; + } +} + +export interface OldPrograms { + programs: [number, string][] } const byteToHex: string[] = []; @@ -298,7 +360,7 @@ async function decompressFragment(c_bytes: ArrayBuffer) { function getJson(value: any) { try { - return JSON.parse(value); + return fromJson(value); } catch (_) { return null; } diff --git a/www/src/ts/utils.ts b/www/src/ts/utils.ts index 35ecf55..32133a7 100644 --- a/www/src/ts/utils.ts +++ b/www/src/ts/utils.ts @@ -23,7 +23,11 @@ function replacer(key: any, value: any) { return { dataType: 'Number', value: "NaN", - } + }; + } else if (typeof value === "undefined" ) { + return { + dataType: 'undefined', + }; } else { return value; } @@ -35,6 +39,8 @@ function reviver(_key: any, value: any) { return new Map(value.value); } else if (value.dataType === 'Number') { return parseFloat(value.value) + } else if (value.dataType === 'undefined') { + return undefined; } } return value; diff --git a/www/src/ts/virtual_machine/base_device.ts b/www/src/ts/virtual_machine/base_device.ts index 86a13d9..ed00f00 100644 --- a/www/src/ts/virtual_machine/base_device.ts +++ b/www/src/ts/virtual_machine/base_device.ts @@ -60,22 +60,22 @@ export const VMDeviceMixin = >( device: DeviceRef; - @state() accessor name: string | null = null; - @state() accessor nameHash: number | null = null; - @state() accessor prefabName: string | null; - @state() accessor fields: LogicFields; - @state() accessor slots: Slot[]; - @state() accessor reagents: Reagents; - @state() accessor connections: Connection[]; - @state() accessor icIP: number; - @state() accessor icOpCount: number; - @state() accessor icState: string; - @state() accessor errors: ICError[]; - @state() accessor registers: Registers | null; - @state() accessor stack: Stack | null; - @state() accessor aliases: Aliases | null; - @state() accessor defines: Defines | null; - @state() accessor pins: Pins | null; + @state() name: string | null = null; + @state() nameHash: number | null = null; + @state() prefabName: string | null; + @state() fields: LogicFields; + @state() slots: Slot[]; + @state() reagents: Reagents; + @state() connections: Connection[]; + @state() icIP: number; + @state() icOpCount: number; + @state() icState: string; + @state() errors: ICError[]; + @state() registers: Registers | null; + @state() stack: Stack | null; + @state() aliases: Aliases | null; + @state() defines: Defines | null; + @state() pins: Pins | null; connectedCallback(): void { const root = super.connectedCallback(); diff --git a/www/src/ts/virtual_machine/device.ts b/www/src/ts/virtual_machine/device.ts index 15e1666..ec48977 100644 --- a/www/src/ts/virtual_machine/device.ts +++ b/www/src/ts/virtual_machine/device.ts @@ -46,6 +46,7 @@ import { DeviceDB, DeviceDBEntry } from "./device_db"; import { connectionFromDeviceDBConnection } from "./utils"; import { SlDialog } from "@shoelace-style/shoelace"; import { repeat } from "lit/directives/repeat.js"; +import { cache } from "lit/directives/cache.js"; @customElement("vm-device-card") export class VMDeviceCard extends VMDeviceMixin(BaseElement) { @@ -221,7 +222,8 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) { class="device-name" size="small" pill - placeholder="${this.prefabName}" + placeholder=${this.prefabName} + value=${this.name} @sl-change=${this._handleChangeName} > Name @@ -814,18 +816,20 @@ export class VMAddDeviceButton extends BaseElement { this.deviceDB = e.detail; } - renderSearchResults(): HTMLTemplateResult { - const renderedResults: HTMLTemplateResult[] = this._searchResults?.map( - (result) => html` + renderSearchResults() { + return repeat( + this._searchResults ?? [], + (result) => result.name, + (result) => cache(html` - `, + `) ); - return html`${renderedResults}`; + } _handleDeviceAdd() { @@ -851,7 +855,7 @@ export class VMAddDeviceButton extends BaseElement { @sl-input=${this._handleSearchInput} > Search Structures - " +
${this.renderSearchResults()}
(that.deviceDB = vm.db)); + this.deviceDB = window.VM.vm.db; } get deviceDB(): DeviceDB { diff --git a/www/src/ts/virtual_machine/index.ts b/www/src/ts/virtual_machine/index.ts index 6d2973a..6390850 100644 --- a/www/src/ts/virtual_machine/index.ts +++ b/www/src/ts/virtual_machine/index.ts @@ -127,6 +127,7 @@ class VirtualMachine extends EventTarget { detail: ids, }), ); + this.app.session.save(); } } @@ -152,7 +153,7 @@ class VirtualMachine extends EventTarget { } } } - this.update(); + this.update(false); } step() { @@ -193,7 +194,7 @@ class VirtualMachine extends EventTarget { } } - update() { + update(save: boolean = true) { this.updateDevices(); this.ic10vm.lastOperationModified.forEach((id, _index, _modifiedIds) => { if (this.devices.has(id)) { @@ -202,16 +203,18 @@ class VirtualMachine extends EventTarget { ); } }, this); - this.updateDevice(this.activeIC); + this.updateDevice(this.activeIC, save); + if (save) this.app.session.save(); } - updateDevice(device: DeviceRef) { + updateDevice(device: DeviceRef, save: boolean = true) { this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: device.id }), ); if (typeof device.ic !== "undefined") { this.app.session.setActiveLine(device.id, device.ip!); } + if (save) this.app.session.save(); } handleVmError(err: Error) { @@ -272,6 +275,7 @@ class VirtualMachine extends EventTarget { this.dispatchEvent( new CustomEvent("vm-device-modified", { detail: id }), ); + this.app.session.save(); return true; } catch (e) { this.handleVmError(e); @@ -372,6 +376,7 @@ class VirtualMachine extends EventTarget { detail: Array.from(device_ids), }), ); + this.app.session.save(); return true; } catch (err) { this.handleVmError(err); @@ -413,9 +418,4 @@ class VirtualMachine extends EventTarget { } } -export interface VMState { - activeIC: number; - vm: FrozenVM; -} - export { VirtualMachine }; diff --git a/www/webpack.config.js b/www/webpack.config.js index 7d7e541..2836fd0 100644 --- a/www/webpack.config.js +++ b/www/webpack.config.js @@ -3,8 +3,13 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"); const miniCssExtractPlugin = require("mini-css-extract-plugin"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); const { SourceMap } = require("module"); +const webpack = require("webpack"); const path = require("path"); +const commitHash = require('child_process') + .execSync('git rev-parse --short HEAD') + .toString() + .trim(); module.exports = { entry: "./src/ts/main.ts", @@ -39,6 +44,10 @@ module.exports = { }), new HtmlWebpackPlugin({ template: "./src/index.html" }), new miniCssExtractPlugin(), + new webpack.DefinePlugin({ + __COMMIT_HASH__: JSON.stringify(commitHash), + __BUILD_DATE__: JSON.stringify(new Date), + }), ], module: { rules: [ diff --git a/www/webpack.config.prod.js b/www/webpack.config.prod.js index 114ef8e..775ccfc 100644 --- a/www/webpack.config.prod.js +++ b/www/webpack.config.prod.js @@ -5,8 +5,13 @@ const miniCssExtractPlugin = require("mini-css-extract-plugin"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); const { EsbuildPlugin } = require("esbuild-loader"); +const webpack = require("webpack"); const path = require("path"); +const commitHash = require('child_process') + .execSync('git rev-parse --short HEAD') + .toString() + .trim(); module.exports = { entry: "./src/ts/main.ts", @@ -36,6 +41,10 @@ module.exports = { }), new HtmlWebpackPlugin({ template: "./src/index.html" }), new miniCssExtractPlugin(), + new webpack.DefinePlugin({ + __COMMIT_HASH__: JSON.stringify(commitHash), + __BUILD_DATE__: JSON.stringify(new Date), + }), ], module: { rules: [