import { PlugNamespaceHook } from "./plugos/hooks/plug_namespace.ts"; import type { SilverBulletHooks } from "@silverbulletmd/silverbullet/type/manifest"; import type { EventHook } from "./plugos/hooks/event.ts"; import { WorkerSandbox } from "./plugos/sandboxes/worker_sandbox.ts"; import assetSyscalls from "./plugos/syscalls/asset.ts"; import { System } from "./plugos/system.ts"; import type { Client } from "./client.ts"; import { CodeWidgetHook } from "./plugos/hooks/code_widget.ts"; import { CommandHook } from "./plugos/hooks/command.ts"; import { SlashCommandHook } from "./plugos/hooks/slash_command.ts"; import { SyscallHook } from "./plugos/hooks/syscall.ts"; import { clientStoreSyscalls } from "./plugos/syscalls/clientStore.ts"; import { editorSyscalls } from "./plugos/syscalls/editor.ts"; import { sandboxFetchSyscalls } from "./plugos/syscalls/fetch.ts"; import { markdownSyscalls } from "./plugos/syscalls/markdown.ts"; import { shellSyscalls } from "./plugos/syscalls/shell.ts"; import { spaceReadSyscalls, spaceWriteSyscalls, } from "./plugos/syscalls/space.ts"; import { syncSyscalls } from "./plugos/syscalls/sync.ts"; import { systemSyscalls } from "./plugos/syscalls/system.ts"; import type { Space } from "./space.ts"; import { MQHook } from "./plugos/hooks/mq.ts"; import { mqSyscalls } from "./plugos/syscalls/mq.ts"; import { dataStoreReadSyscalls, dataStoreWriteSyscalls, } from "./plugos/syscalls/datastore.ts"; import type { DataStore } from "./data/datastore.ts"; import { languageSyscalls } from "./plugos/syscalls/language.ts"; import { codeWidgetSyscalls } from "./plugos/syscalls/code_widget.ts"; import { clientCodeWidgetSyscalls } from "./plugos/syscalls/client_code_widget.ts"; import { KVPrimitivesManifestCache } from "./plugos/manifest_cache.ts"; import { createCommandKeyBindings } from "./codemirror/editor_state.ts"; import type { DataStoreMQ } from "./data/mq.datastore.ts"; import { jsonschemaSyscalls } from "./plugos/syscalls/jsonschema.ts"; import { luaSyscalls } from "./plugos/syscalls/lua.ts"; import { indexSyscalls } from "./plugos/syscalls/index.ts"; import { configSyscalls } from "./plugos/syscalls/config.ts"; import { eventSyscalls } from "./plugos/syscalls/event.ts"; import { DocumentEditorHook } from "./plugos/hooks/document_editor.ts"; import type { Command } from "./types/command.ts"; import { SpaceLuaEnvironment } from "./space_lua.ts"; import { builtinPlugPaths } from "../plugs/builtin_plugs.ts"; import { registerEditorCommands } from "./editor_commands.ts"; import { ServiceRegistry } from "./service_registry.ts"; import { serviceRegistrySyscalls } from "./plugos/syscalls/service_registry.ts"; import type { ObjectIndex } from "./data/object_index.ts"; const mqTimeout = 10000; // 10s const mqTimeoutRetry = 3; /** * Handles the extension-related mechanisms of the client by wrapping a PlugOS System object as well as Space Lua environments */ export class ClientSystem { // PlugOS system system!: System; // ... and hooks commandHook!: CommandHook; slashCommandHook!: SlashCommandHook; namespaceHook!: PlugNamespaceHook; codeWidgetHook!: CodeWidgetHook; documentEditorHook!: DocumentEditorHook; mqHook!: MQHook; serviceRegistry!: ServiceRegistry; // Space Lua spaceLuaEnv: SpaceLuaEnvironment; readonly scriptCommands = new Map(); scriptsLoaded: boolean = false; // Known files (for UI) readonly allKnownFiles = new Set(); public knownFilesLoaded: boolean = false; constructor( private client: Client, protected mq: DataStoreMQ, public ds: DataStore, public eventHook: EventHook, private objectIndex: ObjectIndex, public readOnlyMode: boolean, ) { this.system = new System(undefined, { manifestCache: new KVPrimitivesManifestCache( ds.kv, "manifest", ), }); this.spaceLuaEnv = new SpaceLuaEnvironment(this.system, objectIndex); this.serviceRegistry = new ServiceRegistry(this.eventHook, client.config); setInterval(() => { mq.requeueTimeouts(mqTimeout, mqTimeoutRetry, true).catch(console.error); }, 20000); // Look to requeue every 20s this.system.addHook(this.eventHook); // Plug page namespace hook this.namespaceHook = new PlugNamespaceHook(); this.system.addHook(this.namespaceHook); // Code widget hook this.codeWidgetHook = new CodeWidgetHook(); this.system.addHook(this.codeWidgetHook); // Document editor hook this.documentEditorHook = new DocumentEditorHook(); this.system.addHook(this.documentEditorHook); // Command hook this.commandHook = new CommandHook(this.readOnlyMode, this.scriptCommands); registerEditorCommands(client, this.commandHook); this.commandHook.on({ commandsUpdated: (commandMap) => { this.client.ui?.viewDispatch({ type: "update-commands", commands: commandMap, }); // Replace the key mapping compartment (keybindings) this.client.editorView.dispatch({ effects: this.client.commandKeyHandlerCompartment?.reconfigure( createCommandKeyBindings(this.client), ), }); }, }); this.slashCommandHook = new SlashCommandHook(this.client); // MQ hook this.mqHook = new MQHook(this.system, this.mq, this.client.config); this.system.addHook(this.mqHook); // Syscall hook this.system.addHook(new SyscallHook()); this.eventHook.addLocalListener("editor:reloadState", async () => { await this.reloadState(); }); } init() { // Init is called after the editor is initialized, so we can safely add the command hook this.system.addHook(this.commandHook); this.system.addHook(this.slashCommandHook); // Syscalls available to all plugs this.system.registerSyscalls( [], eventSyscalls(this.eventHook, this.client), editorSyscalls(this.client), spaceReadSyscalls(this.client), systemSyscalls(client, this.readOnlyMode), markdownSyscalls(client), assetSyscalls(this.system), codeWidgetSyscalls(this.codeWidgetHook), clientCodeWidgetSyscalls(this.client), languageSyscalls(), jsonschemaSyscalls(), indexSyscalls(this.objectIndex, this.client), //commandSyscalls(client), luaSyscalls(this), mqSyscalls(this.mq), serviceRegistrySyscalls(this.serviceRegistry), dataStoreReadSyscalls(this.ds, this), dataStoreWriteSyscalls(this.ds), syncSyscalls(this.client), clientStoreSyscalls(this.ds), configSyscalls(this.client.config), ); if (!this.readOnlyMode) { // Write syscalls this.system.registerSyscalls([], spaceWriteSyscalls(this.client)); // Syscalls that require some additional permissions this.system.registerSyscalls( ["fetch"], sandboxFetchSyscalls(this.client), ); this.system.registerSyscalls(["shell"], shellSyscalls(this.client)); } } async loadLuaScripts() { if (this.client.bootConfig.disableSpaceLua) { console.info("Space Lua scripts are disabled, skipping loading scripts"); return; } if (!(await this.objectIndex.hasFullIndexCompleted())) { console.info( "Not loading space scripts, since full indexing has not completed yet", ); return; } this.client.config.clear(); try { await this.spaceLuaEnv.reload(); } catch (e: any) { console.error("Error loading Lua script:", e.message); } // Reset the space script commands this.scriptCommands.clear(); for (const [name, command] of Object.entries( this.client.config.get>("commands", {}), )) { this.scriptCommands.set(name, command); } // Make scripted (slash) commands available this.commandHook.throttledBuildAllCommandsAndEmit(); this.slashCommandHook.throttledBuildAllCommands(); this.mqHook.throttledReloadQueues(); this.scriptsLoaded = true; } async loadPlugFromPath(path: string, lastModified: number) { await this.system.loadPlug( WorkerSandbox.forPath(path), path, lastModified, ); } async reloadPlugsFromSpace(space: Space) { console.log("(Re)loading plugs"); await this.system.unloadAll(); let allPlugs = await space.listPlugs(); // console.log("All plugs", allPlugs); if (this.client.bootConfig.disablePlugs) { // Only keep builtin plugs allPlugs = allPlugs.filter(({ name }) => builtinPlugPaths.includes(name)); console.warn("Not loading custom plugs as `disablePlugs` has been set"); } await Promise.all( allPlugs.map((fileMeta) => this.loadPlugFromPath(fileMeta.name, fileMeta.lastModified).catch( (e) => console.error( `Could not load plug ${fileMeta.name} error: ${e.message}`, ), ), ), ); } localSyscall(name: string, args: any[]) { return this.system.localSyscall(name, args); } public async reloadState() { console.log( "Now loading space scripts, custom styles and rebuilding editor state", ); await this.loadLuaScripts(); await this.client.loadCustomStyles(); this.client.rebuildEditorState(); } }