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 { 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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>();
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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": () => {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 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
|
# 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 Deno’s 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 (let’s 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 (let’s 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}}
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in New Issue