Refactoring and adding ability to create custom commands from space functions

pull/690/head
Zef Hemel 2024-02-07 14:50:01 +01:00
parent b3dc303624
commit 05efbc8741
26 changed files with 316 additions and 213 deletions

View File

@ -8,6 +8,9 @@ import { EndpointHook } from "../plugos/hooks/endpoint.ts";
import { LocalShell } from "../server/shell_backend.ts"; import { LocalShell } from "../server/shell_backend.ts";
import { Hono } from "../server/deps.ts"; import { Hono } from "../server/deps.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
import { DataStore } from "../plugos/lib/datastore.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
import { EventHook } from "../plugos/hooks/event.ts";
export async function runPlug( export async function runPlug(
spacePath: string, spacePath: string,
@ -23,6 +26,10 @@ export async function runPlug(
const endpointHook = new EndpointHook("/_/"); const endpointHook = new EndpointHook("/_/");
const ds = new DataStore(kvPrimitives);
const mq = new DataStoreMQ(ds);
const eventHook = new EventHook();
const serverSystem = new ServerSystem( const serverSystem = new ServerSystem(
new AssetBundlePlugSpacePrimitives( new AssetBundlePlugSpacePrimitives(
new DiskSpacePrimitives(spacePath), new DiskSpacePrimitives(spacePath),
@ -30,6 +37,9 @@ export async function runPlug(
), ),
kvPrimitives, kvPrimitives,
new LocalShell(spacePath), new LocalShell(spacePath),
mq,
ds,
eventHook,
false, false,
true, true,
); );

72
common/common_system.ts Normal file
View File

@ -0,0 +1,72 @@
import { AppCommand, CommandHook } from "./hooks/command.ts";
import { PlugNamespaceHook } from "./hooks/plug_namespace.ts";
import { SilverBulletHooks } from "./manifest.ts";
import { buildQueryFunctions } from "./query_functions.ts";
import { ScriptEnvironment } from "./space_script.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { DataStore } from "../plugos/lib/datastore.ts";
import { System } from "../plugos/system.ts";
import { CodeWidgetHook } from "../web/hooks/code_widget.ts";
import { PanelWidgetHook } from "../web/hooks/panel_widget.ts";
import { SlashCommandHook } from "../web/hooks/slash_command.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
export abstract class CommonSystem {
system!: System<SilverBulletHooks>;
// Hooks
commandHook!: CommandHook;
slashCommandHook!: SlashCommandHook;
namespaceHook!: PlugNamespaceHook;
codeWidgetHook!: CodeWidgetHook;
panelWidgetHook!: PanelWidgetHook;
readonly allKnownPages = new Set<string>();
readonly spaceScriptCommands = new Map<string, AppCommand>();
constructor(
protected mq: DataStoreMQ,
protected ds: DataStore,
protected eventHook: EventHook,
public readOnlyMode: boolean,
protected enableSpaceScript: boolean,
) {
setInterval(() => {
// Timeout after 5s, retries 3 times, otherwise drops the message (no DLQ)
mq.requeueTimeouts(5000, 3, true).catch(console.error);
}, 20000); // Look to requeue every 20s
}
async loadSpaceScripts() {
let functions = buildQueryFunctions(
this.allKnownPages,
this.system,
);
const scriptEnv = new ScriptEnvironment();
if (this.enableSpaceScript) {
try {
await scriptEnv.loadFromSystem(this.system);
console.log(
"Loaded",
Object.keys(scriptEnv.functions).length,
"functions and",
Object.keys(scriptEnv.commands).length,
"commands from space-script",
);
} catch (e: any) {
console.error("Error loading space-script:", e.message);
}
functions = { ...functions, ...scriptEnv.functions };
// Reset the space script commands
this.spaceScriptCommands.clear();
for (const [name, command] of Object.entries(scriptEnv.commands)) {
this.spaceScriptCommands.set(name, command);
}
this.commandHook.throttledBuildAllCommands();
}
// Swap in the expanded function map
this.ds.functionMap = functions;
}
}

View File

@ -43,7 +43,10 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
editorCommands = new Map<string, AppCommand>(); editorCommands = new Map<string, AppCommand>();
system!: System<CommandHookT>; system!: System<CommandHookT>;
constructor(private readOnly: boolean) { constructor(
private readOnly: boolean,
private additionalCommandsMap: Map<string, AppCommand>,
) {
super(); super();
} }
@ -76,6 +79,9 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
} }
} }
await this.loadPageTemplateCommands(); await this.loadPageTemplateCommands();
for (const [name, cmd] of this.additionalCommandsMap) {
this.editorCommands.set(name, cmd);
}
this.emit("commandsUpdated", this.editorCommands); this.emit("commandsUpdated", this.editorCommands);
} }

View File

@ -1,7 +1,7 @@
import * as plugos from "../plugos/types.ts"; import * as plugos from "../plugos/types.ts";
import { CronHookT } from "../plugos/hooks/cron.ts"; import { CronHookT } from "../plugos/hooks/cron.ts";
import { EventHookT } from "../plugos/hooks/event.ts"; import { EventHookT } from "../plugos/hooks/event.ts";
import { CommandHookT } from "../web/hooks/command.ts"; import { CommandHookT } from "./hooks/command.ts";
import { SlashCommandHookT } from "../web/hooks/slash_command.ts"; import { SlashCommandHookT } from "../web/hooks/slash_command.ts";
import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts"; import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts";
import { CodeWidgetT } from "../web/hooks/code_widget.ts"; import { CodeWidgetT } from "../web/hooks/code_widget.ts";

View File

@ -3,29 +3,15 @@ import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
import { System } from "../plugos/system.ts"; import { System } from "../plugos/system.ts";
import { Query } from "$sb/types.ts"; import { Query } from "$sb/types.ts";
import { LimitedMap } from "$sb/lib/limited_map.ts"; import { LimitedMap } from "$sb/lib/limited_map.ts";
import { ScriptEnvironment } from "./space_script.ts";
const pageCacheTtl = 10 * 1000; // 10s const pageCacheTtl = 10 * 1000; // 10s
export async function buildQueryFunctions( export function buildQueryFunctions(
allKnownPages: Set<string>, allKnownPages: Set<string>,
system: System<any>, system: System<any>,
enableSpaceScript: boolean, ): FunctionMap {
): Promise<FunctionMap> {
const pageCache = new LimitedMap<string>(10); const pageCache = new LimitedMap<string>(10);
const scriptEnv = new ScriptEnvironment();
if (enableSpaceScript) {
try {
await scriptEnv.loadFromSystem(system);
console.log(
"Loaded",
Object.keys(scriptEnv.functions).length,
"functions from space-script",
);
} catch (e: any) {
console.error("Error loading space-script:", e.message);
}
}
return { return {
...builtinFunctions, ...builtinFunctions,
pageExists(name: string) { pageExists(name: string) {
@ -54,7 +40,7 @@ export async function buildQueryFunctions(
if (cachedPage) { if (cachedPage) {
return cachedPage; return cachedPage;
} else { } else {
return system.syscall({}, "space.readPage", [name]).then((page) => { return system.localSyscall("space.readPage", [name]).then((page) => {
pageCache.set(name, page, pageCacheTtl); pageCache.set(name, page, pageCacheTtl);
return page; return page;
}).catch((e: any) => { }).catch((e: any) => {
@ -64,6 +50,5 @@ export async function buildQueryFunctions(
}); });
} }
}, },
...scriptEnv.functions,
}; };
} }

View File

@ -1,21 +1,47 @@
// deno-lint-ignore-file ban-types
import { System } from "../plugos/system.ts"; import { System } from "../plugos/system.ts";
import { ScriptObject } from "../plugs/index/script.ts"; import { ScriptObject } from "../plugs/index/script.ts";
import { AppCommand, CommandDef } from "./hooks/command.ts";
export class ScriptEnvironment { export class ScriptEnvironment {
functions: Record<string, Function> = {}; functions: Record<string, (...args: any[]) => any> = {};
commands: Record<string, AppCommand> = {};
// Public API // Public API
registerFunction(name: string, fn: Function) { registerFunction(name: string, fn: (...args: any[]) => any) {
this.functions[name] = fn; this.functions[name] = fn;
} }
registerCommand(command: CommandDef, fn: (...args: any[]) => any) {
this.commands[command.name] = {
command,
run: (...args: any[]) => {
return new Promise((resolve) => {
// Next tick
setTimeout(() => {
resolve(fn(...args));
});
});
},
};
}
// Internal API // Internal API
evalScript(script: string, system: System<any>) { evalScript(script: string, system: System<any>) {
try { try {
Function("silverbullet", "syscall", script)( const fn = Function(
"silverbullet",
"syscall",
"Deno",
"window",
"globalThis",
"self",
script,
);
fn.call(
{},
this, this,
(name: string, ...args: any[]) => system.syscall({}, name, args), (name: string, ...args: any[]) => system.syscall({}, name, args),
// The rest is explicitly left to be undefined to prevent access to the global scope
); );
} catch (e: any) { } catch (e: any) {
throw new Error( throw new Error(

View File

@ -1,15 +1,15 @@
import { SyscallMeta } from "$sb/types.ts"; import { SyscallMeta } from "$sb/types.ts";
import { SysCallMapping, System } from "../../plugos/system.ts"; import { SysCallMapping, System } from "../../plugos/system.ts";
import type { ServerSystem } from "../../server/server_system.ts";
import type { Client } from "../../web/client.ts"; import type { Client } from "../../web/client.ts";
import { CommandDef } from "../../web/hooks/command.ts"; import { CommandDef } from "../hooks/command.ts";
import { proxySyscall } from "../../web/syscalls/util.ts"; import { proxySyscall } from "../../web/syscalls/util.ts";
import type { CommonSystem } from "../common_system.ts";
export function systemSyscalls( export function systemSyscalls(
system: System<any>, system: System<any>,
readOnlyMode: boolean, readOnlyMode: boolean,
client: Client | undefined, commonSystem: CommonSystem,
serverSystem: ServerSystem | undefined, client?: Client,
): SysCallMapping { ): SysCallMapping {
const api: SysCallMapping = { const api: SysCallMapping = {
"system.invokeFunction": ( "system.invokeFunction": (
@ -50,8 +50,7 @@ export function systemSyscalls(
return client.runCommandByName(name, args); return client.runCommandByName(name, args);
}, },
"system.listCommands": (): { [key: string]: CommandDef } => { "system.listCommands": (): { [key: string]: CommandDef } => {
const commandHook = client?.system.commandHook || const commandHook = commonSystem!.commandHook;
serverSystem!.commandHook;
const allCommands: { [key: string]: CommandDef } = {}; const allCommands: { [key: string]: CommandDef } = {};
for (const [cmd, def] of commandHook.editorCommands) { for (const [cmd, def] of commandHook.editorCommands) {
allCommands[cmd] = def.command; allCommands[cmd] = def.command;
@ -76,9 +75,9 @@ export function systemSyscalls(
return client.loadPlugs(); return client.loadPlugs();
}, },
"system.loadSpaceScripts": async () => { "system.loadSpaceScripts": async () => {
// Reload scripts locally
await commonSystem.loadSpaceScripts();
if (client) { if (client) {
// If this is invoked on the client, we need to load the space scripts locally
await client.loadSpaceScripts();
// And we are in a hybrud mode, tell the server to do the same // And we are in a hybrud mode, tell the server to do the same
if (system.env === "client") { if (system.env === "client") {
console.info( console.info(
@ -91,10 +90,6 @@ export function systemSyscalls(
[], [],
); );
} }
} else if (serverSystem) {
return serverSystem.loadSpaceScripts();
} else {
throw new Error("Load space scripts in an undefined environment");
} }
}, },
"system.getEnv": () => { "system.getEnv": () => {

View File

@ -1,4 +1,4 @@
import type { CommandDef } from "../../web/hooks/command.ts"; import type { CommandDef } from "../../common/hooks/command.ts";
import { SyscallMeta } from "$sb/types.ts"; import { SyscallMeta } from "$sb/types.ts";
import { syscall } from "./syscall.ts"; import { syscall } from "./syscall.ts";

View File

@ -5,7 +5,10 @@ import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts
import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { DataStore } from "../plugos/lib/datastore.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
import { System } from "../plugos/system.ts"; import { System } from "../plugos/system.ts";
import { BuiltinSettings } from "../web/types.ts"; import { BuiltinSettings } from "../web/types.ts";
import { JWTIssuer } from "./crypto.ts"; import { JWTIssuer } from "./crypto.ts";
@ -30,6 +33,7 @@ export type SpaceServerConfig = {
clientEncryption: boolean; clientEncryption: boolean;
}; };
// Equivalent of Client on the server
export class SpaceServer { export class SpaceServer {
public pagesPath: string; public pagesPath: string;
auth?: { user: string; pass: string }; auth?: { user: string; pass: string };
@ -99,6 +103,11 @@ export class SpaceServer {
this.spacePrimitives = new ReadOnlySpacePrimitives(this.spacePrimitives); this.spacePrimitives = new ReadOnlySpacePrimitives(this.spacePrimitives);
} }
const ds = new DataStore(this.kvPrimitives);
const mq = new DataStoreMQ(ds);
const eventHook = new EventHook();
// system = undefined in databaseless mode (no PlugOS instance on the server and no DB) // system = undefined in databaseless mode (no PlugOS instance on the server and no DB)
if (!this.syncOnly) { if (!this.syncOnly) {
// Enable server-side processing // Enable server-side processing
@ -106,6 +115,9 @@ export class SpaceServer {
this.spacePrimitives, this.spacePrimitives,
this.kvPrimitives, this.kvPrimitives,
this.shellBackend, this.shellBackend,
mq,
ds,
eventHook,
this.readOnly, this.readOnly,
this.enableSpaceScript, this.enableSpaceScript,
); );

View File

@ -25,7 +25,6 @@ import {
dataStoreReadSyscalls, dataStoreReadSyscalls,
dataStoreWriteSyscalls, dataStoreWriteSyscalls,
} from "../plugos/syscalls/datastore.ts"; } from "../plugos/syscalls/datastore.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
import { languageSyscalls } from "../common/syscalls/language.ts"; import { languageSyscalls } from "../common/syscalls/language.ts";
import { templateSyscalls } from "../common/syscalls/template.ts"; import { templateSyscalls } from "../common/syscalls/template.ts";
import { codeWidgetSyscalls } from "../web/syscalls/code_widget.ts"; import { codeWidgetSyscalls } from "../web/syscalls/code_widget.ts";
@ -35,43 +34,29 @@ import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
import { ShellBackend } from "./shell_backend.ts"; import { ShellBackend } from "./shell_backend.ts";
import { ensureSpaceIndex } from "../common/space_index.ts"; import { ensureSpaceIndex } from "../common/space_index.ts";
import { FileMeta } from "$sb/types.ts"; import { FileMeta } from "$sb/types.ts";
import { buildQueryFunctions } from "../common/query_functions.ts"; import { CommandHook } from "../common/hooks/command.ts";
import { CommandHook } from "../web/hooks/command.ts"; import { CommonSystem } from "../common/common_system.ts";
import { MessageQueue } from "../plugos/lib/mq.ts";
// // Important: load this before the actual plugs import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
// import {
// createSandbox as noSandboxFactory,
// runWithSystemLock,
// } from "../plugos/sandboxes/no_sandbox.ts";
// // Load list of builtin plugs
// import { plug as plugIndex } from "../dist_plug_bundle/_plug/index.plug.js";
// import { plug as plugFederation } from "../dist_plug_bundle/_plug/federation.plug.js";
// import { plug as plugQuery } from "../dist_plug_bundle/_plug/query.plug.js";
// import { plug as plugSearch } from "../dist_plug_bundle/_plug/search.plug.js";
// import { plug as plugTasks } from "../dist_plug_bundle/_plug/tasks.plug.js";
// import { plug as plugTemplate } from "../dist_plug_bundle/_plug/template.plug.js";
const fileListInterval = 30 * 1000; // 30s const fileListInterval = 30 * 1000; // 30s
const plugNameExtractRegex = /([^/]+)\.plug\.js$/; const plugNameExtractRegex = /([^/]+)\.plug\.js$/;
export class ServerSystem { export class ServerSystem extends CommonSystem {
system!: System<SilverBulletHooks>;
public spacePrimitives!: SpacePrimitives;
// denoKv!: Deno.Kv;
listInterval?: number; listInterval?: number;
ds!: DataStore;
allKnownPages = new Set<string>();
commandHook!: CommandHook;
constructor( constructor(
private baseSpacePrimitives: SpacePrimitives, public spacePrimitives: SpacePrimitives,
readonly kvPrimitives: KvPrimitives, private kvPrimitives: KvPrimitives,
private shellBackend: ShellBackend, private shellBackend: ShellBackend,
private readOnlyMode: boolean, mq: DataStoreMQ,
private enableSpaceScript: boolean, ds: DataStore,
eventHook: EventHook,
readOnlyMode: boolean,
enableSpaceScript: boolean,
) { ) {
super(mq, ds, eventHook, readOnlyMode, enableSpaceScript);
} }
// Always needs to be invoked right after construction // Always needs to be invoked right after construction
@ -94,24 +79,20 @@ export class ServerSystem {
this.system.addHook(eventHook); this.system.addHook(eventHook);
// Command hook, just for introspection // Command hook, just for introspection
this.commandHook = new CommandHook(this.readOnlyMode); this.commandHook = new CommandHook(
this.readOnlyMode,
this.spaceScriptCommands,
);
this.system.addHook(this.commandHook); this.system.addHook(this.commandHook);
// Cron hook // Cron hook
const cronHook = new CronHook(this.system); const cronHook = new CronHook(this.system);
this.system.addHook(cronHook); this.system.addHook(cronHook);
const mq = new DataStoreMQ(this.ds);
setInterval(() => {
// Timeout after 5s, retries 3 times, otherwise drops the message (no DLQ)
mq.requeueTimeouts(5000, 3, true).catch(console.error);
}, 20000); // Look to requeue every 20s
const plugNamespaceHook = new PlugNamespaceHook(); const plugNamespaceHook = new PlugNamespaceHook();
this.system.addHook(plugNamespaceHook); this.system.addHook(plugNamespaceHook);
this.system.addHook(new MQHook(this.system, mq)); this.system.addHook(new MQHook(this.system, this.mq));
const codeWidgetHook = new CodeWidgetHook(); const codeWidgetHook = new CodeWidgetHook();
@ -119,7 +100,7 @@ export class ServerSystem {
this.spacePrimitives = new EventedSpacePrimitives( this.spacePrimitives = new EventedSpacePrimitives(
new PlugSpacePrimitives( new PlugSpacePrimitives(
this.baseSpacePrimitives, this.spacePrimitives,
plugNamespaceHook, plugNamespaceHook,
), ),
eventHook, eventHook,
@ -133,8 +114,8 @@ export class ServerSystem {
spaceReadSyscalls(space), spaceReadSyscalls(space),
assetSyscalls(this.system), assetSyscalls(this.system),
yamlSyscalls(), yamlSyscalls(),
systemSyscalls(this.system, this.readOnlyMode, undefined, this), systemSyscalls(this.system, this.readOnlyMode, this),
mqSyscalls(mq), mqSyscalls(this.mq),
languageSyscalls(), languageSyscalls(),
templateSyscalls(this.ds), templateSyscalls(this.ds),
dataStoreReadSyscalls(this.ds), dataStoreReadSyscalls(this.ds),
@ -167,9 +148,6 @@ export class ServerSystem {
await this.loadSpaceScripts(); await this.loadSpaceScripts();
this.listInterval = setInterval(() => { this.listInterval = setInterval(() => {
// runWithSystemLock(this.system, async () => {
// await space.updatePageList();
// });
space.updatePageList().catch(console.error); space.updatePageList().catch(console.error);
}, fileListInterval); }, fileListInterval);
@ -211,9 +189,7 @@ export class ServerSystem {
await indexPromise; await indexPromise;
} }
// await runWithSystemLock(this.system, async () => {
await eventHook.dispatchEvent("system:ready"); await eventHook.dispatchEvent("system:ready");
// });
} }
async loadPlugs() { async loadPlugs() {
@ -224,15 +200,6 @@ export class ServerSystem {
} }
} }
async loadSpaceScripts() {
// Swap in the expanded function map
this.ds.functionMap = await buildQueryFunctions(
this.allKnownPages,
this.system,
this.enableSpaceScript,
);
}
async loadPlugFromSpace(path: string): Promise<Plug<SilverBulletHooks>> { async loadPlugFromSpace(path: string): Promise<Plug<SilverBulletHooks>> {
const { meta, data } = await this.spacePrimitives.readFile(path); const { meta, data } = await this.spacePrimitives.readFile(path);
const plugName = path.match(plugNameExtractRegex)![1]; const plugName = path.match(plugNameExtractRegex)![1];

View File

@ -10,6 +10,12 @@ import { plugCompileCommand } from "./cmd/plug_compile.ts";
import { plugRunCommand } from "./cmd/plug_run.ts"; import { plugRunCommand } from "./cmd/plug_run.ts";
import { syncCommand } from "./cmd/sync.ts"; import { syncCommand } from "./cmd/sync.ts";
// Unhandled rejection, let's not crash
globalThis.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled rejection:", event);
event.preventDefault();
});
await new Command() await new Command()
.name("silverbullet") .name("silverbullet")
.description("Markdown as a platform") .description("Markdown as a platform")

View File

@ -12,7 +12,7 @@ import { Space } from "./space.ts";
import { FilterOption } from "./types.ts"; import { FilterOption } from "./types.ts";
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
import { EventHook } from "../plugos/hooks/event.ts"; import { EventHook } from "../plugos/hooks/event.ts";
import { AppCommand } from "./hooks/command.ts"; import { AppCommand } from "../common/hooks/command.ts";
import { import {
PageState, PageState,
parsePageRefFromURI, parsePageRefFromURI,
@ -57,7 +57,6 @@ import {
} from "../common/space_index.ts"; } from "../common/space_index.ts";
import { LimitedMap } from "$sb/lib/limited_map.ts"; import { LimitedMap } from "$sb/lib/limited_map.ts";
import { renderTheTemplate } from "../common/syscalls/template.ts"; import { renderTheTemplate } from "../common/syscalls/template.ts";
import { buildQueryFunctions } from "../common/query_functions.ts";
import { PageRef } from "$sb/lib/page.ts"; import { PageRef } from "$sb/lib/page.ts";
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts"; import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
@ -83,7 +82,7 @@ declare global {
// history.scrollRestoration = "manual"; // history.scrollRestoration = "manual";
export class Client { export class Client {
system!: ClientSystem; clientSystem!: ClientSystem;
editorView!: EditorView; editorView!: EditorView;
keyHandlerCompartment?: Compartment; keyHandlerCompartment?: Compartment;
@ -116,8 +115,6 @@ export class Client {
spaceKV?: KvPrimitives; spaceKV?: KvPrimitives;
mq!: DataStoreMQ; mq!: DataStoreMQ;
// Used by the "wiki link" highlighter to check if a page exists
public allKnownPages = new Set<string>();
onLoadPageRef: PageRef; onLoadPageRef: PageRef;
constructor( constructor(
@ -148,15 +145,11 @@ export class Client {
// Setup message queue // Setup message queue
this.mq = new DataStoreMQ(this.stateDataStore); this.mq = new DataStoreMQ(this.stateDataStore);
setInterval(() => {
// Timeout after 5s, retries 3 times, otherwise drops the message (no DLQ)
this.mq.requeueTimeouts(5000, 3, true).catch(console.error);
}, 20000); // Look to requeue every 20s
// Event hook // Event hook
this.eventHook = new EventHook(); this.eventHook = new EventHook();
// Instantiate a PlugOS system // Instantiate a PlugOS system
this.system = new ClientSystem( this.clientSystem = new ClientSystem(
this, this,
this.mq, this.mq,
this.stateDataStore, this.stateDataStore,
@ -193,7 +186,7 @@ export class Client {
this.focus(); this.focus();
this.system.init(); this.clientSystem.init();
await this.loadSettings(); await this.loadSettings();
@ -224,7 +217,7 @@ export class Client {
} }
await this.loadPlugs(); await this.loadPlugs();
await this.loadSpaceScripts(); await this.clientSystem.loadSpaceScripts();
await this.initNavigator(); await this.initNavigator();
await this.initSync(); await this.initSync();
@ -246,15 +239,6 @@ export class Client {
this.updatePageListCache().catch(console.error); this.updatePageListCache().catch(console.error);
} }
async loadSpaceScripts() {
// Swap in the expanded function map
this.stateDataStore.functionMap = await buildQueryFunctions(
this.allKnownPages,
this.system.system,
window.silverBulletConfig.enableSpaceScript,
);
}
async loadSettings() { async loadSettings() {
this.settings = await ensureAndLoadSettingsAndIndex( this.settings = await ensureAndLoadSettingsAndIndex(
this.space.spacePrimitives, this.space.spacePrimitives,
@ -286,14 +270,14 @@ export class Client {
// A full sync just completed // A full sync just completed
if (!initialSync) { if (!initialSync) {
// If this was NOT the initial sync let's check if we need to perform a space reindex // If this was NOT the initial sync let's check if we need to perform a space reindex
ensureSpaceIndex(this.stateDataStore, this.system.system).catch( ensureSpaceIndex(this.stateDataStore, this.clientSystem.system).catch(
console.error, console.error,
); );
} else { } else {
// This was the initial sync, let's mark a full index as completed // This was the initial sync, let's mark a full index as completed
await markFullSpaceIndexComplete(this.stateDataStore); await markFullSpaceIndexComplete(this.stateDataStore);
// And load space scripts, which probably weren't loaded before // And load space scripts, which probably weren't loaded before
await this.loadSpaceScripts(); await this.clientSystem.loadSpaceScripts();
} }
} }
if (operations) { if (operations) {
@ -340,7 +324,6 @@ export class Client {
pageState.header !== undefined)) pageState.header !== undefined))
) { ) {
setTimeout(() => { setTimeout(() => {
console.log("Kicking off scroll to", pageState.scrollTop);
this.editorView.scrollDOM.scrollTop = pageState.scrollTop!; this.editorView.scrollDOM.scrollTop = pageState.scrollTop!;
}); });
adjustedPosition = true; adjustedPosition = true;
@ -544,7 +527,7 @@ export class Client {
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives( this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
remoteSpacePrimitives, remoteSpacePrimitives,
this.system.namespaceHook, this.clientSystem.namespaceHook,
this.syncMode ? undefined : "client", this.syncMode ? undefined : "client",
); );
@ -628,21 +611,21 @@ export class Client {
// Caching a list of known pages for the wiki_link highlighter (that checks if a page exists) // Caching a list of known pages for the wiki_link highlighter (that checks if a page exists)
this.eventHook.addLocalListener("page:saved", (pageName: string) => { this.eventHook.addLocalListener("page:saved", (pageName: string) => {
// Make sure this page is in the list of known pages // Make sure this page is in the list of known pages
this.allKnownPages.add(pageName); this.clientSystem.allKnownPages.add(pageName);
}); });
this.eventHook.addLocalListener("page:deleted", (pageName: string) => { this.eventHook.addLocalListener("page:deleted", (pageName: string) => {
this.allKnownPages.delete(pageName); this.clientSystem.allKnownPages.delete(pageName);
}); });
this.eventHook.addLocalListener( this.eventHook.addLocalListener(
"file:listed", "file:listed",
(allFiles: FileMeta[]) => { (allFiles: FileMeta[]) => {
// Update list of known pages // Update list of known pages
this.allKnownPages.clear(); this.clientSystem.allKnownPages.clear();
allFiles.forEach((f) => { allFiles.forEach((f) => {
if (f.name.endsWith(".md")) { if (f.name.endsWith(".md")) {
this.allKnownPages.add(f.name.slice(0, -3)); this.clientSystem.allKnownPages.add(f.name.slice(0, -3));
} }
}); });
}, },
@ -741,11 +724,11 @@ export class Client {
async updatePageListCache() { async updatePageListCache() {
console.log("Updating page list cache"); console.log("Updating page list cache");
if (!this.system.system.loadedPlugs.has("index")) { if (!this.clientSystem.system.loadedPlugs.has("index")) {
console.warn("Index plug not loaded, cannot update page list cache"); console.warn("Index plug not loaded, cannot update page list cache");
return; return;
} }
const allPages = await this.system.queryObjects<PageMeta>("page", {}); const allPages = await this.clientSystem.queryObjects<PageMeta>("page", {});
this.ui.viewDispatch({ this.ui.viewDispatch({
type: "update-page-list", type: "update-page-list",
allPages, allPages,
@ -828,7 +811,7 @@ export class Client {
} }
async loadPlugs() { async loadPlugs() {
await this.system.reloadPlugsFromSpace(this.space); await this.clientSystem.reloadPlugsFromSpace(this.space);
await this.eventHook.dispatchEvent("system:ready"); await this.eventHook.dispatchEvent("system:ready");
await this.dispatchAppEvent("plugs:loaded"); await this.dispatchAppEvent("plugs:loaded");
} }
@ -932,6 +915,7 @@ export class Client {
viewState.showPrompt, viewState.showPrompt,
].some(Boolean) ].some(Boolean)
) { ) {
console.log("not focusing");
// Some other modal UI element is visible, don't focus editor now // Some other modal UI element is visible, don't focus editor now
return; return;
} }
@ -965,7 +949,7 @@ export class Client {
"Navigating to new page in new window", "Navigating to new page in new window",
`${location.origin}/${encodePageRef(pageRef)}`, `${location.origin}/${encodePageRef(pageRef)}`,
); );
const win = window.open( const win = globalThis.open(
`${location.origin}/${encodePageRef(pageRef)}`, `${location.origin}/${encodePageRef(pageRef)}`,
"_blank", "_blank",
); );
@ -1021,13 +1005,14 @@ export class Client {
perm: "rw", perm: "rw",
} as PageMeta, } as PageMeta,
}; };
this.system.system.invokeFunction("template.newPage", [pageName]).then( this.clientSystem.system.invokeFunction("template.newPage", [pageName])
() => { .then(
this.focus(); () => {
}, this.focus();
).catch( },
console.error, ).catch(
); console.error,
);
} else { } else {
this.flashNotification( this.flashNotification(
`Could not load page ${pageName}: ${e.message}`, `Could not load page ${pageName}: ${e.message}`,

View File

@ -9,7 +9,7 @@ import { eventSyscalls } from "../plugos/syscalls/event.ts";
import { System } from "../plugos/system.ts"; import { System } from "../plugos/system.ts";
import type { Client } from "./client.ts"; import type { Client } from "./client.ts";
import { CodeWidgetHook } from "./hooks/code_widget.ts"; import { CodeWidgetHook } from "./hooks/code_widget.ts";
import { CommandHook } from "./hooks/command.ts"; import { CommandHook } from "../common/hooks/command.ts";
import { SlashCommandHook } from "./hooks/slash_command.ts"; import { SlashCommandHook } from "./hooks/slash_command.ts";
import { clientStoreSyscalls } from "./syscalls/clientStore.ts"; import { clientStoreSyscalls } from "./syscalls/clientStore.ts";
import { debugSyscalls } from "./syscalls/debug.ts"; import { debugSyscalls } from "./syscalls/debug.ts";
@ -41,24 +41,29 @@ import { deepObjectMerge } from "$sb/lib/json.ts";
import { Query } from "$sb/types.ts"; import { Query } from "$sb/types.ts";
import { PanelWidgetHook } from "./hooks/panel_widget.ts"; import { PanelWidgetHook } from "./hooks/panel_widget.ts";
import { createKeyBindings } from "./editor_state.ts"; import { createKeyBindings } from "./editor_state.ts";
import { CommonSystem } from "../common/common_system.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
const plugNameExtractRegex = /\/(.+)\.plug\.js$/; const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
export class ClientSystem { /**
commandHook: CommandHook; * Wrapper around a System, used by the client
slashCommandHook: SlashCommandHook; */
namespaceHook: PlugNamespaceHook; export class ClientSystem extends CommonSystem {
codeWidgetHook: CodeWidgetHook;
system: System<SilverBulletHooks>;
panelWidgetHook: PanelWidgetHook;
constructor( constructor(
private client: Client, private client: Client,
private mq: MessageQueue, mq: DataStoreMQ,
private ds: DataStore, ds: DataStore,
private eventHook: EventHook, eventHook: EventHook,
private readOnlyMode: boolean, readOnlyMode: boolean,
) { ) {
super(
mq,
ds,
eventHook,
readOnlyMode,
window.silverBulletConfig.enableSpaceScript,
);
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid) // Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
this.system = new System( this.system = new System(
client.syncMode ? undefined : "client", client.syncMode ? undefined : "client",
@ -95,7 +100,10 @@ export class ClientSystem {
} }
// Command hook // Command hook
this.commandHook = new CommandHook(this.readOnlyMode); this.commandHook = new CommandHook(
this.readOnlyMode,
this.spaceScriptCommands,
);
this.commandHook.on({ this.commandHook.on({
commandsUpdated: (commandMap) => { commandsUpdated: (commandMap) => {
this.client.ui?.viewDispatch({ this.client.ui?.viewDispatch({
@ -158,7 +166,7 @@ export class ClientSystem {
eventSyscalls(this.eventHook), eventSyscalls(this.eventHook),
editorSyscalls(this.client), editorSyscalls(this.client),
spaceReadSyscalls(this.client), spaceReadSyscalls(this.client),
systemSyscalls(this.system, false, this.client, undefined), systemSyscalls(this.system, false, this, this.client),
markdownSyscalls(), markdownSyscalls(),
assetSyscalls(this.system), assetSyscalls(this.system),
yamlSyscalls(), yamlSyscalls(),

View File

@ -22,12 +22,13 @@ export function fencedCodePlugin(editor: Client) {
} }
const text = state.sliceDoc(from, to); const text = state.sliceDoc(from, to);
const [_, lang] = text.match(/^```(\w+)?/)!; const [_, lang] = text.match(/^```(\w+)?/)!;
const codeWidgetCallback = editor.system.codeWidgetHook const codeWidgetCallback = editor.clientSystem.codeWidgetHook
.codeWidgetCallbacks .codeWidgetCallbacks
.get(lang); .get(lang);
const renderMode = editor.system.codeWidgetHook.codeWidgetModes.get( const renderMode = editor.clientSystem.codeWidgetHook.codeWidgetModes
lang, .get(
); lang,
);
// Only custom render when we have a custom renderer, and the current page is not a template // Only custom render when we have a custom renderer, and the current page is not a template
if (codeWidgetCallback && !isTemplate(state.sliceDoc(0, from))) { if (codeWidgetCallback && !isTemplate(state.sliceDoc(0, from))) {
// We got a custom renderer! // We got a custom renderer!

View File

@ -64,7 +64,7 @@ export class MarkdownWidget extends WidgetType {
extendedMarkdownLanguage, extendedMarkdownLanguage,
widgetContent.markdown!, widgetContent.markdown!,
); );
mdTree = await this.client.system.localSyscall( mdTree = await this.client.clientSystem.localSyscall(
"system.invokeFunction", "system.invokeFunction",
[ [
"markdown.expandCodeWidgets", "markdown.expandCodeWidgets",
@ -215,7 +215,7 @@ export class MarkdownWidget extends WidgetType {
// Update state in DOM as well for future toggles // Update state in DOM as well for future toggles
e.target.dataset.state = newState; e.target.dataset.state = newState;
console.log("Toggling task", taskRef); console.log("Toggling task", taskRef);
this.client.system.localSyscall( this.client.clientSystem.localSyscall(
"system.invokeFunction", "system.invokeFunction",
["tasks.updateTaskState", taskRef, oldState, newState], ["tasks.updateTaskState", taskRef, oldState, newState],
).catch( ).catch(
@ -234,7 +234,7 @@ export class MarkdownWidget extends WidgetType {
if (button.widgetTarget) { if (button.widgetTarget) {
div.addEventListener("click", () => { div.addEventListener("click", () => {
console.log("Widget clicked"); console.log("Widget clicked");
this.client.system.localSyscall("system.invokeFunction", [ this.client.clientSystem.localSyscall("system.invokeFunction", [
button.invokeFunction, button.invokeFunction,
this.from, this.from,
]).catch(console.error); ]).catch(console.error);
@ -245,7 +245,7 @@ export class MarkdownWidget extends WidgetType {
(e) => { (e) => {
e.stopPropagation(); e.stopPropagation();
console.log("Button clicked:", button.description); console.log("Button clicked:", button.description);
this.client.system.localSyscall("system.invokeFunction", [ this.client.clientSystem.localSyscall("system.invokeFunction", [
button.invokeFunction, button.invokeFunction,
this.from, this.from,
]).then((newContent: string | undefined) => { ]).then((newContent: string | undefined) => {

View File

@ -6,7 +6,7 @@ import { MarkdownWidget } from "./markdown_widget.ts";
export function postScriptPrefacePlugin( export function postScriptPrefacePlugin(
editor: Client, editor: Client,
) { ) {
const panelWidgetHook = editor.system.panelWidgetHook; const panelWidgetHook = editor.clientSystem.panelWidgetHook;
return decoratorStateField((state: EditorState) => { return decoratorStateField((state: EditorState) => {
const widgets: any[] = []; const widgets: any[] = [];
const topCallback = panelWidgetHook.callbacks.get("top"); const topCallback = panelWidgetHook.callbacks.get("top");

View File

@ -33,7 +33,7 @@ export function cleanWikiLinkPlugin(client: Client) {
const pageRef = parsePageRef(page); const pageRef = parsePageRef(page);
pageRef.page = resolvePath(client.currentPage, pageRef.page); pageRef.page = resolvePath(client.currentPage, pageRef.page);
const lowerCasePageName = pageRef.page.toLowerCase(); const lowerCasePageName = pageRef.page.toLowerCase();
for (const pageName of client.allKnownPages) { for (const pageName of client.clientSystem.allKnownPages) {
if (pageName.toLowerCase() === lowerCasePageName) { if (pageName.toLowerCase() === lowerCasePageName) {
pageExists = true; pageExists = true;
break; break;

View File

@ -1,7 +1,7 @@
import { isMacLike } from "../../common/util.ts"; import { isMacLike } from "../../common/util.ts";
import { FilterList } from "./filter.tsx"; import { FilterList } from "./filter.tsx";
import { CompletionContext, CompletionResult, featherIcons } from "../deps.ts"; import { CompletionContext, CompletionResult, featherIcons } from "../deps.ts";
import { AppCommand } from "../hooks/command.ts"; import { AppCommand } from "../../common/hooks/command.ts";
import { BuiltinSettings, FilterOption } from "../types.ts"; import { BuiltinSettings, FilterOption } from "../types.ts";
import { parseCommand } from "../../common/command.ts"; import { parseCommand } from "../../common/command.ts";

View File

@ -47,7 +47,7 @@ export function Panel({
break; break;
case "syscall": { case "syscall": {
const { id, name, args } = data; const { id, name, args } = data;
editor.system.localSyscall(name, args).then( editor.clientSystem.localSyscall(name, args).then(
(result) => { (result) => {
if (!iFrameRef.current?.contentWindow) { if (!iFrameRef.current?.contentWindow) {
// iFrame already went away // iFrame already went away

View File

@ -123,7 +123,7 @@ export function mountIFrame(
case "syscall": { case "syscall": {
const { id, name, args } = data; const { id, name, args } = data;
try { try {
const result = await client.system.localSyscall(name, args); const result = await client.clientSystem.localSyscall(name, args);
if (!iframe.contentWindow) { if (!iframe.contentWindow) {
// iFrame already went away // iFrame already went away
return; return;

View File

@ -99,8 +99,8 @@ export function createEditorState(
autocompletion({ autocompletion({
override: [ override: [
client.editorComplete.bind(client), client.editorComplete.bind(client),
client.system.slashCommandHook.slashCommandCompleter.bind( client.clientSystem.slashCommandHook.slashCommandCompleter.bind(
client.system.slashCommandHook, client.clientSystem.slashCommandHook,
), ),
], ],
}), }),
@ -299,7 +299,7 @@ export function createCommandKeyBindings(client: Client): KeyBinding[] {
} }
// Then add bindings for plug commands // Then add bindings for plug commands
for (const def of client.system.commandHook.editorCommands.values()) { for (const def of client.clientSystem.commandHook.editorCommands.values()) {
if (def.command.key) { if (def.command.key) {
// If we've already overridden this command, skip it // If we've already overridden this command, skip it
if (overriddenCommands.has(def.command.key)) { if (overriddenCommands.has(def.command.key)) {

View File

@ -210,7 +210,7 @@ export class MainUI {
return; return;
} }
console.log("Now renaming page to...", newName); console.log("Now renaming page to...", newName);
await client.system.system.invokeFunction( await client.clientSystem.system.invokeFunction(
"index.renamePageCommand", "index.renamePageCommand",
[{ page: newName }], [{ page: newName }],
); );
@ -250,26 +250,29 @@ export class MainUI {
}] }]
: [], : [],
...viewState.settings.actionButtons ...viewState.settings.actionButtons
.filter((button) => (typeof button.mobile === "undefined") || (button.mobile === viewState.isMobile)) .filter((button) =>
.map((button) => { (typeof button.mobile === "undefined") ||
const parsedCommand = parseCommand(button.command); (button.mobile === viewState.isMobile)
let featherIcon = )
(featherIcons as any)[kebabToCamel(button.icon)]; .map((button) => {
if (!featherIcon) { const parsedCommand = parseCommand(button.command);
featherIcon = featherIcons.HelpCircle; let featherIcon =
} (featherIcons as any)[kebabToCamel(button.icon)];
return { if (!featherIcon) {
icon: featherIcon, featherIcon = featherIcons.HelpCircle;
description: button.description || "", }
callback: () => { return {
client.runCommandByName( icon: featherIcon,
parsedCommand.name, description: button.description || "",
parsedCommand.args, callback: () => {
); client.runCommandByName(
}, parsedCommand.name,
href: "", parsedCommand.args,
}; );
}), },
href: "",
};
}),
]} ]}
rhs={!!viewState.panels.rhs.mode && ( rhs={!!viewState.panels.rhs.mode && (
<div <div

View File

@ -116,7 +116,7 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
}); });
// Replace with whatever the completion is // Replace with whatever the completion is
safeRun(async () => { safeRun(async () => {
await this.editor.system.system.invokeFunction( await this.editor.clientSystem.system.invokeFunction(
slashCompletion.invoke, slashCompletion.invoke,
[slashCompletion], [slashCompletion],
); );

View File

@ -60,8 +60,10 @@ export function editorSyscalls(client: Client): SysCallMapping {
}, },
"editor.reloadSettingsAndCommands": async () => { "editor.reloadSettingsAndCommands": async () => {
await client.loadSettings(); await client.loadSettings();
await client.system.commandHook.buildAllCommands(); await client.clientSystem.system.localSyscall(
await client.system.system.localSyscall("system.loadSpaceScripts", []); "system.loadSpaceScripts",
[],
);
}, },
"editor.openUrl": (_ctx, url: string, existingWindow = false) => { "editor.openUrl": (_ctx, url: string, existingWindow = false) => {
if (!existingWindow) { if (!existingWindow) {

View File

@ -1,6 +1,6 @@
import { Manifest } from "../common/manifest.ts"; import { Manifest } from "../common/manifest.ts";
import { PageMeta } from "$sb/types.ts"; import { PageMeta } from "$sb/types.ts";
import { AppCommand } from "./hooks/command.ts"; import { AppCommand } from "../common/hooks/command.ts";
import { defaultSettings } from "../common/util.ts"; import { defaultSettings } from "../common/util.ts";
// Used by FilterBox // Used by FilterBox

View File

@ -13,52 +13,49 @@ Space scripts are defined by simply using `space-script` fenced code blocks in y
Here is a trivial example: Here is a trivial example:
```space-script ```space-script
silverbullet.registerFunction("helloSayer", (name) => { silverbullet.registerFunction("helloYeller", (name) => {
return `Hello ${name}!`; return `Hello ${name}!`.toUpperCase();
}) })
``` ```
You can now invoke this function as follows: You can now invoke this function in a template or query:
```template ```template
{{helloSayer("Pete")}} {{helloYeller("Pete")}}
``` ```
Upon client and server boot, all indexed scripts will be loaded and activated. To reload scripts on-demand, use the {[System: Reload]} command (bound to `Ctrl-Alt-r` for convenience). Upon client and server boot, all indexed scripts will be loaded and activated. To reload scripts on-demand, use the {[System: Reload]} command (bound to `Ctrl-Alt-r` for convenience).
If you use things like `console.log` in your script, you will see this output either in your servers logs or browsers JavaScript console (depending on how the script will be invoked). If you use things like `console.log` in your script, you will see this output either in your servers logs or browsers JavaScript console (depending on where the script will be invoked).
# Runtime Environment & API # Runtime Environment & API
Space script is loaded both in the client and server (or only client, if you run in [[Install/Configuration#Security]] `SB_SYNC_ONLY` mode). Space script is loaded directly in the browser environment on the client, and the Deno environment on the server.
Depending on where code is run, a slightly different JavaScript API will be available. However, code should ideally primarily rely on the following explicitly exposed APIs: While not very secure, some effort is put into running this code in a clean JavaScript environment, as such the following global variables are not available: `this`, `self`, `Deno`, `window`, and `globalThis`.
* `silverbullet.registerFunction(name, callback)`: register a custom function (see below) Depending on where code is run (client or server), a slightly different JavaScript API will be available. However, code should ideally primarily rely on the following explicitly exposed APIs:
* `syscall(name, args...)`: invoke a syscall
Many other standard JavaScript APIs are also available, such as: * `silverbullet.registerFunction(name, callback)`: registers a custom function (see [[#Custom functions]]).
* `silverbullet.registerCommand(definition, callback)`: registers a custom command (see [[#Custom commands]]).
* `syscall(name, args...)`: invoke a syscall (see [[#Syscalls]]).
* [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) Many useful standard JavaScript APIs are available, such as:
* [Temporal](https://tc39.es/proposal-temporal/docs/)
* [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (making fetch calls directly from the browser on the client, and via Denos fetch implementation on the server)
* [Temporal](https://tc39.es/proposal-temporal/docs/) (implemented via a polyfill)
# Custom functions # Custom functions
SilverBullet offers a set of [[Functions]] you can use in its [[Template Language]]. You can extend this set of functions using space script using the `silverbullet.registerFunction` API. SilverBullet offers a set of [[Functions]] you can use in its [[Template Language]]. You can extend this set of functions using space script using the `silverbullet.registerFunction` API.
Here is a simple example: Since template rendering happens on the server (except in [[Client Modes#Synced mode]]), this logic is typically executed on the server.
```space-script The `silverbullet.registerFunction` API takes two arguments:
silverbullet.registerFunction("helloSayer", (name) => {
return `Hello ${name}!`;
})
```
You can now invoke this function as follows: * `name`: the function name to register
* `callback`: the callback function to invoke (can be `async` or not)
```template ## Example
{{helloSayer("Pete")}} Even though a [[Functions#readPage(name)]] function already exist, you could implement it in space script as follows (lets name it `myReadPage`) using the `syscall` API (detailed further in [[#Syscalls]]):
```
Even though a [[Functions#readPage(name)]] function already exist, you could implement it in Space Script as follows (lets name it `myReadPage` instead) using the `syscall` API (detailed further in [[#Syscalls]]):
```space-script ```space-script
silverbullet.registerFunction("myReadPage", async (name) => { silverbullet.registerFunction("myReadPage", async (name) => {
@ -75,8 +72,35 @@ This function can be invoked as follows:
{{myReadPage("internal/test page")}} {{myReadPage("internal/test page")}}
``` ```
# Custom commands
You can also define custom commands using space script. Commands are _always_ executed on the client.
Here is an example of defining a custom command using space script:
```space-script
silverbullet.registerCommand({name: "My First Command"}, async () => {
await syscall("editor.flashNotification", "Hello there!");
});
```
You can run it via the command palette, or by pushing this [[Markdown/Command links|command link]]: {[My First Command]}
The `silverbullet.registerCommand` API takes two arguments:
* `options`:
* `name`: Name of the command
* `key` (optional): Keyboard shortcut for the command (Windows/Linux)
* `mac` (optional): Mac keyboard shortcut for the command
* `hide` (optional): Do not show this command in the command palette
* `requireMode` (optional): Only make this command available in `ro` or `rw` mode.
* `callback`: the callback function to invoke (can be `async` or not)
# Syscalls # Syscalls
You can invoke syscalls to get access to various useful SilverBullet APIs. A syscall is invoked via `syscall(name, arg1, arg2)` and returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with the result. The primary way to interact with the SilverBullet environment is using “syscalls”. Syscalls expose SilverBullet functionality largely available both on the client and server in a safe way.
In your space script, a syscall is invoked via `syscall(name, arg1, arg2)` and usually returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) with the result.
Here are all available syscalls:
```template ```template
{{#each @module in {syscall select replace(name, /\.\w+$/, "") as name}}} {{#each @module in {syscall select replace(name, /\.\w+$/, "") as name}}}
@ -84,5 +108,6 @@ You can invoke syscalls to get access to various useful SilverBullet APIs. A sys
{{#each {syscall where @module.name = replace(name, /\.\w+$/, "")}}} {{#each {syscall where @module.name = replace(name, /\.\w+$/, "")}}}
* `{{name}}` * `{{name}}`
{{/each}} {{/each}}
{{/each}} {{/each}}
``` ```