Files
plainleaf/web/client_system.ts
T
MrMugameandGitHub ad39158cd1 Separate plug name and file name (#1541)
* Separate plug name from file path

Plug name and plug path were very intertwined in the code. This could
cause problems, if the file path and plug name didn't align.
The plug manager also modified the plug path for some plugs, which could
be even more confusing.

* Code improvements for the plug manager

* Be more rigorous about failure

* Also allow `silverbullet-name` case again
2025-09-15 08:54:55 +02:00

389 lines
12 KiB
TypeScript

import { PlugNamespaceHook } from "./hooks/plug_namespace.ts";
import type { SilverBulletHooks } from "../lib/manifest.ts";
import type { EventHook } from "./hooks/event.ts";
import { createWorkerSandboxFromLocalPath } from "../lib/plugos/sandboxes/web_worker_sandbox.ts";
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
import { eventSyscalls } from "../lib/plugos/syscalls/event.ts";
import { System } from "../lib/plugos/system.ts";
import type { Client } from "./client.ts";
import { CodeWidgetHook } from "./hooks/code_widget.ts";
import { CommandHook } from "./hooks/command.ts";
import { SlashCommandHook } from "./hooks/slash_command.ts";
import { SyscallHook } from "./hooks/syscall.ts";
import { clientStoreSyscalls } from "./syscalls/clientStore.ts";
import { editorSyscalls } from "./syscalls/editor.ts";
import { sandboxFetchSyscalls } from "./syscalls/fetch.ts";
import { markdownSyscalls } from "./syscalls/markdown.ts";
import { shellSyscalls } from "./syscalls/shell.ts";
import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts";
import { syncSyscalls } from "./syscalls/sync.ts";
import { systemSyscalls } from "./syscalls/system.ts";
import { yamlSyscalls } from "./syscalls/yaml.ts";
import type { Space } from "./space.ts";
import { MQHook } from "../lib/plugos/hooks/mq.ts";
import { mqSyscalls } from "../lib/plugos/syscalls/mq.ts";
import {
dataStoreReadSyscalls,
dataStoreWriteSyscalls,
} from "./syscalls/datastore.ts";
import type { DataStore } from "../lib/data/datastore.ts";
import { languageSyscalls } from "./syscalls/language.ts";
import { codeWidgetSyscalls } from "./syscalls/code_widget.ts";
import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts";
import { KVPrimitivesManifestCache } from "../lib/plugos/manifest_cache.ts";
import { createKeyBindings } from "./editor_state.ts";
import type { DataStoreMQ } from "../lib/data/mq.datastore.ts";
import { plugPrefix } from "../lib/spaces/constants.ts";
import { jsonschemaSyscalls } from "./syscalls/jsonschema.ts";
import { luaSyscalls } from "./syscalls/lua.ts";
import { indexSyscalls } from "./syscalls/index.ts";
import { configSyscalls } from "./syscalls/config.ts";
import { eventListenerSyscalls } from "./syscalls/event.ts";
import { DocumentEditorHook } from "./hooks/document_editor.ts";
import type { LuaCollectionQuery } from "../lib/space_lua/query_collection.ts";
import type { Command } from "../type/command.ts";
import { SpaceLuaEnvironment } from "./space_lua.ts";
import {
type ILuaFunction,
jsToLuaValue,
luaCall,
LuaStackFrame,
type LuaValue,
luaValueToJS,
} from "../lib/space_lua/runtime.ts";
import { buildThreadLocalEnv, handleLuaError } from "./space_lua_api.ts";
import { builtinPlugPaths } from "../plugs/builtin_plugs.ts";
const indexVersionKey = ["$indexVersion"];
const indexQueuedKey = ["$indexQueued"];
// Bump this one every time a full reindex is needed
const desiredIndexVersion = 8;
const mqTimeout = 10000; // 10s
const mqTimeoutRetry = 3;
/**
* Wrapper around a System
*/
export class ClientSystem {
system!: System<SilverBulletHooks>;
// Hooks
commandHook!: CommandHook;
slashCommandHook!: SlashCommandHook;
namespaceHook!: PlugNamespaceHook;
codeWidgetHook!: CodeWidgetHook;
documentEditorHook!: DocumentEditorHook;
// Known files (for UI)
readonly allKnownFiles = new Set<string>();
public knownFilesLoaded: boolean = false;
readonly scriptCommands = new Map<string, Command>();
spaceLuaEnv: SpaceLuaEnvironment;
scriptsLoaded: boolean = false;
constructor(
private client: Client,
protected mq: DataStoreMQ,
public ds: DataStore,
public eventHook: EventHook,
public readOnlyMode: boolean,
) {
this.system = new System(undefined, {
manifestCache: new KVPrimitivesManifestCache<SilverBulletHooks>(
ds.kv,
"manifest",
),
});
this.spaceLuaEnv = new SpaceLuaEnvironment(this.system);
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,
);
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.keyHandlerCompartment?.reconfigure(
createKeyBindings(this.client),
),
});
},
});
this.slashCommandHook = new SlashCommandHook(this.client);
// MQ hook
this.system.addHook(new MQHook(this.system, this.mq));
// Syscall hook
this.system.addHook(new SyscallHook());
this.eventHook.addLocalListener(
"file:changed",
async (path: string, _oldHash, newHash) => {
// Plug (re)loading
if (
!path.startsWith(plugPrefix) ||
!path.endsWith(".plug.js") ||
this.client.bootConfig.disablePlugs
) {
return;
}
console.log("Plug updated, reloading", path);
await this.system.loadPlug(
createWorkerSandboxFromLocalPath(path),
path,
newHash,
).catch((e) => {
console.error(
`Could not load plug ${path} error: ${e.message}`,
);
});
},
);
}
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),
eventListenerSyscalls(this.client),
editorSyscalls(this.client),
spaceReadSyscalls(this.client),
systemSyscalls(client, false),
markdownSyscalls(client),
assetSyscalls(this.system),
yamlSyscalls(),
codeWidgetSyscalls(this.codeWidgetHook),
clientCodeWidgetSyscalls(),
languageSyscalls(),
jsonschemaSyscalls(),
indexSyscalls(client),
//commandSyscalls(client),
luaSyscalls(this),
mqSyscalls(this.mq),
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 loadScripts() {
if (this.client.bootConfig.disableSpaceLua) {
console.info("Space Lua scripts are disabled, skipping loading scripts");
return;
}
if (!await this.hasFullIndexCompleted()) {
console.info(
"Not loading space scripts, since initial 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<Record<string, Command>>("commands", {}),
)
) {
this.scriptCommands.set(name, command);
}
// Make scripted (slash) commands available
this.commandHook.throttledBuildAllCommandsAndEmit();
this.slashCommandHook.throttledBuildAllCommands();
this.scriptsLoaded = true;
}
async reloadPlugsFromSpace(space: Space) {
console.log("(Re)loading plugs");
await this.system.unloadAll();
let allPlugs = await space.listPlugs();
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.system.loadPlug(
createWorkerSandboxFromLocalPath(fileMeta.name),
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);
}
queryLuaObjects<T>(tag: string, query: LuaCollectionQuery): Promise<T[]> {
return this.system.invokeFunction("index.queryLuaObjects", [tag, query]);
}
getObjectByRef<T>(page: string, tag: string, ref: string) {
return this.localSyscall(
"system.invokeFunction",
["index.getObjectByRef", page, tag, ref],
);
}
async isIndexOngoing() {
return !!(await this.ds.get(indexQueuedKey));
}
async setIndexOngoing(val: boolean = true) {
await this.ds.set(indexQueuedKey, val);
}
async ensureFullIndex() {
if (!this.client.fullSyncCompleted) {
console.info(
"Initial full sync not completed, skipping index check",
);
return;
}
const currentIndexVersion = await this.getCurrentIndexVersion();
if (!currentIndexVersion) {
console.log("No index version found, assuming fresh install");
await this.markFullSpaceIndexComplete();
return;
}
console.info(
"[index]",
"Current space index version",
currentIndexVersion,
"index ongoing?",
await this.isIndexOngoing(),
);
if (
currentIndexVersion !== desiredIndexVersion &&
!await this.isIndexOngoing()
) {
console.info(
"[index]",
"Performing a full space reindex, this could take a while...",
);
await this.setIndexOngoing();
await this.system.invokeFunction("index.reindexSpace", []);
console.info("[index]", "Full space index complete.");
await this.markFullSpaceIndexComplete();
await this.setIndexOngoing(false);
// Let's load space scripts again, which probably weren't loaded before
await this.reloadState();
}
}
public async reloadState() {
console.log(
"Now loading space scripts, custom styles and rebuilding editor state",
);
await this.loadScripts();
await this.client.loadCustomStyles();
this.client.rebuildEditorState();
}
public async evalLuaFunction(
luaFunction: ILuaFunction,
args: LuaValue[],
): Promise<LuaValue> {
const tl = await buildThreadLocalEnv(
this.system,
this.spaceLuaEnv.env,
);
const sf = new LuaStackFrame(tl, null);
try {
return luaValueToJS(
await luaCall(luaFunction, args.map(jsToLuaValue), {}, sf),
sf,
);
} catch (e: any) {
await handleLuaError(e, this.system);
}
}
public async hasFullIndexCompleted() {
return (await this.ds.get(indexVersionKey)) === desiredIndexVersion;
}
private getCurrentIndexVersion() {
return this.ds.get(indexVersionKey);
}
async markFullSpaceIndexComplete() {
await this.ds.set(indexVersionKey, desiredIndexVersion);
}
}