Refactoring and adding ability to create custom commands from space functions
parent
b3dc303624
commit
05efbc8741
|
@ -8,6 +8,9 @@ import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
|||
import { LocalShell } from "../server/shell_backend.ts";
|
||||
import { Hono } from "../server/deps.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(
|
||||
spacePath: string,
|
||||
|
@ -23,6 +26,10 @@ export async function runPlug(
|
|||
|
||||
const endpointHook = new EndpointHook("/_/");
|
||||
|
||||
const ds = new DataStore(kvPrimitives);
|
||||
const mq = new DataStoreMQ(ds);
|
||||
const eventHook = new EventHook();
|
||||
|
||||
const serverSystem = new ServerSystem(
|
||||
new AssetBundlePlugSpacePrimitives(
|
||||
new DiskSpacePrimitives(spacePath),
|
||||
|
@ -30,6 +37,9 @@ export async function runPlug(
|
|||
),
|
||||
kvPrimitives,
|
||||
new LocalShell(spacePath),
|
||||
mq,
|
||||
ds,
|
||||
eventHook,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -43,7 +43,10 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
|
|||
editorCommands = new Map<string, AppCommand>();
|
||||
system!: System<CommandHookT>;
|
||||
|
||||
constructor(private readOnly: boolean) {
|
||||
constructor(
|
||||
private readOnly: boolean,
|
||||
private additionalCommandsMap: Map<string, AppCommand>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
@ -76,6 +79,9 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
|
|||
}
|
||||
}
|
||||
await this.loadPageTemplateCommands();
|
||||
for (const [name, cmd] of this.additionalCommandsMap) {
|
||||
this.editorCommands.set(name, cmd);
|
||||
}
|
||||
this.emit("commandsUpdated", this.editorCommands);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import * as plugos from "../plugos/types.ts";
|
||||
import { CronHookT } from "../plugos/hooks/cron.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 { PlugNamespaceHookT } from "./hooks/plug_namespace.ts";
|
||||
import { CodeWidgetT } from "../web/hooks/code_widget.ts";
|
||||
|
|
|
@ -3,29 +3,15 @@ import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
|
|||
import { System } from "../plugos/system.ts";
|
||||
import { Query } from "$sb/types.ts";
|
||||
import { LimitedMap } from "$sb/lib/limited_map.ts";
|
||||
import { ScriptEnvironment } from "./space_script.ts";
|
||||
|
||||
const pageCacheTtl = 10 * 1000; // 10s
|
||||
|
||||
export async function buildQueryFunctions(
|
||||
export function buildQueryFunctions(
|
||||
allKnownPages: Set<string>,
|
||||
system: System<any>,
|
||||
enableSpaceScript: boolean,
|
||||
): Promise<FunctionMap> {
|
||||
): FunctionMap {
|
||||
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 {
|
||||
...builtinFunctions,
|
||||
pageExists(name: string) {
|
||||
|
@ -54,7 +40,7 @@ export async function buildQueryFunctions(
|
|||
if (cachedPage) {
|
||||
return cachedPage;
|
||||
} else {
|
||||
return system.syscall({}, "space.readPage", [name]).then((page) => {
|
||||
return system.localSyscall("space.readPage", [name]).then((page) => {
|
||||
pageCache.set(name, page, pageCacheTtl);
|
||||
return page;
|
||||
}).catch((e: any) => {
|
||||
|
@ -64,6 +50,5 @@ export async function buildQueryFunctions(
|
|||
});
|
||||
}
|
||||
},
|
||||
...scriptEnv.functions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,21 +1,47 @@
|
|||
// deno-lint-ignore-file ban-types
|
||||
import { System } from "../plugos/system.ts";
|
||||
import { ScriptObject } from "../plugs/index/script.ts";
|
||||
import { AppCommand, CommandDef } from "./hooks/command.ts";
|
||||
|
||||
export class ScriptEnvironment {
|
||||
functions: Record<string, Function> = {};
|
||||
functions: Record<string, (...args: any[]) => any> = {};
|
||||
commands: Record<string, AppCommand> = {};
|
||||
|
||||
// Public API
|
||||
registerFunction(name: string, fn: Function) {
|
||||
registerFunction(name: string, fn: (...args: any[]) => any) {
|
||||
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
|
||||
evalScript(script: string, system: System<any>) {
|
||||
try {
|
||||
Function("silverbullet", "syscall", script)(
|
||||
const fn = Function(
|
||||
"silverbullet",
|
||||
"syscall",
|
||||
"Deno",
|
||||
"window",
|
||||
"globalThis",
|
||||
"self",
|
||||
script,
|
||||
);
|
||||
fn.call(
|
||||
{},
|
||||
this,
|
||||
(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) {
|
||||
throw new Error(
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { SyscallMeta } from "$sb/types.ts";
|
||||
import { SysCallMapping, System } from "../../plugos/system.ts";
|
||||
import type { ServerSystem } from "../../server/server_system.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 type { CommonSystem } from "../common_system.ts";
|
||||
|
||||
export function systemSyscalls(
|
||||
system: System<any>,
|
||||
readOnlyMode: boolean,
|
||||
client: Client | undefined,
|
||||
serverSystem: ServerSystem | undefined,
|
||||
commonSystem: CommonSystem,
|
||||
client?: Client,
|
||||
): SysCallMapping {
|
||||
const api: SysCallMapping = {
|
||||
"system.invokeFunction": (
|
||||
|
@ -50,8 +50,7 @@ export function systemSyscalls(
|
|||
return client.runCommandByName(name, args);
|
||||
},
|
||||
"system.listCommands": (): { [key: string]: CommandDef } => {
|
||||
const commandHook = client?.system.commandHook ||
|
||||
serverSystem!.commandHook;
|
||||
const commandHook = commonSystem!.commandHook;
|
||||
const allCommands: { [key: string]: CommandDef } = {};
|
||||
for (const [cmd, def] of commandHook.editorCommands) {
|
||||
allCommands[cmd] = def.command;
|
||||
|
@ -76,9 +75,9 @@ export function systemSyscalls(
|
|||
return client.loadPlugs();
|
||||
},
|
||||
"system.loadSpaceScripts": async () => {
|
||||
// Reload scripts locally
|
||||
await commonSystem.loadSpaceScripts();
|
||||
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
|
||||
if (system.env === "client") {
|
||||
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": () => {
|
||||
|
|
|
@ -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 { syscall } from "./syscall.ts";
|
||||
|
||||
|
|
|
@ -5,7 +5,10 @@ import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts
|
|||
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||
import { ensureAndLoadSettingsAndIndex } from "../common/util.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 { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
||||
import { System } from "../plugos/system.ts";
|
||||
import { BuiltinSettings } from "../web/types.ts";
|
||||
import { JWTIssuer } from "./crypto.ts";
|
||||
|
@ -30,6 +33,7 @@ export type SpaceServerConfig = {
|
|||
clientEncryption: boolean;
|
||||
};
|
||||
|
||||
// Equivalent of Client on the server
|
||||
export class SpaceServer {
|
||||
public pagesPath: string;
|
||||
auth?: { user: string; pass: string };
|
||||
|
@ -99,6 +103,11 @@ export class SpaceServer {
|
|||
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)
|
||||
if (!this.syncOnly) {
|
||||
// Enable server-side processing
|
||||
|
@ -106,6 +115,9 @@ export class SpaceServer {
|
|||
this.spacePrimitives,
|
||||
this.kvPrimitives,
|
||||
this.shellBackend,
|
||||
mq,
|
||||
ds,
|
||||
eventHook,
|
||||
this.readOnly,
|
||||
this.enableSpaceScript,
|
||||
);
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
dataStoreReadSyscalls,
|
||||
dataStoreWriteSyscalls,
|
||||
} from "../plugos/syscalls/datastore.ts";
|
||||
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
||||
import { languageSyscalls } from "../common/syscalls/language.ts";
|
||||
import { templateSyscalls } from "../common/syscalls/template.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 { ensureSpaceIndex } from "../common/space_index.ts";
|
||||
import { FileMeta } from "$sb/types.ts";
|
||||
import { buildQueryFunctions } from "../common/query_functions.ts";
|
||||
import { CommandHook } from "../web/hooks/command.ts";
|
||||
|
||||
// // Important: load this before the actual plugs
|
||||
// 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";
|
||||
import { CommandHook } from "../common/hooks/command.ts";
|
||||
import { CommonSystem } from "../common/common_system.ts";
|
||||
import { MessageQueue } from "../plugos/lib/mq.ts";
|
||||
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
||||
|
||||
const fileListInterval = 30 * 1000; // 30s
|
||||
|
||||
const plugNameExtractRegex = /([^/]+)\.plug\.js$/;
|
||||
|
||||
export class ServerSystem {
|
||||
system!: System<SilverBulletHooks>;
|
||||
public spacePrimitives!: SpacePrimitives;
|
||||
// denoKv!: Deno.Kv;
|
||||
export class ServerSystem extends CommonSystem {
|
||||
listInterval?: number;
|
||||
ds!: DataStore;
|
||||
allKnownPages = new Set<string>();
|
||||
commandHook!: CommandHook;
|
||||
|
||||
constructor(
|
||||
private baseSpacePrimitives: SpacePrimitives,
|
||||
readonly kvPrimitives: KvPrimitives,
|
||||
public spacePrimitives: SpacePrimitives,
|
||||
private kvPrimitives: KvPrimitives,
|
||||
private shellBackend: ShellBackend,
|
||||
private readOnlyMode: boolean,
|
||||
private enableSpaceScript: boolean,
|
||||
mq: DataStoreMQ,
|
||||
ds: DataStore,
|
||||
eventHook: EventHook,
|
||||
readOnlyMode: boolean,
|
||||
enableSpaceScript: boolean,
|
||||
) {
|
||||
super(mq, ds, eventHook, readOnlyMode, enableSpaceScript);
|
||||
}
|
||||
|
||||
// Always needs to be invoked right after construction
|
||||
|
@ -94,24 +79,20 @@ export class ServerSystem {
|
|||
this.system.addHook(eventHook);
|
||||
|
||||
// Command hook, just for introspection
|
||||
this.commandHook = new CommandHook(this.readOnlyMode);
|
||||
this.commandHook = new CommandHook(
|
||||
this.readOnlyMode,
|
||||
this.spaceScriptCommands,
|
||||
);
|
||||
this.system.addHook(this.commandHook);
|
||||
|
||||
// Cron hook
|
||||
const cronHook = new CronHook(this.system);
|
||||
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();
|
||||
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();
|
||||
|
||||
|
@ -119,7 +100,7 @@ export class ServerSystem {
|
|||
|
||||
this.spacePrimitives = new EventedSpacePrimitives(
|
||||
new PlugSpacePrimitives(
|
||||
this.baseSpacePrimitives,
|
||||
this.spacePrimitives,
|
||||
plugNamespaceHook,
|
||||
),
|
||||
eventHook,
|
||||
|
@ -133,8 +114,8 @@ export class ServerSystem {
|
|||
spaceReadSyscalls(space),
|
||||
assetSyscalls(this.system),
|
||||
yamlSyscalls(),
|
||||
systemSyscalls(this.system, this.readOnlyMode, undefined, this),
|
||||
mqSyscalls(mq),
|
||||
systemSyscalls(this.system, this.readOnlyMode, this),
|
||||
mqSyscalls(this.mq),
|
||||
languageSyscalls(),
|
||||
templateSyscalls(this.ds),
|
||||
dataStoreReadSyscalls(this.ds),
|
||||
|
@ -167,9 +148,6 @@ export class ServerSystem {
|
|||
await this.loadSpaceScripts();
|
||||
|
||||
this.listInterval = setInterval(() => {
|
||||
// runWithSystemLock(this.system, async () => {
|
||||
// await space.updatePageList();
|
||||
// });
|
||||
space.updatePageList().catch(console.error);
|
||||
}, fileListInterval);
|
||||
|
||||
|
@ -211,9 +189,7 @@ export class ServerSystem {
|
|||
await indexPromise;
|
||||
}
|
||||
|
||||
// await runWithSystemLock(this.system, async () => {
|
||||
await eventHook.dispatchEvent("system:ready");
|
||||
// });
|
||||
}
|
||||
|
||||
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>> {
|
||||
const { meta, data } = await this.spacePrimitives.readFile(path);
|
||||
const plugName = path.match(plugNameExtractRegex)![1];
|
||||
|
|
|
@ -10,6 +10,12 @@ import { plugCompileCommand } from "./cmd/plug_compile.ts";
|
|||
import { plugRunCommand } from "./cmd/plug_run.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()
|
||||
.name("silverbullet")
|
||||
.description("Markdown as a platform")
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Space } from "./space.ts";
|
|||
import { FilterOption } from "./types.ts";
|
||||
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
|
||||
import { EventHook } from "../plugos/hooks/event.ts";
|
||||
import { AppCommand } from "./hooks/command.ts";
|
||||
import { AppCommand } from "../common/hooks/command.ts";
|
||||
import {
|
||||
PageState,
|
||||
parsePageRefFromURI,
|
||||
|
@ -57,7 +57,6 @@ import {
|
|||
} from "../common/space_index.ts";
|
||||
import { LimitedMap } from "$sb/lib/limited_map.ts";
|
||||
import { renderTheTemplate } from "../common/syscalls/template.ts";
|
||||
import { buildQueryFunctions } from "../common/query_functions.ts";
|
||||
import { PageRef } from "$sb/lib/page.ts";
|
||||
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
|
||||
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
|
||||
|
@ -83,7 +82,7 @@ declare global {
|
|||
// history.scrollRestoration = "manual";
|
||||
|
||||
export class Client {
|
||||
system!: ClientSystem;
|
||||
clientSystem!: ClientSystem;
|
||||
editorView!: EditorView;
|
||||
keyHandlerCompartment?: Compartment;
|
||||
|
||||
|
@ -116,8 +115,6 @@ export class Client {
|
|||
spaceKV?: KvPrimitives;
|
||||
mq!: DataStoreMQ;
|
||||
|
||||
// Used by the "wiki link" highlighter to check if a page exists
|
||||
public allKnownPages = new Set<string>();
|
||||
onLoadPageRef: PageRef;
|
||||
|
||||
constructor(
|
||||
|
@ -148,15 +145,11 @@ export class Client {
|
|||
// Setup message queue
|
||||
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
|
||||
this.eventHook = new EventHook();
|
||||
|
||||
// Instantiate a PlugOS system
|
||||
this.system = new ClientSystem(
|
||||
this.clientSystem = new ClientSystem(
|
||||
this,
|
||||
this.mq,
|
||||
this.stateDataStore,
|
||||
|
@ -193,7 +186,7 @@ export class Client {
|
|||
|
||||
this.focus();
|
||||
|
||||
this.system.init();
|
||||
this.clientSystem.init();
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
|
@ -224,7 +217,7 @@ export class Client {
|
|||
}
|
||||
|
||||
await this.loadPlugs();
|
||||
await this.loadSpaceScripts();
|
||||
await this.clientSystem.loadSpaceScripts();
|
||||
|
||||
await this.initNavigator();
|
||||
await this.initSync();
|
||||
|
@ -246,15 +239,6 @@ export class Client {
|
|||
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() {
|
||||
this.settings = await ensureAndLoadSettingsAndIndex(
|
||||
this.space.spacePrimitives,
|
||||
|
@ -286,14 +270,14 @@ export class Client {
|
|||
// A full sync just completed
|
||||
if (!initialSync) {
|
||||
// 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,
|
||||
);
|
||||
} else {
|
||||
// This was the initial sync, let's mark a full index as completed
|
||||
await markFullSpaceIndexComplete(this.stateDataStore);
|
||||
// And load space scripts, which probably weren't loaded before
|
||||
await this.loadSpaceScripts();
|
||||
await this.clientSystem.loadSpaceScripts();
|
||||
}
|
||||
}
|
||||
if (operations) {
|
||||
|
@ -340,7 +324,6 @@ export class Client {
|
|||
pageState.header !== undefined))
|
||||
) {
|
||||
setTimeout(() => {
|
||||
console.log("Kicking off scroll to", pageState.scrollTop);
|
||||
this.editorView.scrollDOM.scrollTop = pageState.scrollTop!;
|
||||
});
|
||||
adjustedPosition = true;
|
||||
|
@ -544,7 +527,7 @@ export class Client {
|
|||
|
||||
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
||||
remoteSpacePrimitives,
|
||||
this.system.namespaceHook,
|
||||
this.clientSystem.namespaceHook,
|
||||
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)
|
||||
this.eventHook.addLocalListener("page:saved", (pageName: string) => {
|
||||
// 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.allKnownPages.delete(pageName);
|
||||
this.clientSystem.allKnownPages.delete(pageName);
|
||||
});
|
||||
|
||||
this.eventHook.addLocalListener(
|
||||
"file:listed",
|
||||
(allFiles: FileMeta[]) => {
|
||||
// Update list of known pages
|
||||
this.allKnownPages.clear();
|
||||
this.clientSystem.allKnownPages.clear();
|
||||
allFiles.forEach((f) => {
|
||||
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() {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
const allPages = await this.system.queryObjects<PageMeta>("page", {});
|
||||
const allPages = await this.clientSystem.queryObjects<PageMeta>("page", {});
|
||||
this.ui.viewDispatch({
|
||||
type: "update-page-list",
|
||||
allPages,
|
||||
|
@ -828,7 +811,7 @@ export class Client {
|
|||
}
|
||||
|
||||
async loadPlugs() {
|
||||
await this.system.reloadPlugsFromSpace(this.space);
|
||||
await this.clientSystem.reloadPlugsFromSpace(this.space);
|
||||
await this.eventHook.dispatchEvent("system:ready");
|
||||
await this.dispatchAppEvent("plugs:loaded");
|
||||
}
|
||||
|
@ -932,6 +915,7 @@ export class Client {
|
|||
viewState.showPrompt,
|
||||
].some(Boolean)
|
||||
) {
|
||||
console.log("not focusing");
|
||||
// Some other modal UI element is visible, don't focus editor now
|
||||
return;
|
||||
}
|
||||
|
@ -965,7 +949,7 @@ export class Client {
|
|||
"Navigating to new page in new window",
|
||||
`${location.origin}/${encodePageRef(pageRef)}`,
|
||||
);
|
||||
const win = window.open(
|
||||
const win = globalThis.open(
|
||||
`${location.origin}/${encodePageRef(pageRef)}`,
|
||||
"_blank",
|
||||
);
|
||||
|
@ -1021,13 +1005,14 @@ export class Client {
|
|||
perm: "rw",
|
||||
} as PageMeta,
|
||||
};
|
||||
this.system.system.invokeFunction("template.newPage", [pageName]).then(
|
||||
() => {
|
||||
this.focus();
|
||||
},
|
||||
).catch(
|
||||
console.error,
|
||||
);
|
||||
this.clientSystem.system.invokeFunction("template.newPage", [pageName])
|
||||
.then(
|
||||
() => {
|
||||
this.focus();
|
||||
},
|
||||
).catch(
|
||||
console.error,
|
||||
);
|
||||
} else {
|
||||
this.flashNotification(
|
||||
`Could not load page ${pageName}: ${e.message}`,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { eventSyscalls } from "../plugos/syscalls/event.ts";
|
|||
import { System } from "../plugos/system.ts";
|
||||
import type { Client } from "./client.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 { clientStoreSyscalls } from "./syscalls/clientStore.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 { PanelWidgetHook } from "./hooks/panel_widget.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$/;
|
||||
|
||||
export class ClientSystem {
|
||||
commandHook: CommandHook;
|
||||
slashCommandHook: SlashCommandHook;
|
||||
namespaceHook: PlugNamespaceHook;
|
||||
codeWidgetHook: CodeWidgetHook;
|
||||
system: System<SilverBulletHooks>;
|
||||
panelWidgetHook: PanelWidgetHook;
|
||||
|
||||
/**
|
||||
* Wrapper around a System, used by the client
|
||||
*/
|
||||
export class ClientSystem extends CommonSystem {
|
||||
constructor(
|
||||
private client: Client,
|
||||
private mq: MessageQueue,
|
||||
private ds: DataStore,
|
||||
private eventHook: EventHook,
|
||||
private readOnlyMode: boolean,
|
||||
mq: DataStoreMQ,
|
||||
ds: DataStore,
|
||||
eventHook: EventHook,
|
||||
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)
|
||||
this.system = new System(
|
||||
client.syncMode ? undefined : "client",
|
||||
|
@ -95,7 +100,10 @@ export class ClientSystem {
|
|||
}
|
||||
|
||||
// Command hook
|
||||
this.commandHook = new CommandHook(this.readOnlyMode);
|
||||
this.commandHook = new CommandHook(
|
||||
this.readOnlyMode,
|
||||
this.spaceScriptCommands,
|
||||
);
|
||||
this.commandHook.on({
|
||||
commandsUpdated: (commandMap) => {
|
||||
this.client.ui?.viewDispatch({
|
||||
|
@ -158,7 +166,7 @@ export class ClientSystem {
|
|||
eventSyscalls(this.eventHook),
|
||||
editorSyscalls(this.client),
|
||||
spaceReadSyscalls(this.client),
|
||||
systemSyscalls(this.system, false, this.client, undefined),
|
||||
systemSyscalls(this.system, false, this, this.client),
|
||||
markdownSyscalls(),
|
||||
assetSyscalls(this.system),
|
||||
yamlSyscalls(),
|
||||
|
|
|
@ -22,12 +22,13 @@ export function fencedCodePlugin(editor: Client) {
|
|||
}
|
||||
const text = state.sliceDoc(from, to);
|
||||
const [_, lang] = text.match(/^```(\w+)?/)!;
|
||||
const codeWidgetCallback = editor.system.codeWidgetHook
|
||||
const codeWidgetCallback = editor.clientSystem.codeWidgetHook
|
||||
.codeWidgetCallbacks
|
||||
.get(lang);
|
||||
const renderMode = editor.system.codeWidgetHook.codeWidgetModes.get(
|
||||
lang,
|
||||
);
|
||||
const renderMode = editor.clientSystem.codeWidgetHook.codeWidgetModes
|
||||
.get(
|
||||
lang,
|
||||
);
|
||||
// Only custom render when we have a custom renderer, and the current page is not a template
|
||||
if (codeWidgetCallback && !isTemplate(state.sliceDoc(0, from))) {
|
||||
// We got a custom renderer!
|
||||
|
|
|
@ -64,7 +64,7 @@ export class MarkdownWidget extends WidgetType {
|
|||
extendedMarkdownLanguage,
|
||||
widgetContent.markdown!,
|
||||
);
|
||||
mdTree = await this.client.system.localSyscall(
|
||||
mdTree = await this.client.clientSystem.localSyscall(
|
||||
"system.invokeFunction",
|
||||
[
|
||||
"markdown.expandCodeWidgets",
|
||||
|
@ -215,7 +215,7 @@ export class MarkdownWidget extends WidgetType {
|
|||
// Update state in DOM as well for future toggles
|
||||
e.target.dataset.state = newState;
|
||||
console.log("Toggling task", taskRef);
|
||||
this.client.system.localSyscall(
|
||||
this.client.clientSystem.localSyscall(
|
||||
"system.invokeFunction",
|
||||
["tasks.updateTaskState", taskRef, oldState, newState],
|
||||
).catch(
|
||||
|
@ -234,7 +234,7 @@ export class MarkdownWidget extends WidgetType {
|
|||
if (button.widgetTarget) {
|
||||
div.addEventListener("click", () => {
|
||||
console.log("Widget clicked");
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
this.client.clientSystem.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).catch(console.error);
|
||||
|
@ -245,7 +245,7 @@ export class MarkdownWidget extends WidgetType {
|
|||
(e) => {
|
||||
e.stopPropagation();
|
||||
console.log("Button clicked:", button.description);
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
this.client.clientSystem.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).then((newContent: string | undefined) => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MarkdownWidget } from "./markdown_widget.ts";
|
|||
export function postScriptPrefacePlugin(
|
||||
editor: Client,
|
||||
) {
|
||||
const panelWidgetHook = editor.system.panelWidgetHook;
|
||||
const panelWidgetHook = editor.clientSystem.panelWidgetHook;
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
const topCallback = panelWidgetHook.callbacks.get("top");
|
||||
|
|
|
@ -33,7 +33,7 @@ export function cleanWikiLinkPlugin(client: Client) {
|
|||
const pageRef = parsePageRef(page);
|
||||
pageRef.page = resolvePath(client.currentPage, pageRef.page);
|
||||
const lowerCasePageName = pageRef.page.toLowerCase();
|
||||
for (const pageName of client.allKnownPages) {
|
||||
for (const pageName of client.clientSystem.allKnownPages) {
|
||||
if (pageName.toLowerCase() === lowerCasePageName) {
|
||||
pageExists = true;
|
||||
break;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { isMacLike } from "../../common/util.ts";
|
||||
import { FilterList } from "./filter.tsx";
|
||||
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 { parseCommand } from "../../common/command.ts";
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export function Panel({
|
|||
break;
|
||||
case "syscall": {
|
||||
const { id, name, args } = data;
|
||||
editor.system.localSyscall(name, args).then(
|
||||
editor.clientSystem.localSyscall(name, args).then(
|
||||
(result) => {
|
||||
if (!iFrameRef.current?.contentWindow) {
|
||||
// iFrame already went away
|
||||
|
|
|
@ -123,7 +123,7 @@ export function mountIFrame(
|
|||
case "syscall": {
|
||||
const { id, name, args } = data;
|
||||
try {
|
||||
const result = await client.system.localSyscall(name, args);
|
||||
const result = await client.clientSystem.localSyscall(name, args);
|
||||
if (!iframe.contentWindow) {
|
||||
// iFrame already went away
|
||||
return;
|
||||
|
|
|
@ -99,8 +99,8 @@ export function createEditorState(
|
|||
autocompletion({
|
||||
override: [
|
||||
client.editorComplete.bind(client),
|
||||
client.system.slashCommandHook.slashCommandCompleter.bind(
|
||||
client.system.slashCommandHook,
|
||||
client.clientSystem.slashCommandHook.slashCommandCompleter.bind(
|
||||
client.clientSystem.slashCommandHook,
|
||||
),
|
||||
],
|
||||
}),
|
||||
|
@ -299,7 +299,7 @@ export function createCommandKeyBindings(client: Client): KeyBinding[] {
|
|||
}
|
||||
|
||||
// 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 we've already overridden this command, skip it
|
||||
if (overriddenCommands.has(def.command.key)) {
|
||||
|
|
|
@ -210,7 +210,7 @@ export class MainUI {
|
|||
return;
|
||||
}
|
||||
console.log("Now renaming page to...", newName);
|
||||
await client.system.system.invokeFunction(
|
||||
await client.clientSystem.system.invokeFunction(
|
||||
"index.renamePageCommand",
|
||||
[{ page: newName }],
|
||||
);
|
||||
|
@ -250,26 +250,29 @@ export class MainUI {
|
|||
}]
|
||||
: [],
|
||||
...viewState.settings.actionButtons
|
||||
.filter((button) => (typeof button.mobile === "undefined") || (button.mobile === viewState.isMobile))
|
||||
.map((button) => {
|
||||
const parsedCommand = parseCommand(button.command);
|
||||
let featherIcon =
|
||||
(featherIcons as any)[kebabToCamel(button.icon)];
|
||||
if (!featherIcon) {
|
||||
featherIcon = featherIcons.HelpCircle;
|
||||
}
|
||||
return {
|
||||
icon: featherIcon,
|
||||
description: button.description || "",
|
||||
callback: () => {
|
||||
client.runCommandByName(
|
||||
parsedCommand.name,
|
||||
parsedCommand.args,
|
||||
);
|
||||
},
|
||||
href: "",
|
||||
};
|
||||
}),
|
||||
.filter((button) =>
|
||||
(typeof button.mobile === "undefined") ||
|
||||
(button.mobile === viewState.isMobile)
|
||||
)
|
||||
.map((button) => {
|
||||
const parsedCommand = parseCommand(button.command);
|
||||
let featherIcon =
|
||||
(featherIcons as any)[kebabToCamel(button.icon)];
|
||||
if (!featherIcon) {
|
||||
featherIcon = featherIcons.HelpCircle;
|
||||
}
|
||||
return {
|
||||
icon: featherIcon,
|
||||
description: button.description || "",
|
||||
callback: () => {
|
||||
client.runCommandByName(
|
||||
parsedCommand.name,
|
||||
parsedCommand.args,
|
||||
);
|
||||
},
|
||||
href: "",
|
||||
};
|
||||
}),
|
||||
]}
|
||||
rhs={!!viewState.panels.rhs.mode && (
|
||||
<div
|
||||
|
|
|
@ -116,7 +116,7 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
|||
});
|
||||
// Replace with whatever the completion is
|
||||
safeRun(async () => {
|
||||
await this.editor.system.system.invokeFunction(
|
||||
await this.editor.clientSystem.system.invokeFunction(
|
||||
slashCompletion.invoke,
|
||||
[slashCompletion],
|
||||
);
|
||||
|
|
|
@ -60,8 +60,10 @@ export function editorSyscalls(client: Client): SysCallMapping {
|
|||
},
|
||||
"editor.reloadSettingsAndCommands": async () => {
|
||||
await client.loadSettings();
|
||||
await client.system.commandHook.buildAllCommands();
|
||||
await client.system.system.localSyscall("system.loadSpaceScripts", []);
|
||||
await client.clientSystem.system.localSyscall(
|
||||
"system.loadSpaceScripts",
|
||||
[],
|
||||
);
|
||||
},
|
||||
"editor.openUrl": (_ctx, url: string, existingWindow = false) => {
|
||||
if (!existingWindow) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Manifest } from "../common/manifest.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";
|
||||
|
||||
// Used by FilterBox
|
||||
|
|
|
@ -13,52 +13,49 @@ Space scripts are defined by simply using `space-script` fenced code blocks in y
|
|||
Here is a trivial example:
|
||||
|
||||
```space-script
|
||||
silverbullet.registerFunction("helloSayer", (name) => {
|
||||
return `Hello ${name}!`;
|
||||
silverbullet.registerFunction("helloYeller", (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
|
||||
{{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).
|
||||
|
||||
If you use things like `console.log` in your script, you will see this output either in your server’s logs or browser’s 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 server’s logs or browser’s JavaScript console (depending on where the script will be invoked).
|
||||
|
||||
# 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)
|
||||
* `syscall(name, args...)`: invoke a syscall
|
||||
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:
|
||||
|
||||
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)
|
||||
* [Temporal](https://tc39.es/proposal-temporal/docs/)
|
||||
Many useful standard JavaScript APIs are available, such as:
|
||||
|
||||
* [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (making fetch calls directly from the browser on the client, and via Deno’s fetch implementation on the server)
|
||||
* [Temporal](https://tc39.es/proposal-temporal/docs/) (implemented via a polyfill)
|
||||
|
||||
# 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.
|
||||
|
||||
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
|
||||
silverbullet.registerFunction("helloSayer", (name) => {
|
||||
return `Hello ${name}!`;
|
||||
})
|
||||
```
|
||||
The `silverbullet.registerFunction` API takes two arguments:
|
||||
|
||||
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
|
||||
{{helloSayer("Pete")}}
|
||||
```
|
||||
|
||||
Even though a [[Functions#readPage(name)]] function already exist, you could implement it in Space Script as follows (let’s name it `myReadPage` instead) using the `syscall` API (detailed further in [[#Syscalls]]):
|
||||
## Example
|
||||
Even though a [[Functions#readPage(name)]] function already exist, you could implement it in space script as follows (let’s name it `myReadPage`) using the `syscall` API (detailed further in [[#Syscalls]]):
|
||||
|
||||
```space-script
|
||||
silverbullet.registerFunction("myReadPage", async (name) => {
|
||||
|
@ -75,8 +72,35 @@ This function can be invoked as follows:
|
|||
{{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
|
||||
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
|
||||
{{#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+$/, "")}}}
|
||||
* `{{name}}`
|
||||
{{/each}}
|
||||
|
||||
{{/each}}
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue