import { PageMeta } from "./types"; import { Socket } from "socket.io-client"; import { Update } from "@codemirror/collab"; import { ChangeSet, Text, Transaction } from "@codemirror/state"; import { CollabDocument, CollabEvents } from "./collab"; import { cursorEffect } from "./cursorEffect"; import { EventEmitter } from "../common/event"; import { Manifest } from "../common/manifest"; import { SystemJSON } from "../plugos/system"; export type SpaceEvents = { connect: () => void; pageCreated: (meta: PageMeta) => void; pageChanged: (meta: PageMeta) => void; pageDeleted: (name: string) => void; pageListUpdated: (pages: Set) => void; loadSystem: (systemJSON: SystemJSON) => void; plugLoaded: (plugName: string, plug: Manifest) => void; plugUnloaded: (plugName: string) => void; } & CollabEvents; export type KV = { key: string; value: any; }; export class Space extends EventEmitter { socket: Socket; reqId = 0; allPages = new Set(); constructor(socket: Socket) { super(); this.socket = socket; [ "connect", "cursorSnapshot", "pageCreated", "pageChanged", "pageDeleted", "loadSystem", "plugLoaded", "plugUnloaded", ].forEach((eventName) => { socket.on(eventName, (...args) => { this.emit(eventName as keyof SpaceEvents, ...args); }); }); this.wsCall("page.listPages").then((pages) => { this.allPages = new Set(pages); this.emit("pageListUpdated", this.allPages); }); this.on({ pageCreated: (meta) => { // Cannot reply on equivalence in set, need to iterate over all pages let found = false; for (const page of this.allPages) { if (page.name === meta.name) { found = true; break; } } if (!found) { this.allPages.add(meta); console.log("New page created", meta); this.emit("pageListUpdated", this.allPages); } }, pageDeleted: (name) => { console.log("Page delete", name); this.allPages.forEach((meta) => { if (name === meta.name) { this.allPages.delete(meta); } }); this.emit("pageListUpdated", this.allPages); }, }); } openRequests = new Map(); public wsCall(eventName: string, ...args: any[]): Promise { return new Promise((resolve, reject) => { this.reqId++; const reqId = this.reqId; this.openRequests.set(reqId, eventName); this.socket!.once(`${eventName}Resp${reqId}`, (err, result) => { this.openRequests.delete(reqId); if (err) { reject(new Error(err)); } else { resolve(result); } }); this.socket!.emit(eventName, reqId, ...args); }); } async pushUpdates( pageName: string, version: number, fullUpdates: readonly (Update & { origin: Transaction })[] ): Promise { if (this.socket) { let updates = fullUpdates.map((u) => ({ clientID: u.clientID, changes: u.changes.toJSON(), cursors: u.effects?.map((e) => e.value), })); return this.wsCall("page.pushUpdates", pageName, version, updates); } return false; } async pullUpdates( pageName: string, version: number ): Promise { let updates: Update[] = await this.wsCall( "page.pullUpdates", pageName, version ); return updates.map((u) => ({ changes: ChangeSet.fromJSON(u.changes), effects: u.effects?.map((e) => cursorEffect.of(e.value)), clientID: u.clientID, })); } async listPages(): Promise { return Array.from(this.allPages); } async openPage(name: string): Promise { this.reqId++; let pageJSON = await this.wsCall("page.openPage", name); return new CollabDocument( Text.of(pageJSON.text), pageJSON.version, new Map(Object.entries(pageJSON.cursors)) ); } async closePage(name: string): Promise { this.socket.emit("page.closePage", name); } async readPage(name: string): Promise<{ text: string; meta: PageMeta }> { return this.wsCall("page.readPage", name); } async writePage(name: string, text: string): Promise { return this.wsCall("page.writePage", name, text); } async deletePage(name: string): Promise { return this.wsCall("page.deletePage", name); } async getPageMeta(name: string): Promise { return this.wsCall("page.getPageMeta", name); } }